//=============================================================================
// RPG Maker MZ - PluginUtils
//
// Copyright 2024 Panzer-IV. All rights reserved.
// This source code or any portion thereof must not be
// reproduced or used without licensed in any manner whatsoever.
//=============================================================================
/*:
 * @target MZ
 * @plugindesc 共通処理用ユーティリティ
 * @author 四号戦車
 * @base PluginCommonBase
 * @orderAfter PluginCommonBase
 *
 * @help PluginUtils.js
 *
 * Version: 0.0.1
 *
 * 各プラグインの共通処理用ユーティリティプラグイン
 * 単体では意味がありません
 *
 */
(() => {
	"use strict";

    //========================================================================
    // モジュールインポート
    //========================================================================
    const fs = require('fs');
    const exec = require('child_process').exec;

    //========================================================================
    // プラグイン定義
    //========================================================================
    const pluginName = "PluginUtils";
    const script = document.currentScript;
    const param  = PluginManagerEx.createParameter(script);

    //========================================================================
    // 定数定義
    //========================================================================
    const CODE_PLUGIN_COMMAND = 357;

    //========================================================================
    // コアスクリプト変更部 (Game_Map)
    //========================================================================
    Game_Map.prototype.setupDynamicCommon = function(id) {
        const event = $dataCommonEvents[id];
        if (event) {
            return this.setupDynamicInterpreter(event.list);
        }
        return null;
    };

    Game_Map.prototype.setupDynamicInterpreter = function(list) {
        const interpreter = new Game_Interpreter();
        interpreter.setup(list, 0);
        this.initDynamicEvents();
        this._dynamicEvents.push(interpreter);
        this._dynamicEvents = this._dynamicEvents.filter(
            interpreter => interpreter.isRunning());

        return interpreter;
    };

    //========================================================================
    // ファンクション群
    //========================================================================
    const _PluginManager_callCommand = PluginManager.callCommand;
    PluginManager.callCommand = function(self, pluginName, commandName, args) {
        PluginManagerEx.generateSelfSwitchKey(self.eventId());
        const key = pluginName + ":" + commandName;
        const func = this._commands[key];
        if (typeof func === "function") {
            return func.bind(self)(args, self);
        }
        return null;
    };

    const _PluginManagerEx_registerCommand = PluginManagerEx.registerCommand;
    PluginManagerEx.registerCommand = function(currentScript, commandName, funcName) {
        const pluginName = this.findPluginName(currentScript);
        const key = pluginName + ":" + commandName;
        const func = typeof funcName === 'function' ? funcName : Game_Interpreter.prototype[funcName];
        if (!func) {
            throw new Error(`Not found function Game_Interpreter : ${funcName}`)
        }

        PluginManager.registerCommand(pluginName, commandName, function(args, interpreter) {
            const parsedArgs = PluginManagerEx.createCommandArgs(args, key);
            parsedArgs.interpreter = interpreter;
            return func.call(this, parsedArgs);
        });
    };

    //========================================================================
    // ファンクション群
    //========================================================================
    const fireCommonEvent = (eventId) => {
        if (eventId > 0) {
            return $gameMap.setupDynamicCommon(eventId);
        } else {
            Logger.d('fireCommonEvent: event is not specified');
        }
        return null;
    };

    const waitCommonEvent = (eventId, interpreter) => {
        if (eventId > 0 && eventId < $dataCommonEvents.length) {
            const commonEvent = $dataCommonEvents[eventId];
            if (!!commonEvent && !!interpreter) {
                const eventId = interpreter.isOnCurrentMap() ? interpreter._eventId : 0;
                interpreter.setupChild(commonEvent.list, eventId);
                return interpreter._childInterpreter;
            } else {
                Logger.d(`waitCommonEvent: event or interpreter is not spec, id = ${eventId}`);
                return null;
            }
        } else {
            Logger.d(`waitCommonEvent: event is not specified, id = ${eventId}`);
            return null;
        }
    };

    const buildPath = (basePath, fileName) => {
        const pathBase = `${basePath}/${fileName}`;
        if (basePath.startsWith('img') && Utils.hasEncryptedImages()) {
            return `${pathBase}_`;
        } else if (basePath.startsWith('audio') && Utils.hasEncryptedAudio()) {
            return `${pathBase}_`;
        }
        return pathBase;
    };

    const fileExists = (basePath, fileName) => {
        const path = buildPath(basePath, fileName);
        try {
            fs.accessSync(path, fs.constants.R_OK);
            Logger.d(`Check OK: lang = ${path}`);
            return true;
        } catch (err) {
            Logger.w(`cannot access: path = ${path}, err = ${err}`);
            return false;
        }
    };

    /**
     * ツクールのイベントリストに指定したプラグインが無いかを判定する関数
     *
     * イベントリストのfilter処理等に利用できます。
     * 下記のようにご利用できます。
     *
     * $gameMap.event(eventId).list().filter(PluginUtils.checkPluginCommand('plugin name', 'command name'));
     *
     * @argument command RPG.EventCommand
     * @returns true if found, false if or not.
     */
    const checkPluginCommands = (plugin, ...names) => (command) => {
        if (command.code !== CODE_PLUGIN_COMMAND) {
            return false;
        }

        const params = command.parameters;
        const pName = Utils.extractFileName(params[0]);
        if (pName !== plugin) {
            return false;
        }

        const commandName = params[1];
        return names.some(name => name === commandName);
    };

    const injectPluginCommand = (list, position, plugin, commandName, argument) => {
        const format = {
            "code": CODE_PLUGIN_COMMAND,
            "indent": 0,
            "parameters": [
                plugin,
                commandName,
                commandName,
                (!!argument) ? argument : {},
            ]
        };
        list.splice(position, 0, format);
    };

    const transferMap = (mapId, x, y, d, fade, interpreter) => {
        const direction = !!d ? d : 2;
        const fadeType = !!fade ? fade : 0;
        $gamePlayer.reserveTransfer(mapId, x, y, direction, fadeType);
        if (!!interpreter) {
            interpreter.setWaitMode("transfer");
        }
    }

    const openUrl = (url) => {
        if (Utils.isNwjs()) {
            switch (process.platform) {
                case 'win32':
                    exec(`rundll32.exe url.dll,FileProtocolHandler "${url}"`);
                    break;
                default:
                    exec(`open "${url}"`);
                    break;
            }
        } else {
            window.open(url);
        }
    };

    const generateUUID = () => {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
          var r = Math.random() * 16 | 0,
              v = c === 'x' ? r : (r & 0x3 | 0x8);
          return v.toString(16);
        });
    };

    const getClassByName = (classNameText) => {
        try {
            return Function('return (' + classNameText + ')')();
        } catch (err) {
            Logger.w(`getClassByName: class not found, err = ${err}`);
            return undefined;
        }
    };

    const percent = (max, percentage) => {
        return (Number(percentage).clamp(0, 100) / 100) * max;
    }

    const percentOver = (max, percentage) => {
        return (Number(percentage) / 100) * max;
    }

    const parseRgbaHexToCss = (colorText, alpha) => {
        if (colorText.matchAll('^#[0-9A-F]{6}([0-9A-F]{2})?$')) {
            const r = parseInt(colorText.substring(1, 3), 16);
            const g = parseInt(colorText.substring(3, 5), 16);
            const b = parseInt(colorText.substring(5, 7), 16);

            if (colorText.length > 7) {
                // RGBA構文
                const a = parseInt(colorText.substring(7), 16);
                // Logger.d(`parseRgbaHexToCss: rgba r = ${r}, g = ${g}, b = ${b}, a = ${a}`);
                return `rgba(${r}, ${g}, ${b}, ${a})`;
            } else if (!!alpha || alpha === 0) {
                // RGB構文 + Alpha
                // Logger.d(`parseRgbaHexToCss: rgb+a r = ${r}, g = ${g}, b = ${b}, a = ${Number(alpha)}}`);
                return `rgba(${r}, ${g}, ${b}, ${percent(255, alpha)})`;
            } else {
                // RGB構文
                // Logger.d(`parseRgbaHexToCss: rgb r = ${r}, g = ${g}, b = ${b}`);
                return `rgb(${r}, ${g}, ${b})`;
            }
        }
    };

    const parseRgbaHexToInt = (colorText) => {
        if (colorText.matchAll('^#[0-9A-F]{6}$')) {
            return Number.parseInt(colorText.substring(1), 16);
        }
        return 0x000000;
    };

    const recursiveTrimParameter = (object) => {
        if (Array.isArray(object)) {
            return object.map(obj => recursiveTrimParameter(obj));
        } else if (object !== null && typeof object === 'object') {
            if (object.hasOwnProperty('_parameter')) {
                return recursiveTrimParameter(object._parameter);
            } else {
                const entry = Object.keys(object).map(key => {
                    return [key, recursiveTrimParameter(object[key])];
                });
                return Object.fromEntries(entry);
            }
        } else {
            return object;
        }
    }

    const reparseParameter = (script) => {
        return recursiveTrimParameter(PluginManagerEx.createParameter(script));
    };

    const checkHankaku = (ch) => {
        return (ch >= 0x0 && ch <= 0x7f) || ch.match(/\(|\)/g);
    }

    const trimText = (text, max) => {
        let count = 0;
        let chCount = 0;
        let measured = 0;
        for (const ch of text) {
            if (ch.match(/\r?\n/g)) {
                // 改行コードは無視判定
                chCount++;
                continue;
            }

            if (checkHankaku(ch)) {
                count += 0.5;
            } else {
                count++;
            }

            chCount++;
            if (count >= max && measured === 0) {
                measured = chCount;
            }
        }

        if (measured === 0) {
            // 最後まで計測して文字数以下ならそのまま返す
            return text;
        } else {
            return text.substring(0, measured) + '…';
        }
    };

    const splitText = (text, lineMax, ignoreHankaku) => {
        let count = 0;
        let chCount = 0;
        let lastStart = 0;
        const lines = [];
        for (const ch of text) {
            if (ch.match(/\r?\n/g)) {
                const sub = text.substring(lastStart, chCount);
                Logger.d(`splitText: LF sub = ${sub}, start = ${lastStart}, count = ${chCount}`);
                lines.push(sub);
                chCount++;
                lastStart = chCount;
                count = 0;
                continue;
            }

            if (checkHankaku(ch) && !ignoreHankaku) {
                count += 0.5;
            } else {
                count++;
            }

            if (count >= lineMax) {
                const sub = text.substring(lastStart, chCount);
                Logger.d(`splitText: sub = ${sub}, start = ${lastStart}, count = ${count}`);
                lines.push(sub);
                count = 0;
                lastStart = chCount;
            }
            chCount++;
        }

        if (lastStart < text.length) {
            const sub = text.substring(lastStart, text.length);
            Logger.d(`splitText: last sub = ${sub}, start = ${lastStart}, count = ${text.length}`);
            lines.push(sub);
        }
        return lines;
    }

    const loadJsonAsync = (dir, subDir, filename) => {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            const url = `${dir}/${subDir}/${filename}.json`;
            xhr.open("GET", url);
            xhr.overrideMimeType("application/json");

            const onError = () => {
                Logger.e(`load resource: ${xhr.status} error file = ${filename} in lang = ${subDir}`);
                reject(subDir, filename, xhr.status);
            };

            xhr.onload = () => {
                if (xhr.status < 400) {
                    try {
                        const text = xhr.responseText;
                        const json = JSON.parse(text.includes('\x1A') ? text.replace("\x1A", "") : text);
                        resolve({
                            subDir: subDir,
                            filename: filename,
                            json: json,
                        });
                    } catch(err) {
                        onError();
                    }
                } else {
                    onError();
                }
            };
            xhr.onerror = onError;

            Logger.d(`load resources: lang = ${subDir}, file = ${filename}`);

            try {
                xhr.send();
            } catch (err) {
                Logger.e(`loadFileAsync: error = ${err}`);
            }
        });
    };

    const getVariables = (variable) => {
        if (variable > 0) {
            return $gameVariables.value(variable);
        }
        return null;
    };

    const getSwitch = (switchId, defaultValue) => {
        if (switchId > 0) {
            return $gameSwitches.value(switchId);
        }
        return defaultValue === true;
    };

    const alphabetToZenkaku = (str) => {
        // 半角英数字を全角に変換
        return String(str).replace(/[A-Za-z0-9]/g, (s) => {
            return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
        });
    };

    const alphabetToHankaku = (str) => {
        // 全角英数字を半角に変換
        return String(str).replace(/[Ａ-Ｚａ-ｚ０-９]/g, (s) => {
            return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
        });
    };

    window.PluginUtils = {
        fireCommonEvent: fireCommonEvent,
        waitCommonEvent: waitCommonEvent,
        fileExists: fileExists,
        checkPluginCommands: checkPluginCommands,
        injectPluginCommand: injectPluginCommand,
        transferMap: transferMap,
        openUrl: openUrl,
        generateUUID: generateUUID,
        getClassByName: getClassByName,
        percent: percent,
        percentOver: percentOver,
        parseRgbaHexToCss: parseRgbaHexToCss,
        parseRgbaHexToInt: parseRgbaHexToInt,
        reparseParameter: reparseParameter,
        trimText: trimText,
        splitText: splitText,
        loadJsonAsync: loadJsonAsync,
        getVariables: getVariables,
        getSwitch: getSwitch,
        alphabetToZenkaku: alphabetToZenkaku,
        alphabetToHankaku: alphabetToHankaku,
    };
})();
