<?php
namespace Core\App;

use Core\App\Install\Setting;
use Core\App\Install\Phrase;
use Core\App\Install\Database;
use Core\HTTP\Cache;
use Phpfox;
use Phpfox_Installer;
use User_Service_Group_Group;
use Admincp_Service_Menu_Process;

/**
 * Class App
 * @author  Neil
 * @version 4.5.0
 * @package Core\App
 */
abstract class App
{
    /** an app CAN'T change this ID, if change, it will become another app
     *
     * @var string
     */
    public $id;

    /**
     * @var string is alias name of this App. This value shouldn't change
     */
    public $alias;

    /**
     * @var string is name of this App. You can change this value, but we don't recommend
     */
    public $name;

    /**
     * @var string is version of this App. We recommend you use 4.x or 4.x.x
     */
    public $version;

    /**
     * @var string is icon for this app. It can a link of image, a font-awesome class. If it empty or not set, we will
     *      use file "icon.png" is root path of your app. If file "icon.png" doesn't exist, we auto generate two
     *      characters for your icon.
     */
    public $icon;

    /**
     * @var string
     */
    public $admincp_route;

    /**
     * @var string
     */
    public $admincp_menu;

    /**
     * @var string
     */
    public $admincp_help;

    /**
     * @var string
     */
    public $admincp_action_menu;

    /**
     * @var string
     */
    public $map;

    /**
     * @var string
     */
    public $map_search;

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

    /**
     * @var array
     */
    public $settings;

    /**
     * @var Setting\Site
     */
    protected $_settings;

    /**
     * @var Setting\Groups
     */
    protected $_user_group_settings;

    /**
     * @var array
     */
    public $user_group_settings;

    /**
     * @var
     */
    public $notifications;

    /**
     * @var
     */
    public $database;

    /**
     * @var
     */
    public $component;

    /**
     * @var
     */
    public $component_block;

    public $component_block_remove;

    /**
     * @var
     */
    public $routes;

    /**
     * @var string
     */
    public $path;

    /**
     * @var string
     */
    public $start_support_version = '';

    /**
     * @var string
     */
    public $end_support_version = '';

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

    /**
     * @var Phrase\Phrase
     */
    private $_phrase;

    private $_aErrorMessages = [];

    /**
     * Name of publisher
     *
     * @var string
     */
    public $_publisher = 'n/a';

    /**
     * Home page of publisher
     *
     * @var string
     */
    public $_publisher_url = '';

    public $_apps_dir = '';

    /**
     * @var array store errors from this app
     */
    protected $_errors;

    /**
     * @var array of core apps
     */
    private $_aCores = [
        'PHPfox_Core',
        'PHPfox_Flavors',
    ];

    /**
     * @var bool
     */
    public $_admin_cp_menu_ajax = true;

    private $_aOfficial = [
        'PHPfox_AmazonS3',
        'PHPfox_CDN',
        'PHPfox_CDN_Service',
        'PHPfox_Core',
        'PHPfox_Facebook',
        'PHPfox_Flavors',
        'PHPfox_Groups',
        'PHPfox_IM',
        'PHPfox_reCAPTCHA',
        'PHPfox_Twemoji_Awesome',
        'PHPfox_Videos'
    ];

    /**
     * App constructor.
     */
    public function __construct()
    {
        $this->_phrase = new Phrase\Phrase();
        $this->setId();
        $this->setAlias();
        $this->setName();
        $this->setSupportVersion();
        $this->setVersion();
        $this->setSettings();
        $this->setUserGroupSettings();
        $this->setComponent();
        $this->setComponentBlock();
        $this->setOthers();
        $this->addPhrases($this->phrase);

        if (!is_array($this->admincp_menu)) {
            $this->admincp_menu = [
                $this->name => '#'
            ];
        }

        // if don't set apps_dir, we use app_id
        if (empty($this->_apps_dir)) {
            $this->_apps_dir = $this->id;
        }
        $this->path = PHPFOX_DIR_SITE . 'Apps' . PHPFOX_DS . $this->_apps_dir . PHPFOX_DS;

        $this->initData();
        $attributes = get_object_vars($this);
        foreach ($attributes as $iKey => $attribute) {
            if (is_array($attribute) && substr($iKey, 0, 1) != '_') {
                $this->{$iKey} = json_decode(json_encode($attribute));
            }
        }
        //add phrase from json
        $sPath = PHPFOX_DIR_SITE . 'Apps' . PHPFOX_DS . $this->_apps_dir . PHPFOX_DS . 'phrase.json';
        if (file_exists($sPath)) {
            $aJsonPhrases = json_decode(file_get_contents($sPath), true);
            $this->addPhrases($aJsonPhrases);
        }
    }

    /**
     * Set ID
     */
    abstract protected function setId();

    /**
     * Set start and end support version of your App.
     * @example   $this->start_support_version = 4.2.0
     * @example   $this->end_support_version = 4.5.0
     * @see       list of our verson at PF.Base/install/include/installer.class.php ($_aVersions)
     * @important You DO NOT ALLOW to set current version of phpFox for start_support_version and end_support_version. We will reject of app if you use current version of phpFox for these variable. These variables help clients know their sites is work with your app or not.
     */
    abstract protected function setSupportVersion();

    /**
     * Set Alias
     */
    abstract protected function setAlias();

    /**
     * Set name
     */
    abstract protected function setName();

    /**
     * Set version
     */
    abstract protected function setVersion();

    /**
     * Set phrases
     */
    abstract protected function setPhrase();

    /**
     * Set settings
     */
    abstract protected function setSettings();

    /**
     * Set user group settings
     */
    abstract protected function setUserGroupSettings();

    /**
     * Set component
     */
    abstract protected function setComponent();

    /**
     * Set component block
     */
    abstract protected function setComponentBlock();

    /*
     * Set other attributes for this app
     */
    abstract protected function setOthers();

    /**
     * Check this App is valid
     *
     * @return bool
     */
    public function isValid()
    {
        if ($this->isCore()) {
            return true;
        }

        if (empty($this->id)) {
            $this->_aErrorMessages[] = "Missing app_id";
            return false;
        }

        if (empty($this->name)) {
            $this->_aErrorMessages[] = "Missing App name";
            return false;
        }

        if (empty($this->version)) {
            $this->_aErrorMessages[] = "Missing Version";
            return false;
        }

        //Check min version
        if (!empty($this->start_support_version) && version_compare($this->start_support_version, Phpfox::VERSION, ">=")) {
            $this->_aErrorMessages[] = "This app requires phpFox " . $this->start_support_version . " or newer";
            return false;
        }

        //We have to have alias for remove/disable menu when uninstall/disable
        if (count($this->menu) && empty($this->alias)) {
            return false;
        }

        return true;
    }

    /**
     * Init data for this app
     */
    private function initData()
    {
        //Setting
        if (is_array($this->settings)) {
            if (count($this->settings)) {
                foreach ($this->settings as $key => $setting) {
                    if (!isset($setting['var_name'])) {
                        $setting['var_name'] = $key;
                    }
                    $oSetting = new Setting\Site($setting);
                    if ($oSetting->isValid()) {
                        $this->_phrase->addPhrase($oSetting->getPhraseVarName(), $oSetting->getPhraseValue());
                        $this->_settings[] = $oSetting;
                    } else {
                        $this->_errors[] = $oSetting->getError();
                    }
                }
            }
        }

        //User groups setting
        if (count($this->user_group_settings)) {
            foreach ($this->user_group_settings as $key => $group_setting) {
                if (!isset($group_setting['var_name'])) {
                    $group_setting['var_name'] = $key;
                }
                $oGroupSetting = new Setting\Groups($group_setting);
                if ($oGroupSetting->isValid()) {
                    $this->_phrase->addPhrase($oGroupSetting->getPhraseVarName(), $oGroupSetting->getPhraseValue());
                    $this->_user_group_settings[] = $oGroupSetting;
                } else {
                    $this->_errors[] = $oGroupSetting->getError();
                }
            }
        }
        //Icon
        if (!isset($this->icon) || empty($this->icon)) {
            if (file_exists($this->path . 'icon.png')) {
                $this->icon = Phpfox::getParam('core.path_actual') . 'PF.Site/Apps/' . $this->_apps_dir . '/icon.png';
            }
        } elseif (filter_var($this->icon, FILTER_VALIDATE_URL) === false && strpos($this->icon, 'fa') !== false) {
            $name = $this->name[0];
            $parts = explode(' ', $this->name);
            if (isset($parts[1])) {
                $name .= trim($parts[1])[0];
            } elseif (isset($this->name[1])) {
                $name .= $this->name[1];
            }
            $class_color = '_' . $name;
            $name = '<i class="fa ' . $this->icon . '" aria-hidden="true"></i></i>';
            $this->icon = '<b class="app_icons"><i class="app_icon ' . strtolower($class_color) . '">' . $name . '</i></b>';
        }
        //phrase
        $this->setPhrase();
    }

    /**
     * Add a database table for this app
     *
     * @param $database Database\Table
     */
    protected function addDatabase($database)
    {
        if ($database - $this->isValid()) {
            $this->database[] = $database;
        }
    }

    /**
     * Add new phrases
     *
     * @param array $aParams
     */
    protected function addPhrases($aParams)
    {
        foreach ($aParams as $var_name => $value) {
            $this->_phrase->addPhrase($var_name, $value);
        }
    }

    /**
     * Get all phrases of this app
     *
     * @return array
     */
    public function getPhrases()
    {
        return $this->_phrase->all();
    }

    /**
     * @param bool $bRemoveDb
     */
    public function uninstall($bRemoveDb = true)
    {
        if ($bRemoveDb && is_array($this->database) && count($this->database)) {
            foreach ($this->database as $database) {
                $sNamespace = "\\Apps\\" . $this->id . "\\Installation\\Database\\" . $database;
                if (class_exists($sNamespace)) {
                    /**
                     * @var $oDatabase \Core\App\Install\Database\Table
                     */
                    $oDatabase = new $sNamespace();
                    $oDatabase->drop();
                }
            }
        }
        if (isset($this->alias) && !empty($this->alias)) {
            //Delete block, component
            db()->delete(':block', "module_id='" . $this->alias . "'");
            db()->delete(':component', "module_id='" . $this->alias . "'");
            //delete alias
            db()->delete(':module', "module_id='" . $this->alias . "'");
        }
        db()->delete(':apps', 'apps_id="' . $this->id . '"');
    }

    /**
     * Process install/upgrade for this app
     * @return bool
     */
    public function processInstall()
    {
        if (!$this->isValid()) {
            return false;
        }

        $iCnt = db()->select('COUNT(*)')
            ->from(':apps')
            ->where('apps_id="' . $this->id . '"')
            ->execute('getSlaveField');

        if (!$iCnt) {
            db()->insert(':apps', [
                'apps_id' => $this->id,
                'apps_dir' => $this->_apps_dir,
                'apps_name' => $this->name,
                'version' => $this->version,
                'apps_alias' => isset($this->alias) ? $this->alias : '',
                'author' => $this->_publisher,
                'vendor' => $this->_publisher_url,
                'apps_icon' => $this->icon,
                'type' => ($this->isCore()) ? 1 : 2,
                'is_active' => 1,
            ]);
        } else {
            //upgrade case
            $aUpdate = [
                'apps_name' => $this->name,
                'version' => $this->version,
                'apps_dir' => $this->_apps_dir,
                'apps_alias' => isset($this->alias) ? $this->alias : '',
                'author' => $this->_publisher,
                'vendor' => $this->_publisher_url,
                'apps_icon' => $this->icon,
            ];
            db()->update(':apps', $aUpdate, 'apps_id="' . $this->id . '"');
        }

        if (is_array($this->database) && count($this->database)) {
            foreach ($this->database as $database) {
                $sNamespace = "\\Apps\\" . $this->id . "\\Installation\\Database\\" . $database;
                if (class_exists($sNamespace)) {
                    /**
                     * @var $oDatabase \Core\App\Install\Database\Table
                     */
                    $oDatabase = new $sNamespace();
                    $oDatabase->install();
                }
            }
        }
        //Add Phrase
        $aPhrases = $this->_phrase->all();
        \Core\Lib::phrase()->addPhrase($aPhrases);

        //Add Alias to table :module
        if (isset($this->alias)) {
            //Check Alias is exist
            $iCnt = db()->select('COUNT(*)')
                ->from(':module')
                ->where('module_id = "' . $this->alias . '"')
                ->execute('getSlaveField');
            if (!$iCnt) {
                $aInsert = [
                    'module_id' => $this->alias,
                    'product_id' => 'phpfox',
                    'is_core' => '0',
                    'is_active' => '1',
                    'is_menu' => '0',
                    'menu' => '',
                    'phrase_var_name' => 'module_apps'
                ];
                db()->insert(':module', $aInsert);
            }
        }

        //Add Component
        if (isset($this->component)) {
            $InsertData = [];
            if (isset($this->component->block)) {
                foreach ($this->component->block as $key => $value) {
                    //Check is exist
                    $iCnt = db()->select('COUNT(*)')
                        ->from(':component')
                        ->where('component="' . $key . '" AND m_connection="' . $value . '" AND module_id="' . $this->alias . '" AND is_controller=0')
                        ->executeField();
                    if ($iCnt) {
                        //Do not add duplicate component
                        continue;
                    }
                    $InsertData[] = [
                        $key, //component
                        $value,//m_connection
                        $this->alias,//module_id
                        'phpfox',//product_id
                        0,//is_controller
                        1,//is_block
                        1,//is_active
                    ];
                }
            }
            if (isset($this->component->controller)) {
                foreach ($this->component->controller as $key => $value) {
                    //Check is exist
                    $iCnt = db()->select('COUNT(*)')
                        ->from(':component')
                        ->where('component="' . $key . '" AND m_connection="' . $value . '" AND module_id="' . $this->alias . '" AND is_controller=1')
                        ->executeField();
                    if ($iCnt) {
                        //Do not add duplicate component
                        continue;
                    }
                    $InsertData[] = [
                        $key, //component
                        $value,//m_connection
                        $this->alias,//module_id
                        'phpfox',//product_id
                        1,//is_controller
                        0,//is_block
                        1,//is_active
                    ];
                }
            }
            if (count($InsertData)) {
                db()->multiInsert(Phpfox::getT('component'), [
                    'component',
                    'm_connection',
                    'module_id',
                    'product_id',
                    'is_controller',
                    'is_block',
                    'is_active',
                ], $InsertData);
            }
        }
        //Add component block
        if (isset($this->component_block)) {
            $InsertData = [];
            foreach ($this->component_block as $key => $value) {
                $sModuleId = (isset($value->module_id) ? $value->module_id : $this->alias);
                //Check block is exist
                $iCnt = db()->select('COUNT(*)')
                    ->from(':block')
                    ->where('m_connection="' . $value->m_connection . '" AND component= "' . $value->component . '" AND module_id="' . $sModuleId . '"')
                    ->executeField();
                if ($iCnt) {
                    //this block is exist
                    continue;
                }
                $InsertData[] = [
                    $key, //title
                    $value->type_id,//type_id
                    $value->m_connection,//m_connection
                    $sModuleId,//module_id
                    'phpfox',//product_id
                    $value->component,//component
                    $value->location,//location
                    $value->is_active,//is_active
                    $value->ordering,//ordering
                    (isset($value->disallow_access) ? $value->disallow_access : null),//disallow_access
                    (isset($value->can_move) ? $value->can_move : '0'),//can_move
                    (isset($value->version_id) ? $value->version_id : null),//version_id
                ];
            }
            if (count($InsertData)) {
                db()->multiInsert(Phpfox::getT('block'), [
                    'title',
                    'type_id',
                    'm_connection',
                    'module_id',
                    'product_id',
                    'component',
                    'location',
                    'is_active',
                    'ordering',
                    'disallow_access',
                    'can_move',
                    'version_id',
                ], $InsertData);
            }
        }
        //Remove component block
        if (is_array($this->component_block_remove) && count($this->component_block_remove)) {
            foreach ($this->component_block_remove as $aBlockRemove) {
                if (!isset($aBlockRemove->m_connection) || !isset($aBlockRemove->component)) {
                    continue;
                }
                db()->delete(Phpfox::getT('block'), [
                    'module_id' => $this->alias,
                    'm_connection' => $aBlockRemove->m_connection,
                    'component' => $aBlockRemove->component
                ]);
            }
        }
        //Add setting
        if (isset($this->settings)) {
            foreach ($this->settings as $key => $settingValue) {
                //Check the setting is exist
                $iCnt = Phpfox::getLib('database')->select('COUNT(*)')
                    ->from(':setting')
                    ->where(['var_name' => $key])
                    ->executeField();
                if ($iCnt) {
                    //Update setting value
                    Phpfox::getLib('database')->update(':setting', [
                        'module_id' => $this->alias,
                        'product_id' => $this->id,
                        'is_hidden' => 1,
                        'version_id' => $this->version,
                        'type_id' => isset($settingValue->type) ? $settingValue->type : 'input:text',
                        'phrase_var_name' => $key,
                        'value_default' => isset($settingValue->value) ? $settingValue->value : ''
                    ], [
                        'var_name' => $key
                    ]);
                } else {
                    //Add new setting value
                    Phpfox::getLib('database')->insert(':setting', [
                        'module_id' => $this->alias,
                        'product_id' => $this->id,
                        'is_hidden' => 1,
                        'version_id' => $this->version,
                        'type_id' => isset($settingValue->type) ? $settingValue->type : 'input:text',
                        'var_name' => $key,
                        'phrase_var_name' => $key,
                        'value_actual' => isset($settingValue->value) ? $settingValue->value : '',
                        'value_default' => isset($settingValue->value) ? $settingValue->value : ''
                    ]);
                }
            }
        }
        //Add user group setting
        if (isset($this->user_group_settings)) {
            $userGroups = Phpfox::getService('user.group')->get();
            $userGroupSettings = [];
            foreach ($userGroups as $group) {
                $aUserGroupValue = [];
                foreach ($this->user_group_settings as $key => $value) {
                    if (!isset($value->value)) {
                        $value->value = '';
                    }
                    if (is_object($value->value) && isset($value->value->{$group['user_group_id']})) {
                        $aUserGroupValue[$key] = $value->value->{$group['user_group_id']};
                    } elseif (is_object($value->value)) {
                        $aUserGroupValue[$key] = $value->value->{2};
                    } else {
                        $aUserGroupValue[$key] = $value->value;
                    }
                }
                $userGroupSettings[$group['user_group_id']] = $aUserGroupValue;
            }
            foreach ($userGroupSettings as $group_id => $values) {
                foreach ($values as $key => $value) {
                    db()->delete(':user_group_custom', [
                        'user_group_id' => $group_id,
                        'module_id' => 'app_' . $this->id,
                        'name' => $key
                    ]);
                    db()->insert(':user_group_custom', [
                        'user_group_id' => $group_id,
                        'module_id' => 'app_' . $this->id,
                        'name' => $key,
                        'default_value' => $value
                    ]);
                }
            }
        }
        //Add menu
        if (count($this->menu)) {
            $iId = db()->select('menu_id')
                ->from(':menu')
                ->where('module_id= "' . $this->alias . '" AND m_connection="main" AND url_value="' . $this->menu->url . '"')
                ->executeField();

            $aParams = [
                'm_connection' => 'main',
                'module_id' => $this->alias,
                'product_id' => 'phpfox',
                'allow_all' => true,
                'mobile_icon' => (isset($this->menu->icon) ? $this->menu->icon : null),
                'url_value' => $this->menu->url
            ];
            $bAddPhrase = true;
            $bIsUpdate = false;
            if (!empty($this->menu->phrase_var_name)) {
                $aParams['var_name'] = $this->menu->phrase_var_name;
                $bAddPhrase = false;
            }
            else {
                $aParams['text'] = [Phpfox::getService('language')->getDefaultLanguage() => $this->menu->name];
            }
            if ($iId) {
                $aParams['menu_id'] = $iId;
                $bIsUpdate = true;
            }

            Phpfox::getService('admincp.menu.process')->add($aParams, $bIsUpdate, (!$bIsUpdate), $bAddPhrase);
        }

        if (file_exists($this->path . 'installer.php')) {
            Installer::$method = 'onInstall';
            Installer::$basePath = $this->path;
            require_once($this->path . 'installer.php');
        }
        return true;
    }

    /**
     * Check this app is compatible with current phpFox version
     *
     * @return bool
     */
    public function isCompatible()
    {
        $aVersions = (new Phpfox_Installer())->getVersionList();
        $iStart = array_search($this->start_support_version, $aVersions);
        $iCurrent = array_search(Phpfox::getVersion(), $aVersions);
        $iEnd = array_search($this->end_support_version, $aVersions);
        if (($iStart <= $iCurrent) && ($iCurrent <= $iEnd)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Disable this app
     */
    public function disable()
    {
        db()->update(Phpfox::getT('apps'), [
            'is_active' => 0
        ], 'apps_id="' . $this->id . '"');
    }

    /**
     * Enable this app
     */
    public function enable()
    {
        db()->update(Phpfox::getT('apps'), [
            'is_active' => 1
        ], 'apps_id="' . $this->id . '"');
    }

    /**
     * Check this app is a core app or not
     *
     * @return bool
     */
    public function isCore()
    {
        return in_array($this->id, $this->_aOfficial);
    }

    /**
     * Check this app is active
     *
     * @return bool
     */
    public function isActive()
    {
        if (in_array($this->id, $this->_aCores)) {
            return true;
        }
        if (defined('PHPFOX_APP_INSTALLING') && PHPFOX_APP_INSTALLING) {
            return true;//installing app
        }
        $id  = $this->id;

        $iActive = get_from_cache(['app','is_active', $this->id], function() use($id){
            return db()->select('is_active')
                ->from(':apps')
                ->where('apps_id=\'' . $id . '\'')
                ->execute('getSlaveField');
        });
        return ($iActive) ? true : false;
    }

    public function saveSettings($settings)
    {
        foreach ($settings as $key => $value) {
            if (isset($this->settings->{$key}->requires) && $value == '1') {
                $set_value = (isset($settings[$this->settings->{$key}->requires]) ? $settings[$this->settings->{$key}->requires] : false);
                if (!$set_value) {
                    error(_p('"{{ name }}" requires setting "{{ requires }}".', [
                        'name' => $this->settings->{$key}->info,
                        'requires' => $this->settings->{$this->settings->{$key}->requires}->info
                    ]));
                }
            }
        }

        foreach ($settings as $key => $value) {
            //Check the setting is exist
            $iCnt = Phpfox::getLib('database')->select('COUNT(*)')
                ->from(':setting')
                ->where(['var_name' => $key])
                ->executeField();

            if ($iCnt) {
                //Update value of this setting
                $aUpdate = [
                    'value_actual' => $value,
                ];
                Phpfox::getLib('database')->update(':setting', $aUpdate, [
                    'var_name' => $key
                ]);
            } else {
                //Add new setting it not found on database. This case support for upgrade version < 4.5.0
                Phpfox::getLib('database')->insert(':setting', [
                    'module_id' => $this->alias,
                    'product_id' => $this->id,
                    'is_hidden' => 1,
                    'version_id' => $this->version,
                    'type_id' => 'string',
                    'var_name' => $key,
                    'phrase_var_name' => $key,
                    'value_actual' => $value,
                    'value_default' => $value
                ]);
            }
        }

        (new \Core\Cache())->del('app_settings');

        return true;
    }
    public function getErrorMessages()
    {
        return $this->_aErrorMessages;
    }
}