<?php

namespace think\addons;

use PhpZip\Exception\ZipException;
use PhpZip\ZipFile;
use think\Db;
use think\Exception;
use think\facade\Env;
use think\facade\Request;
use think\facade\Config;
use think\Loader;

//插件目录是否存在，不存在则创建
$addons_path=Env::get('addons_path');
if (!is_dir($addons_path)) {
    mkdir($addons_path, 0777, true);
}

/**
 * 插件基类模型
 */
class Service
{

    /**
     * 插件配置文件
     * @var string
     */
    static $addonsConfigFile = __ROOT__ . 'config/addons.php';
    /**
     * 插件目录
     * @var string
     */
    static $addonsPath = __ROOT__ . 'addons/';
    /**
     * 插件主题资源目录
     * @var string
     */
    static $addonsThemePath = 'theme/addons/';


    /**
     * 离线安装
     * @param string $file 插件压缩包
     */
    public static function local($file)
    {

        $addonsTempDir = self::getAddonsBackupDir();
        if (!$file || !$file instanceof \think\File) {
            throw new Exception('No file upload or server upload limit exceeded');
        }
        $uploadFile = $file->rule('uniqid')->validate(['size' => 102400000, 'ext' => 'zip'])->move($addonsTempDir);
        if (!$uploadFile) {
            // 上传失败获取错误信息
            throw new Exception(__($file->getError()));
        }
        $tmpFile = $addonsTempDir . $uploadFile->getSaveName();

        //临时的文件夹名称
        $tempDir = pathinfo($tmpFile)['filename'];
        //临时的解压路径
        $tempDirPath = $addonsTempDir . $tempDir . '/';
        try {
            // 打开插件压缩包,解压
            $zip = new \ZipArchive();
            if ($zip->open($tmpFile) === TRUE) {
                $zip->extractTo($tempDirPath);//提取全部文件
                $zip->close();
            } else {
                throw new Exception('插件解压失败');
            }
            //插件目录中的插件配置
            $config = parse_ini_file($tempDirPath . 'info.ini', true, INI_SCANNER_TYPED);

            // 判断插件标识
            $name = isset($config['name']) ? $config['name'] : '';
            if (!$name) {
                throw new Exception('Addon info file data incorrect');
            }

            // 判断插件标识是否有效
            if (!preg_match("/^[a-zA-Z0-9_]+$/", $name)) {
                throw new Exception('Addon name incorrect');
            }

            // 判断新插件是否存在
            $newAddonDir = self::getAddonDir($name);
            if (is_dir($newAddonDir)) {
                throw new Exception('Addon already exists');
            }
            if (is_dir($newAddonDir)) {
                Service::rmdirs($newAddonDir);
            }
            //移动插件到插件目录中，并重命名
            rename($tempDirPath, $newAddonDir);

            //将资源文件移动到主题目录下
            $addonThemeDir = self::getAddonThemeDir($name);
            if (!is_dir(self::$addonsThemePath)) {
                mkdir(self::$addonsThemePath);
            }
            if (is_dir($addonThemeDir)) {
                Service::rmdirs($addonThemeDir);
            }
            if (is_dir($newAddonDir . 'theme'))
                rename($newAddonDir . 'theme', $addonThemeDir);

            Db::startTrans();
            try {
                //执行插件的安装方法
                $class = get_addon_class($name);
                if (class_exists($class)) {
                    $addon = new $class();
                    $addon->install();
                }

                //导入SQL
                self::importsql($name);
                //启用插件
                self::enable($name);
                Db::commit();
            } catch (\Exception $e) {
                Db::rollback();
                @Service::rmdirs($newAddonDir);
                //移除插件主题资源目录
                @Service::rmdirs($addonThemeDir);
                throw new Exception($e->getMessage());
            }

        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        } finally {
            unset($uploadFile);
            @unlink($tmpFile);
            // 移除插件临时目录
            @Service::rmdirs($tempDirPath);
        }
        return true;
    }

    /**
     * 导入SQL
     *
     * @param string $name 插件名称
     * @return  boolean
     */
    public static function importsql($name)
    {
        $sqlFile = self::getAddonDir($name) . 'install.sql';
        if (is_file($sqlFile)) {
            $lines = file($sqlFile);
            $templine = '';
            foreach ($lines as $line) {
                $line = trim($line);
                if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') {
                    continue;
                }
                if (substr($line, 0, 2) == '*/') {
                    $templine = '';
                    continue;
                }

                $templine .= $line;
                if (substr(trim($line), -1, 1) == ';') {
                    $templine = str_ireplace('__PREFIX__', config('database.prefix'), $templine);
                    $templine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $templine);
                    try {
                        Db::query($templine);
                    } catch (\PDOException $e) {
                        //$e->getMessage();
                    }
                    $templine = '';
                }
            }
        }
        return true;
    }

    /**
     * 插件生成的表名，重命名为以当前时间戳结尾的表名
     * @param string $name 插件名称
     * @return  boolean
     */
    public static function renamesql($name)
    {
        $sqlFile = self::getAddonDir($name) . 'install.sql';
        if (is_file($sqlFile)) {
            $sqlContent = file_get_contents($sqlFile);
            if (preg_match_all('/CREATE TABLE `{0,1}(\w+)`{0,1}/i', $sqlContent, $pat_array)) {
                $ctime = time();
                foreach ($pat_array[1] as $item) {
                    $tname = str_ireplace('__PREFIX__', config('database.prefix'), $item);
                    try {
                        Db::query('ALTER TABLE ' . $tname . ' RENAME TO ' . $tname . '_' . $ctime);
                    }
                    catch (Exception $e){ }
                }
            }
        }
        return true;
    }


    /**
     * 卸载插件
     *
     * @param string $name
     * @return  boolean
     * @throws  Exception
     */
    public static function uninstall($name)
    {
        if (!$name || !is_dir(self::$addonsPath . $name)) {
            throw new Exception('Addon not exists');
        }
        //禁用插件
        self::disable($name);

        // 移除插件全局资源文件
        Service::rmdirs('theme/addons/' . $name);

        // 执行卸载脚本
        try {
            $class = get_addon_class($name);
            if (class_exists($class)) {
                $addon = new $class();
                $addon->uninstall();
            }
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
        //重命名插件表
        self::renamesql($name);
        //删除菜单
        $adminMenuList = self::getAdminMenu($name);
        if ($adminMenuList) {
            self::deleteAdminMenu($adminMenuList);
            cache("ADMIN_AUTH_MENU",null);
        }
        // 移除插件目录
        Service::rmdirs(self::$addonsPath . $name);

        return true;
    }


    /**
     * 启用
     * @param string $name 插件名称
     * @return  boolean
     */
    public static function enable($name)
    {
        if (!$name || !is_dir(self::$addonsPath . $name)) {
            throw new Exception('Addon not exists');
        }

        //执行启用脚本
        try {
            $class = get_addon_class($name);
            if (class_exists($class)) {
                $addon = new $class();
                if (method_exists($class, "enable")) {
                    $addon->enable();
                }
            } else {
                throw new Exception('开启失败，插件无法访问！');
            }
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
        //读取钩子方法
        //读取插件父类方法
        $baseMethods = get_class_methods("\\think\\Addons");
        $addonClass = "\\addons\\" . $name . "\\" . Loader::parseName($name, 1);
        // 读取出所有公共方法
        $methods = (array)get_class_methods($addonClass);
        // 跟插件基类方法做比对，得到差异结果
        $hookList = array_diff($methods, $baseMethods);
        //加载插件配置文件
        $config = self::loadConfig();

        // 循环将钩子方法写入配置中
        foreach ($hookList as $hook) {
            if (!isset($config['hooks'][$hook])) {
                $config['hooks'][$hook] = [];
            }
            // 兼容手动配置项
            if (is_string($config['hooks'][$hook])) {
                $config['hooks'][$hook] = explode(',', $config['hooks'][$hook]);
            }
            if (!in_array($name, $config['hooks'][$hook])) {
                $config['hooks'][$hook][] = $addonClass;
            }
        }
        self::saveConfig($config);

        //插入菜单
        $adminMenuList = self::getAdminMenu($name);
        if ($adminMenuList) {
            //从后台菜单中找到插件管理
            $adminMenu = Db::name('admin_menu')->where('url', 'Addons/index')->find();
            if ($adminMenu)
            {
                self::insertAdminMenu($adminMenuList, $adminMenu['pid']);
                cache("ADMIN_AUTH_MENU",null);
            }
        }

        if ($addon) {
            $addon->saveInfo('status', 1);
        }
        return true;
    }

    /**
     * 获得插件的菜单配置
     */
    protected static function getAdminMenu($name)
    {
        $menuFile = self::getAddonDir($name) . 'menu.json';
        if (is_file($menuFile)) {
            $data = file_get_contents($menuFile);
            $data = json_decode($data, true);
            return $data;
        }
        return false;
    }

    /**
     * 递归将一个树状菜单插入到后台菜单表
     * @param $menuList
     * @param $pid
     */
    protected static function insertAdminMenu($menuList, $pid)
    {
        $ctime = time();
        foreach ($menuList as $item) {
            $data = [];
            $data['title'] = $item['title'];
            $data['url'] = $item['url'];
            if (isset($item['sort'])) {
                $data['sort'] = $item['sort'];
            }
            $data['create_time'] = $ctime;
            $data['pid'] = $pid;
            $amid = Db::name('admin_menu')->insertGetId($data);
            if (isset($item['child'])) {
                self::insertAdminMenu($item['child'], $amid);
            }
        }
    }

    /**
     * 递归删除后台菜单表
     * @param $menuList
     * @param $pid
     */
    protected static function deleteAdminMenu($menuList)
    {
        foreach ($menuList as $item) {
            Db::name('admin_menu')->where('url',$item['url'])->delete();
            if (isset($item['child'])) {
                self::deleteAdminMenu($item['child']);
            }
        }
    }

    /**
     * 递归将一个树状菜单从后台菜单表删除
     * @param $menuList
     */
    protected static function delAdminMenu($menuList)
    {
        foreach ($menuList as $item) {
            Db::name('admin_menu')->where('url', $item['url'])->delete();
            if (isset($item['child'])) {
                self::delAdminMenu($item['child']);
            }
        }
    }

    /**
     * 禁用
     *
     * @param string $name 插件名称
     * @return  boolean
     * @throws  Exception
     */
    public static function disable($name)
    {
        if (!$name || !is_dir(self::$addonsPath . $name)) {
            throw new Exception('Addon not exists');
        }

        // 执行禁用脚本
        try {
            $class = get_addon_class($name);
            if (class_exists($class)) {
                $addon = new $class();

                if (method_exists($class, "disable")) {
                    $addon->disable();
                }
            } else {
                throw new Exception('禁用失败，插件无法访问！');
            }
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
        $addonClass = "\\addons\\" . $name . "\\" . Loader::parseName($name, 1);
        // 读取出所有公共方法
        $methods = (array)get_class_methods($addonClass);

        //加载插件配置文件
        $config = self::loadConfig();
        if (isset($config['hooks'])) {

            // 循环将钩子方法写入配置中
            foreach ($methods as $item) {
                if (!isset($config['hooks'][$item])) {
                    continue;
                }
                // 兼容手动配置项
                if (is_string($config['hooks'][$item])) {
                    $config['hooks'][$item] = explode(',', $config['hooks'][$item]);
                }
                $config['hooks'][$item] = array_merge(array_diff($config['hooks'][$item], array($addonClass)));
                if ($config['hooks'][$item] == []) {
                    unset($config['hooks'][$item]);
                }
            }
            self::saveConfig($config);
        }

        //删除后台菜单
        $menuList = self::getAdminMenu($name);
        if ($menuList)
            self::delAdminMenu($menuList);

        if ($addon) {
            $addon->saveInfo('status', 0);
        }
        return true;
    }

    /**
     * 获取指定插件的目录
     */
    public static function getAddonDir($name)
    {
        $dir = self::$addonsPath . $name . '/';
        return $dir;
    }

    /**
     * 加载插件配置
     */
    static function loadConfig()
    {
        $config = [];
        $addonsConfigFile = self::$addonsConfigFile;
        if (is_file($addonsConfigFile)) {
            $config = include $addonsConfigFile;
        }
        return $config;
    }

    /**
     * 保存插件配置
     */
    static function saveConfig($array)
    {
        $file = self::$addonsConfigFile;
        if ($handle = fopen($file, 'w')) {
            fwrite($handle, "<?php\n\n" . "return " . var_export($array, TRUE) . ";\n");
            fclose($handle);
        } else {
            throw new Exception("文件没有写入权限");
        }
        return true;
    }

    /**
     * 获取指定插件的资源目录
     */
    public static function getAddonThemeDir($name)
    {
        $dir = self::$addonsThemePath . $name;
        return $dir;
    }

    /**
     * 获取插件备份目录
     */
    public static function getAddonsBackupDir()
    {
        $dir = __ROOT__ . 'runtime/addons/';
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        return $dir;
    }

    /**
     * 匹配配置文件中info信息
     * @param ZipFile $zip
     * @return array|false
     * @throws Exception
     */
    protected static function getInfoIni($zip)
    {
        $config = [];
        // 读取插件信息
        try {
            $info = $zip->getEntryContents('info.ini');
            $config = parse_ini_string($info);
        } catch (ZipException $e) {
            throw new Exception('Unable to extract the file');
        }
        return $config;
    }

    private static function rmdirs($dirname, $withself = true)
    {
        if (!is_dir($dirname)) {
            return false;
        }
        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dirname, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($files as $fileinfo) {
            $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
            $todo($fileinfo->getRealPath());
        }
        if ($withself) {
            @rmdir($dirname);
        }
        return true;
    }

}