<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Shell\Task;

use Cake\Console\Shell;
use Cake\Core\Plugin;
use Cake\Filesystem\Folder;
use Cake\Utility\Inflector;

/**
 * Task for symlinking / copying plugin assets to app's webroot.
 */
class AssetsTask extends Shell
{
    /**
     * Attempt to symlink plugin assets to app's webroot. If symlinking fails it
     * fallbacks to copying the assets. For vendor namespaced plugin, parent folder
     * for vendor name are created if required.
     *
     * @param string|null $name Name of plugin for which to symlink assets.
     *   If null all plugins will be processed.
     * @return void
     */
    public function symlink($name = null)
    {
        $this->_process($this->_list($name));
    }

    /**
     * Copying plugin assets to app's webroot. For vendor namespaced plugin,
     * parent folder for vendor name are created if required.
     *
     * @param string|null $name Name of plugin for which to symlink assets.
     *   If null all plugins will be processed.
     * @return void
     */
    public function copy($name = null)
    {
        $this->_process($this->_list($name), true, $this->param('overwrite'));
    }

    /**
     * Remove plugin assets from app's webroot.
     *
     * @param string|null $name Name of plugin for which to remove assets.
     *   If null all plugins will be processed.
     * @return void
     * @since 3.5.12
     */
    public function remove($name = null)
    {
        $plugins = $this->_list($name);

        foreach ($plugins as $plugin => $config) {
            $this->out();
            $this->out('For plugin: ' . $plugin);
            $this->hr();

            $this->_remove($config);
        }

        $this->out();
        $this->out('Done');
    }

    /**
     * Get list of plugins to process. Plugins without a webroot directory are skipped.
     *
     * @param string|null $name Name of plugin for which to symlink assets.
     *   If null all plugins will be processed.
     * @return array List of plugins with meta data.
     */
    protected function _list($name = null)
    {
        if ($name === null) {
            $pluginsList = Plugin::loaded();
        } else {
            if (!Plugin::isLoaded($name)) {
                $this->err(sprintf('Plugin %s is not loaded.', $name));

                return [];
            }
            $pluginsList = [$name];
        }

        $plugins = [];

        foreach ($pluginsList as $plugin) {
            $path = Plugin::path($plugin) . 'webroot';
            if (!is_dir($path)) {
                $this->verbose('', 1);
                $this->verbose(
                    sprintf('Skipping plugin %s. It does not have webroot folder.', $plugin),
                    2
                );
                continue;
            }

            $link = Inflector::underscore($plugin);
            $dir = WWW_ROOT;
            $namespaced = false;
            if (strpos($link, '/') !== false) {
                $namespaced = true;
                $parts = explode('/', $link);
                $link = array_pop($parts);
                $dir = WWW_ROOT . implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
            }

            $plugins[$plugin] = [
                'srcPath' => Plugin::path($plugin) . 'webroot',
                'destDir' => $dir,
                'link' => $link,
                'namespaced' => $namespaced,
            ];
        }

        return $plugins;
    }

    /**
     * Process plugins
     *
     * @param array $plugins List of plugins to process
     * @param bool $copy Force copy mode. Default false.
     * @param bool $overwrite Overwrite existing files.
     * @return void
     */
    protected function _process($plugins, $copy = false, $overwrite = false)
    {
        $overwrite = (bool)$this->param('overwrite');

        foreach ($plugins as $plugin => $config) {
            $this->out();
            $this->out('For plugin: ' . $plugin);
            $this->hr();

            if (
                $config['namespaced'] &&
                !is_dir($config['destDir']) &&
                !$this->_createDirectory($config['destDir'])
            ) {
                continue;
            }

            $dest = $config['destDir'] . $config['link'];

            if (file_exists($dest)) {
                if ($overwrite && !$this->_remove($config)) {
                    continue;
                } elseif (!$overwrite) {
                    $this->verbose(
                        $dest . ' already exists',
                        1
                    );

                    continue;
                }
            }

            if (!$copy) {
                $result = $this->_createSymlink(
                    $config['srcPath'],
                    $dest
                );
                if ($result) {
                    continue;
                }
            }

            $this->_copyDirectory(
                $config['srcPath'],
                $dest
            );
        }

        $this->out();
        $this->out('Done');
    }

    /**
     * Remove folder/symlink.
     *
     * @param array $config Plugin config.
     * @return bool
     */
    protected function _remove($config)
    {
        if ($config['namespaced'] && !is_dir($config['destDir'])) {
            $this->verbose(
                $config['destDir'] . $config['link'] . ' does not exist',
                1
            );

            return false;
        }

        $dest = $config['destDir'] . $config['link'];

        if (!file_exists($dest)) {
            $this->verbose(
                $dest . ' does not exist',
                1
            );

            return false;
        }

        if (is_link($dest)) {
            // @codingStandardsIgnoreLine
            if (@unlink($dest)) {
                $this->out('Unlinked ' . $dest);

                return true;
            } else {
                $this->err('Failed to unlink  ' . $dest);

                return false;
            }
        }

        $folder = new Folder($dest);
        if ($folder->delete()) {
            $this->out('Deleted ' . $dest);

            return true;
        } else {
            $this->err('Failed to delete ' . $dest);

            return false;
        }
    }

    /**
     * Create directory
     *
     * @param string $dir Directory name
     * @return bool
     */
    protected function _createDirectory($dir)
    {
        $old = umask(0);
        // @codingStandardsIgnoreStart
        $result = @mkdir($dir, 0755, true);
        // @codingStandardsIgnoreEnd
        umask($old);

        if ($result) {
            $this->out('Created directory ' . $dir);

            return true;
        }

        $this->err('Failed creating directory ' . $dir);

        return false;
    }

    /**
     * Create symlink
     *
     * @param string $target Target directory
     * @param string $link Link name
     * @return bool
     */
    protected function _createSymlink($target, $link)
    {
        // @codingStandardsIgnoreStart
        $result = @symlink($target, $link);
        // @codingStandardsIgnoreEnd

        if ($result) {
            $this->out('Created symlink ' . $link);

            return true;
        }

        return false;
    }

    /**
     * Copy directory
     *
     * @param string $source Source directory
     * @param string $destination Destination directory
     * @return bool
     */
    protected function _copyDirectory($source, $destination)
    {
        $folder = new Folder($source);
        if ($folder->copy(['to' => $destination])) {
            $this->out('Copied assets to directory ' . $destination);

            return true;
        }

        $this->err('Error copying assets to directory ' . $destination);

        return false;
    }

    /**
     * Gets the option parser instance and configures it.
     *
     * @return \Cake\Console\ConsoleOptionParser
     */
    public function getOptionParser()
    {
        $parser = parent::getOptionParser();

        $parser->addSubcommand('symlink', [
            'help' => 'Symlink (copy as fallback) plugin assets to app\'s webroot.',
        ])->addSubcommand('copy', [
            'help' => 'Copy plugin assets to app\'s webroot.',
        ])->addSubcommand('remove', [
            'help' => 'Remove plugin assets from app\'s webroot.',
        ])->addArgument('name', [
            'help' => 'A specific plugin you want to symlink assets for.',
            'optional' => true,
        ])->addOption('overwrite', [
            'help' => 'Overwrite existing symlink / folder / files.',
            'default' => false,
            'boolean' => true,
        ]);

        return $parser;
    }
}
