<?php
namespace Consolidation\SiteAlias;

/**
 * Parse a string that contains a site specification.
 *
 * Site specifications contain some of the following elements:
 *   - user
 *   - host
 *   - path
 *   - uri (multisite selector)
 */
class SiteSpecParser
{
    /**
     * @var string Relative path from the site root to directory where
     * multisite configuration directories are found.
     */
    protected $multisiteDirectoryRoot = 'sites';

    /**
     * Parse a site specification
     *
     * @param string $spec
     *   A site specification in one of the accepted forms:
     *     - /path/to/drupal#uri
     *     - user@server/path/to/drupal#uri
     *     - user@server/path/to/drupal
     *     - user@server#uri
     *   or, a site name:
     *     - #uri
     * @param string $root
     *   Drupal root (if provided).
     * @return array
     *   A site specification array with the specified components filled in:
     *     - user
     *     - host
     *     - path
     *     - uri
     *   or, an empty array if the provided parameter is not a valid site spec.
     */
    public function parse($spec, $root = '')
    {
        $result = $this->match($spec);
        return $this->fixAndCheckUsability($result, $root);
    }

    /**
     * Determine if the provided specification is valid. Note that this
     * tests only for syntactic validity; to see if the specification is
     * usable, call 'parse()', which will also filter out specifications
     * for local sites that specify a multidev site that does not exist.
     *
     * @param string $spec
     *   @see parse()
     * @return bool
     */
    public function validSiteSpec($spec)
    {
        $result = $this->match($spec);
        return !empty($result);
    }

    /**
     * Determine whether or not the provided name is an alias name.
     *
     * @param string $aliasName
     * @return bool
     */
    public function isAliasName($aliasName)
    {
        return !empty($aliasName) && ($aliasName[0] == '@');
    }

    public function setMultisiteDirectoryRoot($location)
    {
        $this->multisiteDirectoryRoot = $location;
    }

    public function getMultisiteDirectoryRoot($root)
    {
        return $root . DIRECTORY_SEPARATOR . $this->multisiteDirectoryRoot;
    }

    /**
     * Return the set of regular expression patterns that match the available
     * site specification formats.
     *
     * @return array
     *   key: site specification regex
     *   value: an array mapping from site specification component names to
     *     the elements in the 'matches' array containing the data for that element.
     */
    protected function patterns()
    {
        $PATH = '([a-zA-Z]:[/\\\\][^#]*|[/\\\\][^#]*)';
        $USER = '([a-zA-Z0-9\._-]+)';
        $SERVER = '([a-zA-Z0-9\._-]+)';
        $URI = '([a-zA-Z0-9_-]+)';

        return [
            // /path/to/drupal#uri
            "%^{$PATH}#{$URI}\$%" => [
                'root' => 1,
                'uri' => 2,
            ],
            // user@server/path/to/drupal#uri
            "%^{$USER}@{$SERVER}{$PATH}#{$URI}\$%" => [
                'user' => 1,
                'host' => 2,
                'root' => 3,
                'uri' => 4,
            ],
            // user@server/path/to/drupal
            "%^{$USER}@{$SERVER}{$PATH}\$%" => [
                'user' => 1,
                'host' => 2,
                'root' => 3,
                'uri' => 'default', // Or '2' if uri should be 'host'
            ],
            // user@server#uri
            "%^{$USER}@{$SERVER}#{$URI}\$%" => [
                'user' => 1,
                'host' => 2,
                'uri' => 3,
            ],
            // #uri
            "%^#{$URI}\$%" => [
                'uri' => 1,
            ],
        ];
    }

    /**
     * Run through all of the available regex patterns and determine if
     * any match the provided specification.
     *
     * @return array
     *   @see parse()
     */
    protected function match($spec)
    {
        if ($spec === null) {
            $spec = '';
        }

        foreach ($this->patterns() as $regex => $map) {
            if (preg_match($regex, $spec, $matches)) {
                return $this->mapResult($map, $matches);
            }
        }
        return [];
    }

    /**
     * Inflate the provided array so that it always contains the required
     * elements.
     *
     * @return array
     *   @see parse()
     */
    protected function defaults($result = [])
    {
        $result += [
            'root' => '',
            'uri' => '',
        ];

        return $result;
    }

    /**
     * Take the data from the matches from the regular expression and
     * plug them into the result array per the info in the provided map.
     *
     * @param array $map
     *   An array mapping from result key to matches index.
     * @param array $matches
     *   The matched strings returned from preg_match
     * @return array
     *   @see parse()
     */
    protected function mapResult($map, $matches)
    {
        $result = [];

        foreach ($map as $key => $index) {
            $value = is_string($index) ? $index : $matches[$index];
            $result[$key] = $value;
        }

        if (empty($result)) {
            return [];
        }

        return $this->defaults($result);
    }

    /**
     * Validate the provided result. If the result is local, then it must
     * have a 'root'. If it does not, then fill in the root that was provided
     * to us in our consturctor.
     *
     * @param array $result
     *   @see parse() result.
     * @return array
     *   @see parse()
     */
    protected function fixAndCheckUsability($result, $root)
    {
        if (empty($result) || !empty($result['host'])) {
            return $result;
        }

        if (empty($result['root'])) {
            // TODO: should these throw an exception, so the user knows
            // why their site spec was invalid?
            if (empty($root) || !is_dir($root)) {
                return [];
            }

            $result['root'] = $root;
        }

        // If using a sitespec `#uri`, then `uri` MUST
        // be the name of a folder that exists in __DRUPAL_ROOT__/sites.
        // This restriction does NOT apply to the --uri option. Are there
        // instances where we need to allow 'uri' to be a literal uri
        // rather than the folder name? If so, we need to loosen this check.
        // I think it's fine as it is, though.
        $path = $this->getMultisiteDirectoryRoot($result['root']) . DIRECTORY_SEPARATOR . $result['uri'];
        if (!is_dir($path)) {
            return [];
        }

        return $result;
    }
}
