<?php

namespace Illuminate\Cache;

use Closure;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Cache\Factory as FactoryContract;

class CacheManager implements FactoryContract {

  /**
   * The application instance.
   *
   * @var \Illuminate\Foundation\Application
   */
  protected $app;

  /**
   * The array of resolved cache stores.
   *
   * @var array
   */
  protected $stores = [];

  /**
   * The registered custom driver creators.
   *
   * @var array
   */
  protected $customCreators = [];

  /**
   * Create a new Cache manager instance.
   *
   * @param  \Illuminate\Foundation\Application  $app
   * @return void
   */
  public function __construct($app) {
    $this->app = $app;
  }

  /**
   * Get a cache store instance by name.
   *
   * @param  string|null  $name
   * @return mixed
   */
  public function store($name = null) {
    $name = $name ? : $this->getDefaultDriver();

    return $this->stores[$name] = $this->get($name);
  }

  /**
   * Get a cache driver instance.
   *
   * @param  string  $driver
   * @return mixed
   */
  public function driver($driver = null) {
    return $this->store($driver);
  }

  /**
   * Attempt to get the store from the local cache.
   *
   * @param  string  $name
   * @return \Illuminate\Contracts\Cache\Repository
   */
  protected function get($name) {
    return isset($this->stores[$name]) ? $this->stores[$name] : $this->resolve($name);
  }

  /**
   * Resolve the given store.
   *
   * @param  string  $name
   * @return \Illuminate\Contracts\Cache\Repository
   *
   * @throws \InvalidArgumentException
   */
  protected function resolve($name) {
    $config = $this->getConfig($name);

    if (is_null($config)) {
      throw new InvalidArgumentException("Cache store [{$name}] is not defined.");
    }

    if (isset($this->customCreators[$config['driver']])) {
      return $this->callCustomCreator($config);
    } else {
      $driverMethod = 'create' . ucfirst($config['driver']) . 'Driver';

      if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($config);
      } else {
        throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
      }
    }
  }

  /**
   * Call a custom driver creator.
   *
   * @param  array  $config
   * @return mixed
   */
  protected function callCustomCreator(array $config) {
    return $this->customCreators[$config['driver']]($this->app, $config);
  }

  /**
   * Create an instance of the APC cache driver.
   *
   * @param  array  $config
   * @return \Illuminate\Cache\ApcStore
   */
  protected function createApcDriver(array $config) {
    $prefix = $this->getPrefix($config);

    return $this->repository(new ApcStore(new ApcWrapper, $prefix));
  }

  /**
   * Create an instance of the array cache driver.
   *
   * @return \Illuminate\Cache\ArrayStore
   */
  protected function createArrayDriver() {
    return $this->repository(new ArrayStore);
  }

  /**
   * Create an instance of the file cache driver.
   *
   * @param  array  $config
   * @return \Illuminate\Cache\FileStore
   */
  protected function createFileDriver(array $config) {
    return $this->repository(new FileStore($this->app['files'], $config['path']));
  }

  /**
   * Create an instance of the Memcached cache driver.
   *
   * @param  array  $config
   * @return \Illuminate\Cache\MemcachedStore
   */
  protected function createMemcachedDriver(array $config) {
    $prefix = $this->getPrefix($config);

    $memcached = $this->app['memcached.connector']->connect($config['servers']);

    return $this->repository(new MemcachedStore($memcached, $prefix));
  }

  /**
   * Create an instance of the Null cache driver.
   *
   * @return \Illuminate\Cache\NullStore
   */
  protected function createNullDriver() {
    return $this->repository(new NullStore);
  }

  /**
   * Create an instance of the Redis cache driver.
   *
   * @param  array  $config
   * @return \Illuminate\Cache\RedisStore
   */
  protected function createRedisDriver(array $config) {
    $redis = $this->app['redis'];

    $connection = Arr::get($config, 'connection', 'default');

    return $this->repository(new RedisStore($redis, $this->getPrefix($config), $connection));
  }

  /**
   * Create an instance of the database cache driver.
   *
   * @param  array  $config
   * @return \Illuminate\Cache\DatabaseStore
   */
  protected function createDatabaseDriver(array $config) {
    $connection = $this->app['db']->connection(Arr::get($config, 'connection'));

    return $this->repository(
                    new DatabaseStore(
                    $connection, $this->app['encrypter'], $config['table'], $this->getPrefix($config)
                    )
    );
  }

  /**
   * Create a new cache repository with the given implementation.
   *
   * @param  \Illuminate\Contracts\Cache\Store  $store
   * @return \Illuminate\Cache\Repository
   */
  public function repository(Store $store) {
    $repository = new Repository($store);

    if ($this->app->bound('Illuminate\Contracts\Events\Dispatcher')) {
      $repository->setEventDispatcher(
              $this->app['Illuminate\Contracts\Events\Dispatcher']
      );
    }

    return $repository;
  }

  /**
   * Get the cache prefix.
   *
   * @param  array  $config
   * @return string
   */
  protected function getPrefix(array $config) {
    return Arr::get($config, 'prefix') ? : $this->app['config']['cache.prefix'];
  }

  /**
   * Get the cache connection configuration.
   *
   * @param  string  $name
   * @return array
   */
  protected function getConfig($name) {
    return $this->app['config']["cache.stores.{$name}"];
  }

  /**
   * Get the default cache driver name.
   *
   * @return string
   */
  public function getDefaultDriver() {
    return $this->app['config']['cache.default'];
  }

  /**
   * Set the default cache driver name.
   *
   * @param  string  $name
   * @return void
   */
  public function setDefaultDriver($name) {
    $this->app['config']['cache.default'] = $name;
  }

  /**
   * Register a custom driver creator Closure.
   *
   * @param  string    $driver
   * @param  \Closure  $callback
   * @return $this
   */
  public function extend($driver, Closure $callback) {
    $this->customCreators[$driver] = $callback;

    return $this;
  }

  /**
   * Dynamically call the default driver instance.
   *
   * @param  string  $method
   * @param  array   $parameters
   * @return mixed
   */
  public function __call($method, $parameters) {
    return call_user_func_array([$this->store(), $method], $parameters);
  }

}
