<?php

namespace Croogo\Extensions;

use Cake\Core\App;
use Cake\Filesystem\Folder;
use Croogo\Core\Exception\Exception;
use ZipArchive;

/**
 * Extensions Installer
 *
 * @category Extensions.Model
 * @package  Croogo.Extensions.Lib
 * @version  1.0
 * @author   Fahad Ibnay Heylaal <contact@fahad19.com>
 * @license  http://www.opensource.org/licenses/mit-license.php The MIT License
 * @link     http://www.croogo.org
 */
class ExtensionsInstaller
{

    /**
     * Cache last retrieved plugin names for paths
     *
     * @var array
     */
    protected $_pluginName = [];

    /**
     * Cache last retrieved theme names for paths
     *
     * @var array
     */
    protected $_themeName = [];

    /**
     * Holds the found root paths of checked zip file
     *
     * @var array
     */
    protected $_rootPath = [];

    /**
     * Hold instance of CroogoComposer
     *
     * @var CroogoComposer
     */
    protected $_CroogoComposer = null;

    /**
     * __construct
     */
    public function __construct()
    {
        $this->_CroogoComposer = new CroogoComposer();
    }

    /**
     * Get Plugin Name from zip file
     *
     * @param string $path Path to zip file of plugin
     * @return string Plugin name
     * @throws Exception
     */
    public function getPluginName($path = null)
    {
        if (empty($path)) {
            throw new Exception(__d('croogo', 'Invalid plugin path'));
        }
        if (isset($this->_pluginName[$path])) {
            return $this->_pluginName[$path];
        }
        $Zip = new ZipArchive;
        if ($Zip->open($path) === true) {
            $search = 'config/plugin.json';
            $indexJson = $Zip->locateName('plugin.json', ZIPARCHIVE::FL_NODIR);
            if ($indexJson === false) {
                throw new Exception(__d('croogo', 'Invalid zip archive'));
            } else {
                $fileName = $Zip->getNameIndex($indexJson);
                $fileJson = json_decode($Zip->getFromIndex($indexJson));

                if (empty($fileJson->name)) {
                    throw new Exception(__d('croogo', 'Invalid plugin.json or missing plugin name'));
                }

                $this->_rootPath[$path] = str_replace($search, '', $fileName);
                $plugin = $fileJson->name;

                $searches = [
                    $plugin . 'Activation.php',
                    $plugin . 'AppController.php',
                    $plugin . 'AppModel.php',
                    $plugin . 'Helper.php'
                ];

                $hasFile = false;
                foreach ($searches as $search) {
                    if ($Zip->locateName($search, ZIPARCHIVE::FL_NODIR) !== false) {
                        $hasFile = true;
                        break;
                    }
                }
                if (!$hasFile) {
                    Log::critical(__d('croogo', 'Missing expected files: %s in: %s', implode(',', $searches), $path));
                    throw new Exception(__d('croogo', 'Invalid plugin or missing expected files'));
                }
            }
            $Zip->close();
            if (!$plugin) {
                throw new Exception(__d('croogo', 'Invalid plugin'));
            }
            $this->_pluginName[$path] = $plugin;

            return $plugin;
        } else {
            throw new Exception(__d('croogo', 'Invalid zip archive'));
        }

        return false;
    }

    /**
     * Extract a plugin from a zip file
     *
     * @param string $path Path to extension zip file
     * @param string $plugin Optional plugin name
     * @return bool
     * @throws Exception
     */
    public function extractPlugin($path = null, $plugin = null)
    {
        if (!file_exists($path)) {
            throw new Exception(__d('croogo', 'Invalid plugin file path'));
        }

        if (empty($plugin)) {
            $plugin = $this->getPluginName($path);
        }

        $pluginHome = App::path('Plugin');
        $pluginHome = reset($pluginHome);
        $pluginPath = $pluginHome . $plugin . DS;
        if (is_dir($pluginPath)) {
            throw new Exception(__d('croogo', 'Plugin already exists'));
        }

        $Zip = new ZipArchive;
        if ($Zip->open($path) === true) {
            new Folder($pluginPath, true);
            $Zip->extractTo($pluginPath);
            if (!empty($this->_rootPath[$path])) {
                $old = $pluginPath . $this->_rootPath[$path];
                $new = $pluginPath;
                $Folder = new Folder($old);
                $Folder->move($new);
            }
            $Zip->close();

            return true;
        } else {
            throw new Exception(__d('croogo', 'Failed to extract plugin'));
        }

        return false;
    }

    /**
     * Get name of theme
     *
     * @param string $path Path to zip file of theme
     * @throws Exception
     */
    public function getThemeName($path = null)
    {
        if (empty($path)) {
            throw new Exception(__d('croogo', 'Invalid theme path'));
        }

        if (isset($this->_themeName[$path])) {
            return $this->_themeName[$path];
        }

        $Zip = new ZipArchive;
        if ($Zip->open($path) === true) {
            $search = 'webroot/theme.json';
            $index = $Zip->locateName('theme.json', ZIPARCHIVE::FL_NODIR);
            if ($index !== false) {
                $file = $Zip->getNameIndex($index);
                $this->_rootPath[$path] = str_replace($search, '', $file);
                $json = json_decode($Zip->getFromIndex($index));
                if (!empty($json->name)) {
                    $theme = $json->name;
                }
            }
            $Zip->close();
            if (!$theme) {
                throw new Exception(__d('croogo', 'Invalid theme'));
            }
            $this->_themeName[$path] = $theme;

            return $theme;
        } else {
            throw new Exception(__d('croogo', 'Invalid zip archive'));
        }

        return false;
    }

    /**
     * Extract a theme from a zip file
     *
     * @param string $path Path to extension zip file
     * @param string $theme Optional theme name
     * @return bool
     * @throws Exception
     */
    public function extractTheme($path = null, $theme = null)
    {
        if (!file_exists($path)) {
            throw new Exception(__d('croogo', 'Invalid theme file path'));
        }

        if (empty($theme)) {
            $theme = $this->getThemeName($path);
        }

        $themeHome = App::path('Plugin');
        $themeHome = reset($themeHome);
        $themePath = $themeHome . $theme . DS;
        if (is_dir($themePath)) {
            throw new Exception(__d('croogo', 'Theme already exists'));
        }

        $Zip = new ZipArchive;
        if ($Zip->open($path) === true) {
            new Folder($themePath, true);
            $Zip->extractTo($themePath);
            if (!empty($this->_rootPath[$path])) {
                $old = $themePath . $this->_rootPath[$path];
                $new = $themePath;
                $Folder = new Folder($old);
                $Folder->move($new);
            }
            $Zip->close();

            return true;
        } else {
            throw new Exception(__d('croogo', 'Failed to extract theme'));
        }

        return false;
    }

    /**
     * Install packages with CroogoComposer
     *
     * @param array $data
     * @return bool
     * @throws Exception
     */
    public function composerInstall($data = [])
    {
        $data = array_merge([
            'package' => '',
            'version' => '*',
            'type' => 'plugin',
        ], $data);
        if (empty($data['package']) || strpos($data['package'], '/') === false) {
            throw new Exception(__d('croogo', 'Must specify a valid package name: vendor/name.'));
        }
        if ($data['type'] == 'theme') {
            throw new Exception(__d('croogo', 'Themes installed via composer are not yet supported.'));
        }
        $this->_CroogoComposer->getComposer();
        $this->_CroogoComposer->setConfig([
            $data['package'] => $data['version'],
        ]);

        return $this->_CroogoComposer->runComposer();
    }
}
