<?php
/**
 * core.framework
 *
 * @category  Core
 * @package   Core_Asset_Manager
 * @copyright Copyright (c) 2013. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */

/**
 * @category  Core
 * @package   Core_Asset_Manager
 * @copyright Copyright (c) 2013. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_Asset_Manager
{
    /**
     * Filters to use
     * @var array
     */
    protected $_filters             = array();

    /**
     * Content buffer (assets get concatenated into this variable)
     * @var string
     */
    protected $_content;

    /**
     * Asset types:
     */
    const ASSET_TYPE_CSS            = 'css';
    const ASSET_TYPE_SCSS           = 'scss';
    const ASSET_TYPE_SASS           = 'sass';
    const ASSET_TYPE_COFFEE         = 'coffee';
    const ASSET_TYPE_JAVASCRIPT     = 'js';

    /**
     * Define aliases (folders that are symlinked to another folder)
     * @var array
     */
    protected $_aliases;

    /**
     * Core Storage
     *
     * @var \Core_Storage
     */
    protected $_storage;

    /**
     * Container for Core_File
     *
     * @var \Core_File
     */
    protected $_file;

    /**
     * Process assets?
     * @var boolean
     */
    protected $_process;

    /**
     * Set processing on/off
     *
     * @param boolean $enableProcessing
     * @return \Core_Asset_Manager
     */
    public function setProcessingEnabled($enabledProcessing = true)
    {
        $this->_process = (bool) $enabledProcessing;
        return $this;
    }

    /**
     * Get processing flag
     *
     * @return boolean
     * @throws InvalidArgumentException
     */
    public function getProcessingEnabled()
    {
        if ($this->_process === null) {
            throw new InvalidArgumentException('Cannot get processing flag, flag not set!');
        }
        return (bool) $this->_process;
    }

    /**
     * Set alias (symlink)
     *
     * @param   string  $source
     * @param   string  $destination
     *
     * @return  \Core_Asset_Manager
     */
    public function setAlias($source, $destination)
    {
        if (!strlen($source)) {
            throw new InvalidArgumentException('Error adding alias: '. $source . ' -> '. $destination);
        }

        $this->_aliases[$source] = $destination;
        return $this;
    }

    /**
     * Set aliases (multiple) - helper
     * - requires source -> destination array
     *
     * @param   array   $aliases
     * @return  \Core_Asset_Manager
     */
    public function setAliases(array $aliases)
    {
        foreach ($aliases as $source => $destination) {
            $this->setAlias($source, $destination);
        }
        return $this;
    }


    /**
     * Get aliases
     *
     * @return array
     */
    public function getAliases()
    {
        return (array) $this->_aliases;
    }

    /**
     * Is asset an aliased one? (contains directory that is on the alias map)
     * - returns false if not, else return alias key
     *
     * @param   string  $asset
     * @return  string|boolean
     */
    protected function _isAlias($assetUrl)
    {
        if (!strlen($assetUrl)) {
            return false;
        }

        foreach ($this->getAliases() as $source => $destination) {
            if (preg_match("#^". $source ."#", $assetUrl)) {
                return $source;
            }
        }
        return false;
    }

    /**
     * Set storage controler
     *
     * @param Core_Storage $storage
     *
     * @return \Core_Asset_Manager
     */
    public function setStorage(Core_Storage_Strategy_Interface $storage)
    {
        $this->_storage = $storage;
        return $this;
    }

    /**
     * Get Storage controller
     *
     * @return \Core_Storage
     */
    public function getStorage()
    {
        if ($this->_storage === null) {
            throw new LogicException('Unable to get Storage, storage not set!');
        }
        return $this->_storage;
    }

    /**
     * Map aliased asset
     *
     * @param   string  $asset
     * @return  string
     * @throws  InvalidArgumentException
     */
    protected function _mapAliasedAsset($asset)
    {
        if (!strlen($asset)) {
            throw new InvalidArgumentException('Unable to map aliased asset, asset not set!');
        }

        $source = $this->_isAlias($asset);
        if ($source !== false) {
            $aliases            = $this->getAliases();
            $trimmedSource      = rtrim($source, '/');
            $trimmedDestination = rtrim($aliases[$source], '/');
            $regex              = '/^'. preg_quote($trimmedSource, '/') .'/';
            $path               = preg_replace($regex, $trimmedDestination, $asset);

            return $path;
        }

        return false;
    }

    /**
     * Add filter
     *
     * @param Core_Asset_Filter_Interface $filter
     *
     * @return \Core_Asset
     */
    public function addFilter(Core_Asset_Filter_Interface $filter)
    {
        $this->_content     = null;
        $this->_filters[]   = $filter;

        return $this;
    }

    /**
     * Get filters
     *
     * @return array
     */
    public function getFilters()
    {
        return $this->_filters;
    }

    /**
     * Read all assets and dump them into a single 'buffer' -
     * - makes sure to not do the same job more than 1 time
     *
     * @todo Run the files through filters if filters support them
     *
     * @throws LogicException
     *
     * @param Core_Asset[] $assets Array of Core_Asset objects
     * @return string
     */
    public function dump($assets)
    {
        $content = '';

        if (empty($assets)) {
            throw new LogicException('Unable to dump, no assets have been added');
        }

        foreach ($assets as $asset) {

            $assetData  = trim((string) $asset) . PHP_EOL;
            $content    .= $assetData;

            if ($this->_getType($asset) === self::ASSET_TYPE_JAVASCRIPT) {
                if (strlen($assetData) - strrpos($assetData, ';') != 2) {
                    $content .= ';';
                }
            }
        }

        return $content;
    }

    /**
     * Process assets and generate asset file
     *
     * @param array $assets
     * @param array $options
     *
     * @return array
     * @throws InvalidArgumentException
     */
    public function process($assets, $options = array())
    {
        if (!is_array($assets)) {
            throw new InvalidArgumentException('Unable to process assets, input data is not an array!');
        }

        if (isset($options['enabled'])) {
            $process = $options['enabled'] ? true : false;
        } else {
            $process = $this->getProcessingEnabled();
        }

        foreach ($assets as $assetUrl) {

            if (false !== ($aliasSource = $this->_isAlias($assetUrl))) {
                $fullAssetPath  = $this->_mapAliasedAsset($assetUrl);
            } else {
                $fullAssetPath  = $this->getStorage()->getUrlRoot() . $assetUrl;
            }
            $coreAsset      = new Core_Asset($fullAssetPath, null, null, null, $assetUrl);
            $coreAsset->setStorage($this->getStorage());
            $assetObjects[] = $coreAsset;
        }

        $assets = $assetObjects;

        // check that assets dont have mixed types, if mixed types are detected, throw exception
        $this->_checkAssets($assets);

        if (!$process) {
            return $assets;
        }

        // create temporary file that holds new content, then object to represent that file
        $tempFilename   = tempnam(sys_get_temp_dir(), 'cam_');
        $content        = $this->dump($assets);
        file_put_contents($tempFilename, $content);

        $file   = new Core_Storage_File($tempFilename);
        $file->setStorageName($this->_generateOutputFilename($assets));

        /* @var $storageFile Core_Storage_File */
        $storageFile = $this->getStorage()->store($file);
        $storageFile->setStorage($this->getStorage());

        // Core_Asset
        $pathWithCacheBust  = $storageFile->getPath() .'?v='. md5($this->dump($assets));
        $filename           = $storageFile->getStorageName();

        $asset = new Core_Asset($pathWithCacheBust, null, null, null, '/asset/'. $filename);
        $asset->setStorage($this->getStorage());
        $asset->setCacheBust(true);

        unlink($tempFilename);

        // return single asset inside an array
        $result[] = $asset;
        return $result;
    }

    /**
     * Generates output filename for generated asset
     *
     * @param array(Core_Asset) $asset
     * @return string
     */
    protected function _generateOutputFilename(array $assets)
    {
        foreach ($assets as $asset) {
            $filenames[]    = $asset->getName();
        }

        $assetFilenameHash  = md5(implode('-', $filenames));
        $outputFile         = $assetFilenameHash .'.'. $this->_checkAssets($assets);
        return $outputFile;
    }

    /**
     * Get filename type (extension)
     *
     * @param string $filename
     *
     * @return string
     * @throws InvalidArgumentException
     */
    protected function _getType(Core_Asset $asset)
    {
        $type = pathinfo($asset->getPath(), PATHINFO_EXTENSION);

        switch ($type) {
            case self::ASSET_TYPE_CSS:
            case self::ASSET_TYPE_JAVASCRIPT:
            case self::ASSET_TYPE_SCSS:
            case self::ASSET_TYPE_COFFEE:
            case self::ASSET_TYPE_SASS:
                return $type;
            break;

            default:
                throw new InvalidArgumentException('Asset '. $asset->getPath() . ' is of an uknown type: '. $type .'!');
            break;
        }
    }

    /**
     * Check Assets - makes sure we didn't add different asset types (a.k.a. js and css)
     * - throws IAException if all assets added are not of the same type (different extensions)
     * - if everything goes well, returns target extension of processed assets
     *
     * @param array $assets Array of Core_Asset objects
     * @throws InvalidArgumentException
     * @return string
     */
    protected function _checkAssets($assets)
    {
        foreach ($assets as $asset) {
            $extensions[]    = $asset->getExtension();
        }

        $numberOfExtensions = array_unique($extensions);

//        if (count($numberOfExtensions) !== 1) {
//            throw new InvalidArgumentException('Cannot add assets of different type: '. implode(', ', $numberOfExtensions) . '!');
//        }
        return $extensions[0];
    }

}
