<?php

/*
 * This file is part of the symfony package.
 * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
 * 
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * sfPluginManager allows you to manage plugins installation and uninstallation.
 *
 * @package    symfony
 * @subpackage plugin
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
 * @version    SVN: $Id$
 */
class sfPluginManager
{
  protected
   $dispatcher  = null,
   $environment = null,
   $installing  = array();

  /**
   * Constructs a new sfPluginManager.
   *
   * @param sfEventDispatcher $dispatcher   An event dispatcher instance
   * @param sfPearEnvironment $environment  A sfPearEnvironment instance
   */
  public function __construct(sfEventDispatcher $dispatcher, sfPearEnvironment $environment)
  {
    $this->initialize($dispatcher, $environment);
  }

  /**
   * Initializes this sfPluginManager instance.
   *
   * see sfPearEnvironment for available options.
   *
   * @param sfEventDispatcher $dispatcher   An event dispatcher instance
   * @param sfPearEnvironment $environment  A sfPearEnvironment instance
   */
  public function initialize(sfEventDispatcher $dispatcher, sfPearEnvironment $environment)
  {
    $this->dispatcher  = $dispatcher;
    $this->environment = $environment;

    // configure this plugin manager
    $this->configure();
  }

  /**
   * Configures this plugin manager.
   */
  public function configure()
  {
  }

  /**
   * Returns the sfPearEnvironment instance.
   *
   * @return sfPearEnvironment The sfPearEnvironment instance
   */
  public function getEnvironment()
  {
    return $this->environment;
  }

  /**
   * Returns a list of installed plugin.
   *
   * @return array An array of installed plugins
   */
  public function getInstalledPlugins()
  {
    $installed = array();
    foreach ($this->environment->getRegistry()->packageInfo(null, null, null) as $channel => $packages)
    {
      foreach ($packages as $package)
      {
        $installed[] = $this->environment->getRegistry()->getPackage(isset($package['package']) ? $package['package'] : $package['name'], $channel);
      }
    }

    return $installed;
  }

  /**
   * Installs a plugin.
   *
   * If you don't pass a version, it will install the latest version available
   * for the current project symfony version.
   *
   * Available options:
   *
   *  * channel:      The plugin channel name
   *  * version:      The version to install
   *  * stability:    The stability preference
   *  * install_deps: Whether to automatically install dependencies (default to false)
   *
   * @param string $plugin  The plugin name
   * @param array  $options An array of options
   *
   * @return Boolean|string true if the plugin is already installed, the name of the installed plugin otherwise
   */
  public function installPlugin($plugin, $options = array())
  {
    $this->installing = array();

    return $this->doInstallPlugin($plugin, $options);
  }

  /**
   * Installs a plugin
   *
   * @see installPlugin()
   */
  protected function doInstallPlugin($plugin, $options = array())
  {
    $channel   = isset($options['channel']) ? $options['channel'] : $this->environment->getConfig()->get('default_channel');
    $stability = isset($options['stability']) ? $options['stability'] : $this->environment->getConfig()->get('preferred_state', null, $channel);
    $version   = isset($options['version']) ? $options['version'] : null;

    $isPackage = true;
    if (0 === strpos($plugin, 'http://') || file_exists($plugin))
    {
      if (0 === strpos($plugin, 'http://plugins.symfony-project.'))
      {
        throw new sfPluginException("You try to install a symfony 1.0 plugin.\nPlease read the help message of this task to know how to install a plugin for the current version of symfony.");
      }

      $download  = $plugin;
      $isPackage = false;
    }
    else if (false !== strpos($plugin, '/'))
    {
      list($channel, $plugin) = explode('/', $plugin);
    }

    $this->dispatcher->notify(new sfEvent($this, 'plugin.pre_install', array('channel' => $channel, 'plugin' => $plugin, 'is_package' => $isPackage)));

    if ($isPackage)
    {
      $this->environment->getRest()->setChannel($channel);

      if (!preg_match(PEAR_COMMON_PACKAGE_NAME_PREG, $plugin))
      {
        throw new sfPluginException(sprintf('Plugin name "%s" is not a valid package name', $plugin));
      }

      if (!$version)
      {
        $version = $this->getPluginVersion($plugin, $stability);
      }
      else
      {
        if (!$this->isPluginCompatible($plugin, $version))
        {
          throw new sfPluginDependencyException(sprintf('Plugin "%s" in version "%s" is not compatible with the current application', $plugin, $version));
        }
      }

      if (!preg_match(PEAR_COMMON_PACKAGE_VERSION_PREG, $version))
      {
        throw new sfPluginException(sprintf('Plugin version "%s" is not a valid version', $version));
      }

      $existing = $this->environment->getRegistry()->packageInfo($plugin, 'version', $channel);
      if (version_compare($existing, $version) === 0)
      {
        $this->dispatcher->notify(new sfEvent($this, 'application.log', array('Plugin is already installed')));

        return true;
      }

      // skip if the plugin is already installing and we are here through a dependency)
      if (isset($this->installing[$channel.'/'.$plugin]))
      {
        return true;
      }

      // convert the plugin package into a discrete download URL
      $download = $this->environment->getRest()->getPluginDownloadURL($plugin, $version, $stability);
      if (PEAR::isError($download))
      {
        throw new sfPluginException(sprintf('Problem downloading the plugin "%s": %s', $plugin, $download->getMessage()));
      }
    }

    // download the plugin and install
    $class = $this->environment->getOption('downloader_base_class');
    $downloader = new $class($this, array('upgrade' => true), $this->environment->getConfig());

    $this->installing[$channel.'/'.$plugin] = true;

    if ($isPackage)
    {
      $this->checkPluginDependencies($plugin, $version, array(
        'install_deps' => isset($options['install_deps']) ? (bool) $options['install_deps'] : false,
        'stability'    => $stability,
      ));
    }

    // download the actual URL to the plugin
    $downloaded = $downloader->download(array($download));
    if (PEAR::isError($downloaded))
    {
      throw new sfPluginException(sprintf('Problem when downloading "%s": %s', $download, $downloaded->getMessage()));
    }
    $errors = $downloader->getErrorMsgs();
    if (count($errors))
    {
      $err = array();
      foreach ($errors as $error)
      {
        $err[] = $error;
      }

      if (!count($downloaded))
      {
        throw new sfPluginException(sprintf('Plugin "%s" installation failed: %s', $plugin, implode("\n", $err)));
      }
    }

    $pluginPackage = $downloaded[0];

    $installer = new PEAR_Installer($this);
    $installer->setOptions(array('upgrade' => true));
    $packages = array($pluginPackage);
    $installer->sortPackagesForInstall($packages);
    PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
    $err = $installer->setDownloadedPackages($packages);
    if (PEAR::isError($err))
    {
      PEAR::staticPopErrorHandling();
      throw new sfPluginException($err->getMessage());
    }

    $info = $installer->install($pluginPackage, array('upgrade' => true));
    PEAR::staticPopErrorHandling();
    if (PEAR::isError($info))
    {
      throw new sfPluginException(sprintf('Installation of "%s" plugin failed: %s', $plugin, $info->getMessage()));
    }

    if (is_array($info))
    {
      $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Installation successful for plugin "%s"', $plugin))));

      $this->dispatcher->notify(new sfEvent($this, 'plugin.post_install', array('channel' => $channel, 'plugin' => $pluginPackage->getPackage())));

      unset($this->installing[$channel.'/'.$plugin]);

      return $pluginPackage->getPackage();
    }
    else
    {
      throw new sfPluginException(sprintf('Installation of "%s" plugin failed', $plugin));
    }
  }

  /**
   * Uninstalls a plugin.
   *
   * @param string $plugin  The plugin name
   * @param string $channel The channel name
   */
  public function uninstallPlugin($plugin, $channel = null)
  {
    if (false !== strpos($plugin, '/'))
    {
      list($channel, $plugin) = explode('/', $plugin);
    }

    $channel = null === $channel ? $this->environment->getConfig()->get('default_channel') : $channel;

    $existing = $this->environment->getRegistry()->packageInfo($plugin, 'version', $channel);
    if (null === $existing)
    {
      $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Plugin "%s" is not installed', $plugin))));

      return false;
    }

    $this->dispatcher->notify(new sfEvent($this, 'plugin.pre_uninstall', array('channel' => $channel, 'plugin' => $plugin)));

    $package = $this->environment->getRegistry()->parsePackageName($plugin, $channel);

    $installer = new PEAR_Installer($this);
    $packages = array($this->environment->getRegistry()->getPackage($plugin, $channel));
    $installer->setUninstallPackages($packages);
    $ret = $installer->uninstall($package);
    if (PEAR::isError($ret))
    {
      throw new sfPluginException(sprintf('Problem uninstalling plugin "%s": %s', $plugin, $ret->getMessage()));
    }

    if ($ret)
    {
      $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Uninstallation successful for plugin "%s"', $plugin))));

      $this->dispatcher->notify(new sfEvent($this, 'plugin.post_uninstall', array('channel' => $channel, 'plugin' => $plugin)));
    }
    else
    {
      throw new sfPluginException(sprintf('Uninstallation of "%s" plugin failed', $plugin));
    }

    return $ret;
  }

  /**
   * Checks all plugin dependencies.
   *
   * Available options:
   *
   *  * stability:    The stability preference
   *  * install_deps: Whether to automatically install dependencies (default to false)
   *
   * @param string $plugin  The plugin name
   * @param string $version The plugin version
   * @param array  $options An array of options
   */
  public function checkPluginDependencies($plugin, $version, $options = false)
  {
    $dependencies = $this->environment->getRest()->getPluginDependencies($plugin, $version);

    if (!isset($dependencies['required']) || !isset($dependencies['required']['package']))
    {
      return;
    }

    $deps = $dependencies['required']['package'];
    if (!isset($deps[0]))
    {
      $deps = array($deps);
    }

    foreach ($deps as $dependency)
    {
      if (!$this->checkDependency($dependency))
      {
        $version = (isset($dependency['min']) ? ' >= '.$dependency['min'] : '').(isset($dependency['max']) ? ' <= '.$dependency['max'] : '').(isset($dependency['exclude']) ? ' exclude '.$dependency['exclude'] : '');

        if (isset($options['install_deps']) && $options['install_deps'])
        {
          try
          {
            $this->doInstallPlugin($dependency['name'], array_merge($options, array('channel' => $dependency['channel'])));
          }
          catch (sfException $e)
          {
            throw new sfPluginRecursiveDependencyException(sprintf('Unable to install plugin "%s" (version %s) because it depends on plugin "%s" which cannot be installed automatically: %s', $plugin, $version, $dependency['name'], $e->getMessage()));
          }

          continue;
        }

        throw new sfPluginDependencyException(sprintf('Unable to install plugin "%s" (version %s) because it depends on plugin "%s" which is not installed (install dependencies by hand or use the --install_deps option for automatic installation).', $plugin, $version, $dependency['name']));
      }
    }
  }

  /**
   * Gets the "best" version available for a given plugin.
   *
   * @param  string $plugin     The plugin name
   * @param  string $stability  The stability name
   *
   * @return string The version
   */
  public function getPluginVersion($plugin, $stability = null)
  {
    $versions = $this->environment->getRest()->getPluginVersions($plugin, $stability);
    foreach ($versions as $version)
    {
      if (!$this->isPluginCompatible($plugin, $version))
      {
        continue;
      }

      return $version;
    }

    throw new sfPluginDependencyException(sprintf('No release available for plugin "%s" in state "%s" that satisfies the application requirements.', $plugin, $stability));
  }

  /**
   * Returns true if the plugin is comptatible with your environment.
   *
   * @param  string $plugin   The plugin name
   * @param  string $version  The plugin version
   *
   * @return Boolean true if the plugin is compatible, false otherwise
   */
  public function isPluginCompatible($plugin, $version)
  {
    $dependencies = $this->environment->getRest()->getPluginDependencies($plugin, $version);

    if (!isset($dependencies['required']) || !isset($dependencies['required']['package']))
    {
      return true;
    }

    $deps = $dependencies['required']['package'];
    if (!isset($deps[0]))
    {
      $deps = array($deps);
    }

    foreach ($deps as $dependency)
    {
      if (!$this->isPluginCompatibleWithDependency($dependency))
      {
        return false;
      }
    }

    return true;
  }

  /**
   * Returns the license for a given plugin.
   *
   * @param string $plugin    The plugin name
   * @param array  $options   An array of options
   *
   * @return string The license
   *
   * @see installPlugin() for available options
   */
  public function getPluginLicense($plugin, $options = array())
  {
    $channel   = isset($options['channel']) ? $options['channel'] : $this->environment->getConfig()->get('default_channel');
    $stability = isset($options['stability']) ? $options['stability'] : $this->environment->getConfig()->get('preferred_state', null, $channel);
    $version   = isset($options['version']) ? $options['version'] : null;

    $rest = $this->environment->getRest();
    $rest->setChannel(null === $channel ? $this->environment->getConfig()->get('default_channel') : $channel);

    if (null === $version)
    {
      try
      {
        $version = $this->getPluginVersion($plugin, $stability);
      }
      catch (Exception $e)
      {
        // no release available
        return false;
      }
    }
    else
    {
      if (!$this->isPluginCompatible($plugin, $version))
      {
        throw new sfPluginDependencyException(sprintf('Plugin "%s" in version "%s" is not compatible with the current application', $plugin, $version));
      }
    }

    return $rest->getPluginLicense($plugin, $version);
  }

  /**
   * Returns true if the plugin is comptatible with the dependency.
   *
   * @param  array   $dependency An dependency array
   *
   * @return Boolean true if the plugin is compatible, false otherwise
   */
  protected function isPluginCompatibleWithDependency($dependency)
  {
    return true;
  }

  /**
   * Checks that the dependency is valid.
   *
   * @param  array   $dependency A dependency array
   *
   * @return Boolean true if the dependency is valid, false otherwise
   */
  protected function checkDependency($dependency)
  {
    $dependencyChecker = new PEAR_Dependency2($this->environment->getConfig(), array(), array('package' => '', 'channel' => ''));

    PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
    $e = $dependencyChecker->validatePackageDependency($dependency, true, array());
    PEAR::staticPopErrorHandling();
    if (PEAR::isError($e))
    {
      return false;
    }

    return true;
  }
}
