<?php

/*
 * This file is part of the `liip/LiipImagineBundle` project.
 *
 * (c) https://github.com/liip/LiipImagineBundle/graphs/contributors
 *
 * For the full copyright and license information, please view the LICENSE.md
 * file that was distributed with this source code.
 */

namespace Liip\ImagineBundle\Binary\Locator;

use Liip\ImagineBundle\Exception\Binary\Loader\NotLoadableException;
use Liip\ImagineBundle\Exception\InvalidArgumentException;

class FileSystemLocator implements LocatorInterface
{
    /**
     * @var string[]
     */
    private $roots = [];

    /**
     * @param string[] $roots
     */
    public function __construct(array $roots = [], bool $allowUnresolvable = false)
    {
        $this->roots = array_filter(array_map(function (string $root) use ($allowUnresolvable): ?string {
            return $this->sanitizeRootPath($root, $allowUnresolvable);
        }, $roots));
    }

    /**
     * @throws NotLoadableException
     */
    public function locate(string $path): string
    {
        if (null !== $absolute = $this->locateUsingRootPlaceholder($path)) {
            return $this->sanitizeAbsolutePath($absolute);
        }

        if (null !== $absolute = $this->locateUsingRootPathsSearch($path)) {
            return $this->sanitizeAbsolutePath($absolute);
        }

        throw new NotLoadableException(sprintf('Source image not resolvable "%s" in root path(s) "%s"', $path, implode(':', $this->roots)));
    }

    protected function generateAbsolutePath(string $root, string $path): ?string
    {
        if (false !== $absolute = realpath($root.DIRECTORY_SEPARATOR.$path)) {
            return $absolute;
        }

        return null;
    }

    private function locateUsingRootPathsSearch(string $path): ?string
    {
        foreach ($this->roots as $root) {
            if (null !== $absolute = $this->generateAbsolutePath($root, $path)) {
                return $absolute;
            }
        }

        return null;
    }

    private function locateUsingRootPlaceholder(string $path): ?string
    {
        if (0 !== mb_strpos($path, '@') || 1 !== preg_match('{^@(?<name>[^:]+):(?<path>.+)$}', $path, $match)) {
            return null;
        }

        if (isset($this->roots[$match['name']])) {
            return $this->generateAbsolutePath($this->roots[$match['name']], $match['path']);
        }

        throw new NotLoadableException(sprintf('Invalid root placeholder "@%s" for path "%s"', $match['name'], $match['path']));
    }

    /**
     * @throws InvalidArgumentException
     */
    private function sanitizeRootPath(string $path, bool $allowUnresolvable): ?string
    {
        if (!empty($path) && false !== $real = realpath($path)) {
            return $real;
        }

        if ($allowUnresolvable) {
            return null;
        }

        throw new InvalidArgumentException(sprintf('Root image path not resolvable "%s"', $path));
    }

    /**
     * @throws NotLoadableException
     */
    private function sanitizeAbsolutePath(string $path): string
    {
        $roots = array_filter($this->roots, function (string $root) use ($path): bool {
            return 0 === mb_strpos($path, $root);
        });

        if (0 === \count($roots)) {
            throw new NotLoadableException(sprintf('Source image invalid "%s" as it is outside of the defined root path(s) "%s"', $path, implode(':', $this->roots)));
        }

        if (!is_readable($path)) {
            throw new NotLoadableException(sprintf('Source image invalid "%s" as it is not readable', $path));
        }

        return $path;
    }
}
