<?php namespace Lovata\Toolbox\Classes\Item;

use Model;
use October\Rain\Extension\ExtendableTrait;

use Kharanenka\Helper\CCache;

/**
 * Class ElementItem
 * @package Lovata\Toolbox\Classes\Item
 * @author  Andrey Kharanenka, a.khoronenko@lovata.com, LOVATA Group
 *
 * @link    https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem
 */
abstract class ElementItem extends MainItem
{
    use ExtendableTrait;

    const MODEL_CLASS = Model::class;
    const QUERY_FIELD = 'id';

    public static $arQueryWith = [];

    public $implement = [];

    /** @var int */
    protected $iElementID = null;

    /** @var \Model */
    protected $obElement = null;

    /** @var array */
    protected static $arBooted = [];

    /** @var array */
    public $arExtendResult = [];

    /**
     * ElementItem constructor.
     * @param int    $iElementID
     * @param \Model $obElement
     * @throws \Exception
     */
    public function __construct($iElementID, $obElement)
    {
        $this->iElementID = $iElementID;
        $this->obElement = $obElement;

        //Check instance of obElement
        $sModelClass = static::MODEL_CLASS;
        if (!empty($this->obElement) && !$this->obElement instanceof $sModelClass) {
            $this->obElement = null;
        }

        $this->bootIfNotBooted();

        $this->initActiveLang();
        $this->extendableConstruct();
    }

    /**
     * @param string $sName
     * @return string
     */
    public function __get($sName)
    {
        return $this->extendableGet($sName);
    }

    /**
     * @param string $sName
     * @param mixed  $obValue
     */
    public function __set($sName, $obValue)
    {
        $this->extendableSet($sName, $obValue);
    }

    /**
     * @param string $sName
     * @param array  $arParamList
     * @return mixed
     */
    public function __call($sName, $arParamList)
    {
        return $this->extendableCall($sName, $arParamList);
    }

    /**
     * @param string $sName
     * @param array  $arParamList
     * @return mixed
     * @throws \Exception
     */
    public static function __callStatic($sName, $arParamList)
    {
        return self::extendableCallStatic($sName, $arParamList);
    }


    /**
     * Serialize item object
     * @return array
     */
    public function __sleep()
    {
        return ['iElementID'];
    }

    /**
     * Unserialize object
     */
    public function __wakeup()
    {
        $this->setCachedData();
    }

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

    /**
     * @param callable $callback
     */
    public static function extend(callable $callback)
    {
        self::extendableExtendCallback($callback);
    }

    /**
     * Make new element item
     * @param int|string $iElementID
     * @param \Model     $obElement
     *
     * @return $this
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#makeielementid-obelement--null
     * @see  \Lovata\Toolbox\Tests\Unit\ItemTest::testItem()
     */
    public static function make($iElementID, $obElement = null)
    {
        $arParamList = [
            'iElementID' => $iElementID,
            'obElement'  => $obElement,
        ];

        $obItem = ItemStorage::get(static::class, $iElementID);
        if (!empty($obItem)) {
            return $obItem;
        }

        /** @var ElementItem $obItem */
        $obItem = app()->make(static::class, $arParamList);

        //Init cached array model data
        $obItem->setCachedData();

        ItemStorage::set(static::class, $iElementID, $obItem);

        return $obItem;
    }

    /**
     * Make new element item
     * @param int|string $iElementID
     * @param Model      $obElement
     * @return $this
     * @throws \Exception
     */
    public static function makeOnlyCache($iElementID, $obElement = null)
    {
        $arParamList = [
            'iElementID' => $iElementID,
            'obElement'  => $obElement,
        ];

        $obItem = ItemStorage::get(static::class, $iElementID);
        if (!empty($obItem)) {
            return $obItem;
        }

        /** @var $this $obItem */
        $obItem = app()->make(static::class, $arParamList);

        //Init cached array model data
        $obItem->setCachedData(false);
        if ($obItem->isEmpty()) {
            return $obItem;
        }

        ItemStorage::set(static::class, $iElementID, $obItem);

        return $obItem;
    }

    /**
     * Make new element item (no cache)
     * @param int    $iElementID
     * @param \Model $obElement
     * @return $this
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#makenocacheielementid-obelement--null
     * @see  \Lovata\Toolbox\Tests\Unit\ItemTest::testItem()
     */
    public static function makeNoCache($iElementID, $obElement = null)
    {
        $arParamList = [
            'iElementID' => $iElementID,
            'obElement'  => $obElement,
        ];

        /** @var $this $obItem */
        $obItem = app()->make(static::class, $arParamList);

        //Init array model data (no cache)
        $obItem->setData();

        return $obItem;
    }

    /**
     * Remove model data from cache
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#clearcacheielementid
     * @param int $iElementID
     */
    public static function clearCache($iElementID)
    {
        if (empty($iElementID)) {
            return;
        }

        ItemStorage::clear(static::class, $iElementID);
        CCache::clear(static::getCacheTag(), $iElementID);
    }

    /**
     * Check model data is empty
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#isempty
     * @return bool
     */
    public function isEmpty()
    {
        return empty($this->arModelData);
    }

    /**
     * Check model data is not empty
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#isnotempty
     * @return bool
     */
    public function isNotEmpty()
    {
        return !$this->isEmpty();
    }

    /**
     * Get model data
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#toarray
     * @return array
     */
    public function toArray()
    {
        return $this->arModelData;
    }

    /**
     * Get model data in JSON string
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#tojson
     * @return string
     */
    public function toJSON()
    {
        return json_encode($this->arModelData);
    }

    /**
     * Get model object
     * @link https://github.com/lovata/oc-toolbox-plugin/wiki/ElementItem#getobject
     *
     * @return \Model
     */
    public function getObject()
    {
        $this->initElementObject();

        return $this->obElement;
    }

    /**
     * Set data from model object
     */
    protected function setData()
    {
        $this->initElementObject();
        if (empty($this->obElement)) {
            return;
        }

        //Set default lang (if update cache with non default lang)
        if (self::$bLangInit && !empty(self::$sDefaultLang) && $this->obElement->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) {
            $this->obElement->lang(self::$sDefaultLang);
        }

        //Get cached field list from model and add fields to cache array
        $this->setCachedFieldList();

        //Get element data
        $arResult = $this->getElementData();
        if (empty($arResult) || !is_array($arResult)) {
            $arResult = [];
        }

        //Add fields values to cached array
        foreach ($arResult as $sField => $sValue) {
            $this->setAttribute($sField, $sValue);
        }

        //Save lang properties (integration with Translate plugin)
        $this->setLangProperties();

        //Run methods from $arExtendResult array
        $this->setExtendData();
    }

    /**
     * Get cached field list from model and add fields to cache array
     */
    protected function setCachedFieldList()
    {
        if (!$this->obElement->methodExists('getCachedField')) {
            return;
        }

        //Get cached field list
        $arFieldList = $this->obElement->getCachedField();
        if (empty($arFieldList)) {
            return;
        }

        foreach ($arFieldList as $sField) {
            if (array_key_exists($sField, (array) $this->obElement->attachOne)) {
                $arFileData = $this->getUploadFileData($this->obElement->$sField);
                $sFieldName = 'attachOne|'.$sField;
                $this->setAttribute($sFieldName, $arFileData);
            } elseif (array_key_exists($sField, (array) $this->obElement->attachMany)) {
                $arFileList = [];
                $obFileList = $this->obElement->$sField;
                foreach ($obFileList as $obFile) {
                    $arFileData = $this->getUploadFileData($obFile);
                    $arFileList[] = $arFileData;
                }

                $sFieldName = 'attachMany|'.$sField;
                $this->setAttribute($sFieldName, $arFileList);
            } else {
                $this->setAttribute($sField, $this->obElement->$sField);
            }
        }
    }

    /**
     * Run methods from $arExtendResult array
     */
    protected function setExtendData()
    {
        //Check extend result methods
        if (empty($this->arExtendResult)) {
            return;
        }

        //Apply extend methods
        foreach ($this->arExtendResult as $sMethodName) {
            if (empty($sMethodName) || !(method_exists($this, $sMethodName) || $this->methodExists($sMethodName))) {
                continue;
            }

            $this->$sMethodName();
        }
    }

    /**
     * Set cached brand data
     * @param bool $bWithQuery
     */
    protected function setCachedData($bWithQuery = true)
    {
        if (empty($this->iElementID)) {
            return;
        }

        $arCacheTags = static::getCacheTag();
        $sCacheKey = $this->iElementID;

        $this->arModelData = CCache::get($arCacheTags, $sCacheKey);
        if (!$this->isEmpty() || !$bWithQuery) {
            return;
        }

        $this->setData();

        //Set cache data
        CCache::forever($arCacheTags, $sCacheKey, $this->arModelData);
    }

    /**
     * Set model data from object
     * @return mixed
     */
    protected function getElementData()
    {
        return [];
    }

    /**
     * Check if the model needs to be booted and if so, do it.
     *
     * @return void
     */
    protected function bootIfNotBooted()
    {
        if (isset(static::$arBooted[static::class])) {
            return;
        }

        static::boot();
        static::$arBooted[static::class] = true;
    }

    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        static::bootTraits();
    }

    /**
     * Boot all of the bootable traits on the model.
     *
     * @return void
     */
    protected static function bootTraits()
    {
        foreach (class_uses_recursive(get_called_class()) as $trait) {
            if (method_exists(get_called_class(), $method = 'boot'.class_basename($trait))) {
                forward_static_call([get_called_class(), $method]);
            }
        }
    }

    /**
     * Init element object
     */
    protected function initElementObject()
    {
        $sModelClass = static::MODEL_CLASS;
        if (!empty($this->obElement) && !$this->obElement instanceof $sModelClass) {
            $this->obElement = null;
        }

        if (!empty($this->obElement) || empty($this->iElementID)) {
            return;
        }

        $this->setElementObject();
    }

    /**
     * Set element object
     */
    protected function setElementObject()
    {
        $sModelClass = static::MODEL_CLASS;

        $obQuery = $sModelClass::where(static::QUERY_FIELD, $this->iElementID);
        if (method_exists($sModelClass, 'trashed')) {
            $obQuery->withTrashed();
        }

        if (!empty(static::$arQueryWith)) {
            $obQuery->with(static::$arQueryWith);
        }

        $this->obElement = $obQuery->first();
    }

    /**
     * Get cache tag array for model
     * @return array
     */
    protected static function getCacheTag()
    {
        return [static::class];
    }

    /**
     * Process translatable fields and save values, how 'field_name|lang_code'
     */
    private function setLangProperties()
    {
        if (empty($this->obElement) || !$this->obElement->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) {
            return;
        }

        //Check translate model property
        if (empty($this->obElement->translatable) || !is_array($this->obElement->translatable)) {
            return;
        }

        //Get active lang list from Translate plugin
        $arLangList = self::getActiveLangList();
        if (empty($arLangList)) {
            return;
        }

        //Process translatable fields
        foreach ($this->obElement->translatable as $sField) {
            //Check field name
            if (empty($sField) || (!is_string($sField) && !is_array($sField))) {
                continue;
            }

            if (is_array($sField)) {
                $sField = array_shift($sField);
            }

            if (!isset($this->arModelData[$sField]) || array_key_exists($sField, (array) $this->obElement->attachOne) || array_key_exists($sField, (array) $this->obElement->attachMany)) {
                continue;
            }

            //Save field value with different lang code
            foreach ($arLangList as $sLangCode) {
                $sLangField = $sField.'|'.$sLangCode;
                $sValue = $this->obElement->lang($sLangCode)->$sField;
                $this->setAttribute($sLangField, $sValue);
            }
        }
    }

    /**
     * Get image data from image object
     * @param \System\Models\File $obFile
     * @return []
     */
    private function getUploadFileData($obFile) : array
    {
        if (empty($obFile)) {
            return [];
        }

        //Set default lang in image object
        if (!empty(self::$sDefaultLang) && $obFile->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) {
            $obFile->lang(self::$sDefaultLang);
        }

        //Convert image data to array
        $arFileData = $obFile->toArray();
        $arLangList = $this->getActiveLangList();
        if (empty($arLangList) || !$obFile->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) {
            return $arFileData;
        }

        //Add lang fields to array
        foreach ($arLangList as $sLangCode) {
            $arFileData[$sLangCode] = [];
            foreach ($obFile->translatable as $sLangField) {
                $arFileData[$sLangCode][$sLangField] = $obFile->lang($sLangCode)->$sLangField;
            }
        }

        return $arFileData;
    }
}
