<?php

/*
 * This file is part of the Fxp Composer Asset Plugin package.
 *
 * (c) François Pluchino <francois.pluchino@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Fxp\Composer\AssetPlugin\Repository;

use Composer\Config;
use Composer\DependencyResolver\Pool;
use Composer\Downloader\TransportException;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Repository\ComposerRepository;
use Composer\Repository\RepositoryManager;
use Fxp\Composer\AssetPlugin\Assets;
use Fxp\Composer\AssetPlugin\Type\AssetTypeInterface;

/**
 * Abstract assets repository.
 *
 * @author François Pluchino <francois.pluchino@gmail.com>
 */
abstract class AbstractAssetsRepository extends ComposerRepository
{
    /**
     * @var AssetTypeInterface
     */
    protected $assetType;

    /**
     * @var AssetVcsRepository[]
     */
    protected $repos;

    /**
     * @var bool
     */
    protected $searchable;

    /**
     * @var bool
     */
    protected $fallbackProviders;

    /**
     * @var RepositoryManager
     */
    protected $repositoryManager;

    /**
     * @var AssetRepositoryManager
     */
    protected $assetRepositoryManager;

    /**
     * @var VcsPackageFilter
     */
    protected $packageFilter;

    /**
     * Constructor.
     *
     * @param array           $repoConfig
     * @param IOInterface     $io
     * @param Config          $config
     * @param EventDispatcher $eventDispatcher
     */
    public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null)
    {
        $repoConfig = array_merge($repoConfig, array(
            'url' => $this->getUrl(),
        ));
        $this->assetRepositoryManager = $repoConfig['asset-repository-manager'];
        $this->repositoryManager = $this->assetRepositoryManager->getRepositoryManager();

        parent::__construct($repoConfig, $io, $config, $eventDispatcher);

        $this->assetType = Assets::createType($this->getType());
        $this->lazyProvidersUrl = $this->getPackageUrl();
        $this->providersUrl = $this->lazyProvidersUrl;
        $this->searchUrl = $this->getSearchUrl();
        $this->hasProviders = true;
        $this->packageFilter = isset($repoConfig['vcs-package-filter'])
            ? $repoConfig['vcs-package-filter']
            : null;
        $this->repos = array();
        $this->searchable = (bool) $this->getOption($repoConfig['asset-options'], 'searchable', true);
        $this->fallbackProviders = false;
    }

    /**
     * {@inheritdoc}
     */
    public function search($query, $mode = 0, $type = null)
    {
        if (!$this->searchable) {
            return array();
        }

        $url = str_replace('%query%', urlencode(Util::cleanPackageName($query)), $this->searchUrl);
        $hostname = (string) parse_url($url, PHP_URL_HOST) ?: $url;
        $json = (string) $this->rfs->getContents($hostname, $url, false);
        $data = JsonFile::parseJson($json, $url);
        $results = array();

        /** @var array $item */
        foreach ($data as $item) {
            $results[] = $this->createSearchItem($item);
        }

        return $results;
    }

    /**
     * {@inheritdoc}
     */
    public function whatProvides(Pool $pool, $name, $bypassFilters = false)
    {
        if (null !== $provides = $this->findWhatProvides($name)) {
            return $provides;
        }

        try {
            $repoName = Util::convertAliasName($name);
            $packageName = Util::cleanPackageName($repoName);
            $packageUrl = $this->buildPackageUrl($packageName);
            $cacheName = $packageName.'-'.sha1($packageName).'-package.json';
            $data = $this->fetchFile($packageUrl, $cacheName);
            $repo = $this->createVcsRepositoryConfig($data, Util::cleanPackageName($name));
            $repo['asset-repository-manager'] = $this->assetRepositoryManager;
            $repo['vcs-package-filter'] = $this->packageFilter;
            $repo['vcs-driver-options'] = Util::getArrayValue($this->repoConfig, 'vcs-driver-options', array());

            Util::addRepository($this->io, $this->repositoryManager, $this->repos, $name, $repo, $pool);

            $this->providers[$name] = array();
        } catch (\Exception $ex) {
            $this->whatProvidesManageException($pool, $name, $ex);
        }

        return $this->providers[$name];
    }

    /**
     * {@inheritdoc}
     */
    public function getMinimalPackages()
    {
        return array();
    }

    /**
     * Build the package url.
     *
     * @param string $packageName The package name
     *
     * @return string
     */
    protected function buildPackageUrl($packageName)
    {
        return str_replace('%package%', $packageName, $this->lazyProvidersUrl);
    }

    /**
     * Finds what provides in cache or return empty array if the
     * name is not a asset package.
     *
     * @param string $name
     *
     * @return null|array
     */
    protected function findWhatProvides($name)
    {
        $assetPrefix = $this->assetType->getComposerVendorName().'/';

        if (false === strpos($name, $assetPrefix)) {
            return array();
        }

        if (isset($this->providers[$name])) {
            return $this->providers[$name];
        }

        $data = null;
        if ($this->hasVcsRepository($name)) {
            $this->providers[$name] = array();
            $data = $this->providers[$name];
        }

        return $data;
    }

    /**
     * Checks if the package vcs repository is already include in repository manager.
     *
     * @param string $name The package name of the vcs repository
     *
     * @return bool
     */
    protected function hasVcsRepository($name)
    {
        foreach ($this->repositoryManager->getRepositories() as $mRepo) {
            if ($mRepo instanceof AssetVcsRepository
                    && $name === $mRepo->getComposerPackageName()) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    protected function loadRootServerFile()
    {
        return array(
            'providers' => array(),
        );
    }

    /**
     * Gets the option.
     *
     * @param array  $options The options
     * @param string $key     The key
     * @param mixed  $default The default value
     *
     * @return mixed The option value or default value if key is not found
     */
    protected function getOption(array $options, $key, $default = null)
    {
        if (\array_key_exists($key, $options)) {
            return $options[$key];
        }

        return $default;
    }

    /**
     * Creates the search result item.
     *
     * @param array $item The item
     *
     * @return array An array('name' => '...', 'description' => '...')
     */
    protected function createSearchItem(array $item)
    {
        return array(
            'name' => $this->assetType->getComposerVendorName().'/'.$item['name'],
            'description' => null,
        );
    }

    /**
     * Manage exception for "whatProvides" method.
     *
     * @param Pool       $pool
     * @param string     $name
     * @param \Exception $exception
     *
     * @throws \Exception When exception is not a TransportException instance
     */
    protected function whatProvidesManageException(Pool $pool, $name, \Exception $exception)
    {
        if ($exception instanceof TransportException) {
            $this->fallbackWathProvides($pool, $name, $exception);

            return;
        }

        throw $exception;
    }

    /**
     * Searchs if the registry has a package with the same name exists with a
     * different camelcase.
     *
     * @param Pool               $pool
     * @param string             $name
     * @param TransportException $ex
     */
    protected function fallbackWathProvides(Pool $pool, $name, TransportException $ex)
    {
        $providers = array();

        if (404 === $ex->getCode() && !$this->fallbackProviders) {
            $this->fallbackProviders = true;
            $repoName = Util::convertAliasName($name);
            $results = $this->search($repoName);

            foreach ($results as $item) {
                if ($name === strtolower($item['name'])) {
                    $providers = $this->whatProvides($pool, $item['name']);

                    break;
                }
            }
        }

        $this->fallbackProviders = false;
        $this->providers[$name] = $providers;
    }

    /**
     * Gets the asset type name.
     *
     * @return string
     */
    abstract protected function getType();

    /**
     * Gets the URL of repository.
     *
     * @return string
     */
    abstract protected function getUrl();

    /**
     * Gets the URL for get the package information.
     *
     * @return string
     */
    abstract protected function getPackageUrl();

    /**
     * Gets the URL for get the search result.
     *
     * @return string
     */
    abstract protected function getSearchUrl();

    /**
     * Creates a config of vcs repository.
     *
     * @param array  $data         The repository config
     * @param string $registryName The package name in asset registry
     *
     * @return array An array('type' => '...', 'url' => '...')
     */
    abstract protected function createVcsRepositoryConfig(array $data, $registryName = null);
}
