<?php

/**
 * @package         EngageBox
 * @version         7.1.0 Free
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace Tassos\EngageBox;

defined('_JEXEC') or die('Restricted access');

use Joomla\Registry\Registry;
use Joomla\CMS\Factory;

/**
 * EngageBox Migrator Class helps us fix and prevent backward compatibility issues between release updates.
 */
class Migrator
{
    /**
     * The database class
     *
     * @var object
     */
    private $db;

    /**
     * The destination database class
     * 
     * @var object
     */
    private $dbDestination;

    /**
     * Indicates the current installed version of the extension
     *
     * @var string
     */
    private $installedVersion;
    
    /**
     * Class constructor
     *
     * @param string $installedVersion  The current extension version
     */
    public function __construct($installedVersion, $dbSource = null, $dbDestination = null)
    {
        $this->db = $dbSource ? $dbSource : Factory::getDbo();
        $this->dbDestination = $dbDestination ? $dbDestination : Factory::getDbo();
        $this->installedVersion = $installedVersion;
    }
    
    /**
     * Start the migration process
     *
     * @return void
     */
    public function start()
    {
        $this->removeTemplatesData();
       
        if (!$data = $this->getBoxes())
        {
            return;
        }

        foreach ($data as $key => $box)
        {
            $box->params = new Registry($box->params);

            if (version_compare($this->installedVersion, '4.0.0', '<=')) 
            {
                $this->moveCloseOpenedBoxesOptionToActions($box);
                $this->moveAutoCloseTimerToActions($box);
                $this->fixCustomCSS($box);
                $this->fixCloseButton($box);
            }
            
            if (version_compare($this->installedVersion, '5.0.1', '<=')) 
            {
                // Pass only params
                \NRFramework\Conditions\Migrator::run($box->params);
            }
            
            if (version_compare($this->installedVersion, '6.1.2', '<='))
            {
                $this->updateEcommerceConditions($box->params);
            }

            if (version_compare($this->installedVersion, '7.0.0', '<='))
            {
                $this->updateBoxAnimations($box->params);
                $this->updateOnPageReadyTrigger($box);
            }
            
            // Update box using id as the primary key.
            $box->params = json_encode($box->params);

            $this->dbDestination->updateObject('#__rstbox', $box, 'id');
        }
    }

    /**
     * Get all boxes from the database
     *
     * @return array
     */
    private function getBoxes()
    {
        $db = $this->db;
    
        $query = $db->getQuery(true)
            ->select('*')
            ->from('#__rstbox');
        
        $db->setQuery($query);
    
        return $db->loadObjectList();
    }

    private function fixCloseButton(&$box)
    {
        if (!$box->params->offsetExists('closebutton.hide'))
        {
            return;
        }

        $close_button_value = $box->params->get('closebutton.hide', false) ? 0 : 1;
        $box->params->set('closebutton.show', $close_button_value);
        $box->params->remove('closebutton.hide');
    }

    private function fixCustomCSS(&$box)
    {
        if (!$css = $box->params->get('customcss'))
        {
            return;
        }

        $replacements = [
            '.rstbox-heading' => '.eb-header', // rstbox-heading no longer exist
            '.rstboxes' => '',
            '.rstbox-'  => '.eb-',
            '.rstbox_'  => '.eb-',
            '#rstbox_' . $box->id  => '.eb-' . $box->id . ' .eb-dialog'
        ];
        
        $css_new = str_ireplace(array_keys($replacements), array_values($replacements), $css);
        
        if ($css == $css_new)
        {
            return;
        }

        $box->params->set('customcss', $css_new);
    }

    private function moveCloseOpenedBoxesOptionToActions(&$box)
    {
        if (!$box->params->get('closeopened', false))
        {
            return;
        }   

        $actions = (array) $box->params->get('actions', []);

        $actions[] = [
            'enabled' => true,
            'when'    => 'open',
            'do'      => 'closeall'
        ];

        $box->params->set('actions', $actions);
        $box->params->remove('closeopened');
    }

    private function moveAutoCloseTimerToActions(&$box)
    {
        if (!$box->params->get('autoclose', false))
        {
            return;
        }

        if ((int) $box->params->get('autoclosevalue', 0) == 0)
        {
            return;
        }

        $actions = (array) $box->params->get('actions', []);

        $actions[] = [
            'enabled' => true,
            'when'    => 'afterOpen',
            'do'      => 'closebox',
            'box'     => $box->id,
            'delay'   => $box->params->get('autoclosevalue', 0) / 1000 // Convert ms to sec.
        ];

        $box->params->set('actions', $actions);
        $box->params->remove('autoclose');
    }

    /**
     * Updates eCommerce Conditions:
     * 
     * CartContainsProducts
     * CartContainsXProducts
     * CartValue
     * 
     * @param   object  $params
     * 
     * @return  void
     */
    private function updateEcommerceConditions(&$params)
    {
        $this->updateCartContainsProductsCondition($params);
        $this->updateCartContainsXProductsCondition($params);
        $this->updateCartValueCondition($params);
    }

    /**
     * Migrates value of CartContainsProducts Condition for both Hikashop & VirtueMart.
     * 
     * @param   object  $param
     * 
     * @return  void
     */
    private function updateCartContainsProductsCondition(&$params)
    {
        if (!$rules = $params->get('rules', []))
        {
            return;
        }

        /**
         * $rules may be either a string or an array of objects.
         * For this reason, we transform all cases to arrays.
         */
        if (is_array($rules))
        {
            $rules = json_encode($rules);
        }
        
        if (!$rules = json_decode($rules, true))
        {
            return;
        }

        $allowed_rules = [
            'Component\HikashopCartContainsProducts',
            'Component\VirtueMartCartContainsProducts'
        ];

        $changed = false;

        foreach ($rules as &$ruleset)
        {
            if (!isset($ruleset['rules']))
            {
                continue;
            }
            
            foreach ($ruleset['rules'] as &$rule)
            {
                if (!isset($rule['name']))
                {
                    continue;
                }
                
                if (!in_array($rule['name'], $allowed_rules))
                {
                    continue;
                }

                if (!isset($rule['value']))
                {
                    continue;
                }

                if (!is_array($rule['value']))
                {
                    continue;
                }

                if (!count($rule['value']))
                {
                    continue;
                }

                $changed = true;

                $newValue = [];
                foreach ($rule['value'] as $index => $id)
                {
                    $key = 'value' . $index;
                    
                    // Old value wasn't an array, skip if new value was found
                    if (is_array($id))
                    {
                        $newValue[$key] = $id;
                    }
                    else
                    {
                        $newValue[$key] = [
                            'value' => $id,
                            'params' => [
                                'operator' => 'any',
                                'value' => '1',
                                'value2' => '1'
                            ]
                        ];
                    }
                }
                
                $rule['value'] = $newValue;
            }
        }

        if ($changed)
        {
            $params->set('rules', $rules);
        }
    }

    /**
     * Migrates value of CartContainsXProducts Condition for both Hikashop & VirtueMart.
     * 
     * @param   object  $param
     * 
     * @return  void
     */
    private function updateCartContainsXProductsCondition(&$params)
    {
        if (!$rules = $params->get('rules', []))
        {
            return;
        }

        /**
         * $rules may be either a string or an array of objects.
         * For this reason, we transform all cases to arrays.
         */
        if (is_array($rules))
        {
            $rules = json_encode($rules);
        }
        
        if (!$rules = json_decode($rules, true))
        {
            return;
        }

        $allowed_rules = [
            'Component\HikashopCartContainsXProducts',
            'Component\VirtueMartCartContainsXProducts'
        ];

        $changed = false;

        foreach ($rules as &$ruleset)
        {
            if (!isset($ruleset['rules']))
            {
                continue;
            }
            
            foreach ($ruleset['rules'] as &$rule)
            {
                
                if (!isset($rule['name']))
                {
                    continue;
                }
                
                if (!in_array($rule['name'], $allowed_rules))
                {
                    continue;
                }

                if (!isset($rule['value']))
                {
                    continue;
                }

                if (!is_scalar($rule['value']))
                {
                    continue;
                }

                // Abort if params property is set
                if (isset($rule['params']) && is_array($rule['params']) && count($rule['params']))
                {
                    continue;
                }

                $rule['params'] = [
                    'operator' => $rule['operator']
                ];

                if (isset($rule['operator']))
                {
                    unset($rule['operator']);
                }
                
                $changed = true;
            }
        }

        if ($changed)
        {
            $params->set('rules', $rules);
        }
    }

    /**
     * Migrates value of CartValue Condition for both Hikashop & VirtueMart.
     * 
     * @param   object  $param
     * 
     * @return  void
     */
    private function updateCartValueCondition(&$params)
    {
        if (!$rules = $params->get('rules', []))
        {
            return;
        }

        /**
         * $rules may be either a string or an array of objects.
         * For this reason, we transform all cases to arrays.
         */
        if (is_array($rules))
        {
            $rules = json_encode($rules);
        }
        
        if (!$rules = json_decode($rules, true))
        {
            return;
        }

        $allowed_rules = [
            'Component\HikashopCartValue',
            'Component\VirtueMartCartValue'
        ];

        $changed = false;

        foreach ($rules as &$ruleset)
        {
            if (!isset($ruleset['rules']))
            {
                continue;
            }
            
            foreach ($ruleset['rules'] as &$rule)
            {
                
                if (!isset($rule['name']))
                {
                    continue;
                }
                
                if (!in_array($rule['name'], $allowed_rules))
                {
                    continue;
                }

                if (!isset($rule['value']))
                {
                    continue;
                }

                if (!is_scalar($rule['value']))
                {
                    continue;
                }

                // Abort if params property is properly set
                if (isset($rule['params']) && is_array($rule['params']) && count($rule['params']) && isset($rule['params']['value']))
                {
                    continue;
                }

                $rule['params'] = [
                    'total' => 'total',
                    'operator' => $rule['operator'],
                    'value2' => '',
                    'exclude_shipping_cost' => isset($rule['params']['exclude_shipping_cost']) ? $rule['params']['exclude_shipping_cost'] : '0'
                ];

                if (isset($rule['operator']))
                {
                    unset($rule['operator']);
                }
                
                $changed = true;
            }
        }

        if ($changed)
        {
            $params->set('rules', $rules);
        }
    }

    /**
     * Removes the templates.json & favorites.json file from EngageBox installations <= 5.2.0.
     * 
     * This is to ensure the customers download the latest templates.json file
     * the first time they upgrade to EngageBox 5.2.0 
     * from the remote endpoint as the existing templates wont work with the
     * new Templates Library.
     * 
     * Also favorites.json contains outdated IDs so we remove it to start clean.
     * 
     * @since  5.2.0
     */
    private function removeTemplatesData()
    {
        if (version_compare($this->installedVersion, '5.2.0', '>')) 
        {
            return;
        }

        $this->deleteTemplatesJSONFile();
        $this->deleteTemplatesFavoritesJSONFile();
    }

    /**
     * Removes the templates/templates.json file.
     * 
     * @return  void
     */
    private function deleteTemplatesJSONFile()
    {
        $path = JPATH_ROOT . '/media/com_rstbox/templates/templates.json';
        if (!file_exists($path))
        {
            return;
        }

        unlink($path);
    }

    /**
     * Removes the templates/favorites.json file.
     * 
     * @return  void
     */
    private function deleteTemplatesFavoritesJSONFile()
    {
        $path = JPATH_ROOT . '/media/com_rstbox/templates/favorites.json';
        if (!file_exists($path))
        {
            return;
        }

        unlink($path);
    }

    /**
     * Migrates box animations from Velocity.js to Animate.css.
     * 
     * @param  mixed $params  The box parameters.
     * 
     * @return void
     */
    public static function updateBoxAnimations(&$params)
    {
        // Convert to Registry object if it's not already one
        $isRegistry = ($params instanceof Registry);
        if (!$isRegistry)
        {
            $params = new Registry($params);
        }
        
        $velocityToAnimateCssMap = [
            // Fade animations
            'transition.fadeIn' => 'fadeIn',
            'transition.fadeOut' => 'fadeOut',
            'transition.slideUpIn' => 'fadeInUp',
            'transition.slideDownIn' => 'fadeInDown',
            'transition.slideLeftIn' => 'fadeInLeft',
            'transition.slideRightIn' => 'fadeInRight',
            'transition.slideUpBigIn' => 'fadeInUpBig',
            'transition.slideDownBigIn' => 'fadeInDownBig',
            'transition.slideLeftBigIn' => 'fadeInLeftBig',
            'transition.slideRightBigIn' => 'fadeInRightBig',
            'transition.slideUpOut' => 'fadeOutUp',
            'transition.slideDownOut' => 'fadeOutDown',
            'transition.slideLeftOut' => 'fadeOutLeft',
            'transition.slideRightOut' => 'fadeOutRight',
            'transition.slideUpBigOut' => 'fadeOutUpBig',
            'transition.slideDownBigOut' => 'fadeOutDownBig',
            'transition.slideLeftBigOut' => 'fadeOutLeftBig',
            'transition.slideRightBigOut' => 'fadeOutRightBig',
            // Slide animations
            'slideDown' => 'slideInDown',
            'slideUp' => 'slideOutUp',
            'rstbox.slideUpIn' => 'slideInUp',
            'rstbox.slideDownIn' => 'slideInDown',
            'rstbox.slideLeftIn' => 'slideInLeft',
            'rstbox.slideRightIn' => 'slideInRight',
            'rstbox.slideUpOut' => 'fadeOutDown',
            'rstbox.slideDownOut' => 'slideOutUp',
            'rstbox.slideLeftOut' => 'slideOutLeft',
            'rstbox.slideRightOut' => 'slideOutRight',
            'transition.perspectiveUpIn' => 'slideInUp',
            'transition.perspectiveDownIn' => 'slideInDown',
            'transition.perspectiveLeftIn' => 'slideInLeft',
            'transition.perspectiveRightIn' => 'slideInRight',
            'transition.perspectiveUpOut' => 'slideOutUp',
            'transition.perspectiveDownOut' => 'slideOutDown',
            'transition.perspectiveLeftOut' => 'slideOutLeft',
            'transition.perspectiveRightOut' => 'slideOutRight',
            // Other animations
            'callout.bounce' => 'bounce',
            'callout.shake' => 'shakeX',
            'callout.flash' => 'flash',
            'callout.pulse' => 'pulse',
            'callout.swing' => 'swing',
            'callout.tada' => 'tada',
            'transition.swoopIn' => 'zoomIn',
            'transition.whirlIn' => 'zoomIn',
            'transition.shrinkIn' => 'zoomIn',
            'transition.expandIn' => 'zoomIn',
            'transition.flipXIn' => 'flipInX',
            'transition.flipYIn' => 'flipInY',
            'transition.flipBounceXIn' => 'flipInX',
            'transition.flipBounceYIn' => 'flipInY',
            'transition.bounceIn' => 'bounceIn',
            'transition.bounceUpIn' => 'bounceInUp',
            'transition.bounceDownIn' => 'bounceInDown',
            'transition.bounceLeftIn' => 'bounceInLeft',
            'transition.bounceRightIn' => 'bounceInRight',
            'transition.swoopOut' => 'zoomOut',
            'transition.whirlOut' => 'zoomOut',
            'transition.shrinkOut' => 'zoomOut',
            'transition.expandOut' => 'zoomOut',
            'transition.flipXOut' => 'flipOutX',
            'transition.flipYOut' => 'flipOutY',
            'transition.flipBounceXOut' => 'flipOutX',
            'transition.flipBounceYOut' => 'flipOutY',
            'transition.bounceOut' => 'bounceOut',
            'transition.bounceUpOut' => 'bounceOutUp',
            'transition.bounceDownOut' => 'bounceOutDown',
            'transition.bounceLeftOut' => 'bounceOutLeft',
            'transition.bounceRightOut' => 'bounceOutRight',
        ];

        // If animationin/out is set, we check if it exists in the Velocity<->Animate.css map and update it accordingly
        if ($animationIn = $params->get('animationin', false))
        {
            if (isset($velocityToAnimateCssMap[$animationIn]))
            {
                $params->set('animationin', $velocityToAnimateCssMap[$animationIn]);
            }
        }

        if ($animationOut = $params->get('animationout', false))
        {
            if (isset($velocityToAnimateCssMap[$animationOut]))
            {
                $params->set('animationout', $velocityToAnimateCssMap[$animationOut]);
            }
        }

        // Convert $params back to standard array if it wasn't originally a Registry
        if (!$isRegistry)
        {
            $params = $params->toArray();
        }
    }

    /**
     * Migrate the onPageReady trigger to onPageLoad
     */
    public static function updateOnPageReadyTrigger(&$box)
    {        
        if (!$box->triggermethod)
        {
            return;
        }

        if ($box->triggermethod === 'pageready')
        {
            $box->triggermethod = 'pageload';
            $box->params->set('early_trigger', 1);
        }
    } 
}