<?php
namespace Robo\Task\Assets;

use Robo\Result;
use Robo\Task\BaseTask;

/**
 * Minifies asset file (CSS or JS).
 *
 * ``` php
 * <?php
 * $this->taskMinify( 'web/assets/theme.css' )
 *      ->run()
 * ?>
 * ```
 * Please install additional dependencies to use:
 *
 * ```
 * "patchwork/jsqueeze": "~1.0",
 * "natxet/CssMin": "~3.0"
 * ```
 */
class Minify extends BaseTask
{
    /**
     * @var array
     */
    protected $types = ['css', 'js'];

    /**
     * @var string
     */
    protected $text;

    /**
     * @var string
     */
    protected $dst;

    /**
     * @var string
     */
    protected $type;

    /**
     * @var array
     */
    protected $squeezeOptions = [
        'singleLine' => true,
        'keepImportantComments' => true,
        'specialVarRx' => false,
    ];

    /**
     * Constructor. Accepts asset file path or string source.
     *
     * @param string $input
     */
    public function __construct($input)
    {
        if (file_exists($input)) {
            $this->fromFile($input);
            return;
        }

        $this->fromText($input);
    }

    /**
     * Sets destination. Tries to guess type from it.
     *
     * @param string $dst
     *
     * @return $this
     */
    public function to($dst)
    {
        $this->dst = $dst;

        if (!empty($this->dst) && empty($this->type)) {
            $this->type($this->getExtension($this->dst));
        }

        return $this;
    }

    /**
     * Sets type with validation.
     *
     * @param string $type css|js
     *
     * @return $this
     */
    public function type($type)
    {
        $type = strtolower($type);

        if (in_array($type, $this->types)) {
            $this->type = $type;
        }

        return $this;
    }

    /**
     * Sets text from string source.
     *
     * @param string $text
     *
     * @return $this
     */
    protected function fromText($text)
    {
        $this->text = (string)$text;
        unset($this->type);

        return $this;
    }

    /**
     * Sets text from asset file path. Tries to guess type and set default destination.
     *
     * @param string $path
     *
     * @return $this
     */
    protected function fromFile($path)
    {
        $this->text = file_get_contents($path);

        unset($this->type);
        $this->type($this->getExtension($path));

        if (empty($this->dst) && !empty($this->type)) {
            $ext_length = strlen($this->type) + 1;
            $this->dst = substr($path, 0, -$ext_length) . '.min.' . $this->type;
        }

        return $this;
    }

    /**
     * Gets file extension from path.
     *
     * @param string $path
     *
     * @return string
     */
    protected function getExtension($path)
    {
        return pathinfo($path, PATHINFO_EXTENSION);
    }

    /**
     * Minifies and returns text.
     *
     * @return string|bool
     */
    protected function getMinifiedText()
    {
        switch ($this->type) {
            case 'css':
                if (!class_exists('\CssMin')) {
                    return Result::errorMissingPackage($this, 'CssMin', 'natxet/CssMin');
                }

                return \CssMin::minify($this->text);
                break;

            case 'js':
                if (!class_exists('\JSqueeze') && !class_exists('\Patchwork\JSqueeze')) {
                    return Result::errorMissingPackage($this, 'Patchwork\JSqueeze', 'patchwork/jsqueeze');
                }

                if (class_exists('\JSqueeze')) {
                    $jsqueeze = new \JSqueeze();
                } else {
                    $jsqueeze = new \Patchwork\JSqueeze();
                }

                return $jsqueeze->squeeze(
                    $this->text,
                    $this->squeezeOptions['singleLine'],
                    $this->squeezeOptions['keepImportantComments'],
                    $this->squeezeOptions['specialVarRx']
                );
                break;
        }

        return false;
    }

    /**
     * Single line option for the JS minimisation.
     *
     * @param bool $singleLine
     *
     * @return $this
     */
    public function singleLine($singleLine)
    {
        $this->squeezeOptions['singleLine'] = (bool)$singleLine;
        return $this;
    }

    /**
     * keepImportantComments option for the JS minimisation.
     *
     * @param bool $keepImportantComments
     *
     * @return $this
     */
    public function keepImportantComments($keepImportantComments)
    {
        $this->squeezeOptions['keepImportantComments'] = (bool)$keepImportantComments;
        return $this;
    }

    /**
     * specialVarRx option for the JS minimisation.
     *
     * @param bool $specialVarRx
     *
     * @return $this ;
     */
    public function specialVarRx($specialVarRx)
    {
        $this->squeezeOptions['specialVarRx'] = (bool)$specialVarRx;
        return $this;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return (string) $this->getMinifiedText();
    }

    /**
     * {@inheritdoc}
     */
    public function run()
    {
        if (empty($this->type)) {
            return Result::error($this, 'Unknown asset type.');
        }

        if (empty($this->dst)) {
            return Result::error($this, 'Unknown file destination.');
        }

        if (file_exists($this->dst) && !is_writable($this->dst)) {
            return Result::error($this, 'Destination already exists and cannot be overwritten.');
        }

        $size_before = strlen($this->text);
        $minified = $this->getMinifiedText();

        if ($minified instanceof Result) {
            return $minified;
        } elseif (false === $minified) {
            return Result::error($this, 'Minification failed.');
        }

        $size_after = strlen($minified);

        // Minification did not reduce file size, so use original file.
        if ($size_after > $size_before) {
            $minified = $this->text;
            $size_after = $size_before;
        }

        $dst = $this->dst . '.part';
        $write_result = file_put_contents($dst, $minified);

        if (false === $write_result) {
            @unlink($dst);
            return Result::error($this, 'File write failed.');
        }
        // Cannot be cross-volume; should always succeed.
        @rename($dst, $this->dst);
        if ($size_before === 0) {
            $minified_percent = 0;
        } else {
            $minified_percent = number_format(100 - ($size_after / $size_before * 100), 1);
        }
        $this->printTaskSuccess('Wrote {filepath}', ['filepath' => $this->dst]);
        $context = [
            'bytes' => $this->formatBytes($size_after),
            'reduction' => $this->formatBytes(($size_before - $size_after)),
            'percentage' => $minified_percent,
        ];
        $this->printTaskSuccess('Wrote {bytes} (reduced by {reduction} / {percentage})', $context);
        return Result::success($this, 'Asset minified.');
    }
}
