<?php

declare (strict_types=1);
namespace Syde\Vendor\Inpsyde\Assets\Loader;

use Syde\Vendor\Inpsyde\Assets\Asset;
use Syde\Vendor\Inpsyde\Assets\BaseAsset;
use Syde\Vendor\Inpsyde\Assets\ConfigureAutodiscoverVersionTrait;
use Syde\Vendor\Inpsyde\Assets\Exception\FileNotFoundException;
use Syde\Vendor\Inpsyde\Assets\Exception\InvalidResourceException;
use Syde\Vendor\Inpsyde\Assets\Script;
use Syde\Vendor\Inpsyde\Assets\ScriptModule;
use Syde\Vendor\Inpsyde\Assets\Style;
abstract class AbstractWebpackLoader implements LoaderInterface
{
    use ConfigureAutodiscoverVersionTrait;
    protected string $directoryUrl = '';
    /**
     * @param string $directoryUrl optional directory URL which will be used for the Asset
     *
     * @return static
     */
    public function withDirectoryUrl(string $directoryUrl): AbstractWebpackLoader
    {
        $this->directoryUrl = $directoryUrl;
        return $this;
    }
    /**
     * @param array<string, string> $data
     * @param string $resource
     *
     * @return Asset[]
     */
    abstract protected function parseData(array $data, string $resource): array;
    /**
     * @param mixed $resource
     *
     * @return Asset[]
     *
     * phpcs:disable Syde.Functions.ArgumentTypeDeclaration.NoArgumentType
     * @psalm-suppress MixedArgument
     */
    public function load($resource): array
    {
        if (!is_string($resource) || !is_readable($resource)) {
            throw new FileNotFoundException(sprintf('The given file "%s" does not exists or is not readable.', esc_html($resource)));
        }
        $data = @file_get_contents($resource) ?: '';
        // phpcs:ignore
        $data = json_decode($data, \true);
        $errorCode = json_last_error();
        if (0 < $errorCode) {
            throw new InvalidResourceException(sprintf('Error parsing JSON - %s', esc_html($this->getJSONErrorMessage($errorCode))));
        }
        return $this->parseData($data, $resource);
    }
    /**
     * Translates JSON_ERROR_* constant into meaningful message.
     *
     * @param int $errorCode
     *
     * @return string Message string
     */
    private function getJSONErrorMessage(int $errorCode): string
    {
        switch ($errorCode) {
            case \JSON_ERROR_DEPTH:
                return 'Maximum stack depth exceeded';
            case \JSON_ERROR_STATE_MISMATCH:
                return 'Underflow or the modes mismatch';
            case \JSON_ERROR_CTRL_CHAR:
                return 'Unexpected control character found';
            case \JSON_ERROR_SYNTAX:
                return 'Syntax error, malformed JSON';
            case \JSON_ERROR_UTF8:
                return 'Malformed UTF-8 characters, possibly incorrectly encoded';
            default:
                return 'Unknown error';
        }
    }
    /**
     * @param string $handle
     * @param string $fileUrl
     * @param string $filePath
     *
     * @return Asset|null
     */
    protected function buildAsset(string $handle, string $fileUrl, string $filePath): ?Asset
    {
        /** @var array{filename?:string, extension?:string} $pathInfo */
        $pathInfo = pathinfo($filePath);
        $filename = $pathInfo['filename'] ?? '';
        $class = $this->resolveClassByExtension($filePath);
        if (!$class) {
            return null;
        }
        /** @var Style|Script|ScriptModule $asset */
        $asset = new $class($handle, $fileUrl, $this->resolveLocation($filename));
        $asset->withFilePath($filePath);
        $asset->canEnqueue(\true);
        if ($asset instanceof BaseAsset) {
            $this->autodiscoverVersion ? $asset->enableAutodiscoverVersion() : $asset->disableAutodiscoverVersion();
        }
        return $asset;
    }
    protected function resolveClassByExtension(string $filePath): ?string
    {
        $extensionsToClass = ['css' => Style::class, 'js' => Script::class, 'mjs' => ScriptModule::class, 'module.js' => ScriptModule::class];
        // TODO Maybe make use of \SplFileInfo since it's typed and we can share it
        //      we have to just make a factory method and that's it.
        /** @var array{filename?:string, extension?:string} $pathInfo */
        $pathInfo = pathinfo($filePath);
        $baseName = $pathInfo['basename'] ?? '';
        $extension = $pathInfo['extension'] ?? '';
        if (self::isModule($baseName)) {
            $extension = 'module.js';
        }
        if (!in_array($extension, array_keys($extensionsToClass), \true)) {
            return null;
        }
        return $extensionsToClass[$extension];
    }
    protected static function isModule(string $fileName): bool
    {
        // TODO replace it with `str_ends_with` once dropping support for php 7.4
        $strEndsWith = static function (string $haystack, string $needle): bool {
            return substr_compare($haystack, $needle, -strlen($needle)) === 0;
        };
        return $strEndsWith($fileName, '.module.js') || $strEndsWith($fileName, '.mjs');
    }
    /**
     * The "file"-value can contain:
     *  - URL
     *  - Path to current folder
     *  - Absolute path
     *
     * We try to build a clean path which will be appended to the directoryPath or urlPath.
     *
     * @param string $file
     *
     * @return string
     */
    protected function sanitizeFileName(string $file): string
    {
        // Check if the given "file"-value is a URL
        $parsedUrl = parse_url($file);
        // the "file"-value can contain "./file.css" or "/file.css".
        return ltrim($parsedUrl['path'] ?? $file, './');
    }
    /**
     * Internal function to sanitize the handle based on the file
     * by taking into consideration that @vendor can be present.
     *
     * @param string $file
     *
     * @return string
     * @example /path/to/@vendor/script.module.js   -> @vendor/script.module
     *
     * @example /path/to/script.js                  -> script
     * @example @vendor/script.module.js            -> @vendor/script.module
     */
    protected function normalizeHandle(string $file): string
    {
        $pathInfo = pathinfo($file);
        $dirName = $pathInfo['dirname'] ?? '';
        $parts = explode('@', $dirName);
        $vendor = $parts[1] ?? null;
        $handle = $pathInfo['filename'];
        if ($vendor !== null) {
            $handle = "@{$vendor}/{$handle}";
        }
        return $handle;
    }
    /**
     * Internal function to resolve a location for a given file name.
     *
     * @param string $fileName
     *
     * @return int
     *
     * @example foo-customizer.css  -> Asset::CUSTOMIZER
     * @example foo-block.css       -> Asset::BLOCK_EDITOR_ASSETS
     * @example foo-login.css       -> Asset::LOGIN
     * @example foo.css             -> Asset::FRONTEND
     * @example foo-backend.css     -> Asset::BACKEND
     */
    protected function resolveLocation(string $fileName): int
    {
        if (stristr($fileName, '-backend')) {
            return Asset::BACKEND;
        }
        if (stristr($fileName, '-block')) {
            return Asset::BLOCK_EDITOR_ASSETS;
        }
        if (stristr($fileName, '-login')) {
            return Asset::LOGIN;
        }
        if (stristr($fileName, '-customizer-preview')) {
            return Asset::CUSTOMIZER_PREVIEW;
        }
        if (stristr($fileName, '-customizer')) {
            return Asset::CUSTOMIZER;
        }
        return Asset::FRONTEND;
    }
}
