//=============================================================================
// RPG Maker MZ - Extra Log Window
//
// 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
 * @base TextResource
 * @orderAfter PluginCommonBase
 * @orderAfter DebugSwitch
 * @orderAfter TextResource
 *
 * @param logWaitVariable
 * @text ログ更新速度変数
 * @desc ログの更新速度を変数に紐づける場合に指定できます。
 *       指定しない場合でも速度はコマンドで変更可能です。
 * @type variable
 * @default 0
 *
 * @param windows
 * @text ウィンドウ
 * @desc 表示するウィンドウの設定
 * @type struct<WindowData>[]
 * @default []
 *
 * @command showWindow
 * @text ウィンドウの表示
 * @desc ウィンドウの表示をします
 *
 * @arg windowName
 * @text 対象ウィンドウの名前
 * @desc 表示対象のウィンドウを指定します。
 *       指定しない場合現在のシーンの全てのウィンドウを表示します
 * @type string
 * @default
 *
 * @command hideWindow
 * @text ウィンドウの消去
 * @desc ウィンドウの消去をします
 *
 * @arg windowName
 * @text 対象ウィンドウの名前
 * @desc 表示対象のウィンドウを指定します。
 *       指定しない場合現在のシーンの全てのウィンドウを消去します
 * @type string
 * @default
 *
 * @command addLog
 * @text ログの追加
 * @desc ログを追加します。対象指定が無い場合共有ログに追加されます。
 *
 * @arg windowName
 * @text 追加対象のウィンドウ名
 * @desc 追加対象のウィンドウを指定します。
 *       指定しない場合共有ログに追加されます。
 * @type string
 * @default
 *
 * @arg label
 * @text ログ内容(ラベル)
 * @desc 追加するログのリソースラベルを指定します。
 * @type string
 * @default
 *
 * @arg text
 * @text ログ内容
 * @desc 追加するログの内容を指定します。
 * @type string
 * @default
 *
 * @arg icon
 * @text アイコン
 * @desc 追加するログに表示するアイコンを指定します。
 * @dir img/pictures
 * @type file
 * @default
 *
 * @command clearLog
 * @text ログのクリア
 * @desc ログをクリアします。対象指定が無い場合共有ログがクリアされます。
 *
 * @arg windowName
 * @text 対象のウィンドウ名
 * @desc 対象のウィンドウを指定します。
 *       指定しない場合共有ログがクリアされます。
 * @type string
 * @default
 *
 * @command setLogWaitSpeed
 * @text ログ更新速度設定
 * @desc ログ表示時間を設定します。値を大きくすると早く消えます。
 *       変数を紐づけている場合変数も更新されます。
 *
 * @arg speed
 * @text ログ更新速度
 * @desc ログの更新速度です。値を大きくすると早く消えます。
 * @type number
 * @min 1
 * @max 60
 * @default 10
 *
 *
 * @help
 *
 * LogWindow
 * Version: 0.0.3
 *
 * シーンにログ用のウィンドウを追加します。
 *
 */

/*~struct~WindowData:
 *
 * @param name
 * @text ウィンドウの名前
 * @desc ウィンドウの名前です。個別に表示制御をしたいときの制御等に使用します。
 *       指定しない場合、[シーン名_連番]になります。
 *
 * @param targetScene
 * @text 対象シーン
 * @desc 追加対象のシーンです。オリジナルのシーンを対象にする場合はシーンクラス名を直接記入します。
 * @type select
 * @default Scene_Title
 * @option タイトル
 * @value Scene_Title
 * @option マップ
 * @value Scene_Map
 * @option ゲームオーバー
 * @value Scene_Gameover
 * @option バトル
 * @value Scene_Battle
 * @option メインメニュー
 * @value Scene_Menu
 * @option アイテム
 * @value Scene_Item
 * @option スキル
 * @value Scene_Skill
 * @option 装備
 * @value Scene_Equip
 * @option ステータス
 * @value Scene_Status
 * @option オプション
 * @value Scene_Options
 * @option セーブ
 * @value Scene_Save
 * @option ロード
 * @value Scene_Load
 * @option ゲーム終了
 * @value Scene_End
 * @option ショップ
 * @value Scene_Shop
 * @option 名前入力
 * @value Scene_Name
 * @option デバッグ
 * @value Scene_Debug
 *
 * @param logLineCount
 * @text 一度に表示できるログの数
 * @desc 一度に表示可能なログの最大行です。
 *       これ以上のログは自動的に削除されます。
 *       ウィンドウの高さはこれにより自動決定されます。
 * @type number
 * @default 20
 * @min 1
 *
 * @param windowWidth
 * @text ログウィンドウの幅
 * @type number
 * @default 300
 * @min 1
 *
 * @param horizontalPos
 * @text 横位置
 * @desc ウィンドウの横位置です。
 * @type select
 * @default left
 * @option 左
 * @value left
 * @option 中央
 * @value center
 * @option 右
 * @value right
 *
 * @param virticalPos
 * @text 縦位置
 * @desc ウィンドウの縦位置です。
 * @type select
 * @default top
 * @option 上
 * @value top
 * @option 中央
 * @value center
 * @option 下
 * @value bottom
 *
 * @param offsetX
 * @text 横位置オフセット
 * @desc ウィンドウの横位置オフセットです。
 *       ウィンドウ位置を微調整したいときのみ使用して下さい。
 * @type number
 * @default 0
 *
 * @param offsetY
 * @text 縦位置オフセット
 * @desc ウィンドウの縦位置オフセットです。
 *       ウィンドウ位置を微調整したいときのみ使用して下さい。
 * @type number
 * @default 0
 *
 * @param backgroundColor
 * @text ログの背景色
 * @desc 背景の色と透明度をRGBAのCSSカラーで指定します。
 *       デフォルトは透明です。
 * @type string
 * @default #00000000
 *
 * @param font
 * @text フォント
 * @desc 表示する文字のフォントです。
 *       指定しない場合システムの標準フォントを使用します。
 * @type file
 * @default
 * @dir fonts
 *
 * @param fontSize
 * @text 文字サイズ
 * @desc ウィンドウに表示する文字のサイズです。
 * @type number
 * @default 18
 * @min 8
 * @max 128
 *
 * @param fontStyle
 * @text 文字修飾
 * @desc 表示する文字の修飾です。
 * @type select
 * @option 通常
 * @value normal
 * @oprion 太字
 * @value bold
 * @option 斜体
 * @value italic
 *
 */
(() => {
	"use strict";

    const pluginName = "LogWindow";
    const script = document.currentScript;
    const param  = PluginManagerEx.createParameter(script);

    /********************************************************************************************************
     * 共有基盤
     ********************************************************************************************************/
    const log = (text) => {
        if (DebugSwitch.isEnable()) {
            console.log(text);
        }
    };

    //========================================================================
    // 各シーンでのローディング用
    //========================================================================
    const _Scene_Base_create    = Scene_Base.prototype.create;
    Scene_Base.prototype.create = function() {
        _Scene_Base_create.apply(this, arguments);
        const currentSceneName = PluginManagerEx.findClassName(this);
        if (!!$gameTemp && !!$gameTemp.logWindowController) {
            $gameTemp.logWindowController.buildWindows(currentSceneName);
        }
    };

    const _Scene_Base_start    = Scene_Base.prototype.start;
    Scene_Base.prototype.start = function() {
        _Scene_Base_start.apply(this, arguments);

        if (!!$gameTemp && !!$gameTemp.logWindowController) {
            const sceneName = PluginManagerEx.findClassName(this);
            Logger.d(`[log] add window: scene = ${sceneName}, windows = ${$gameTemp.logWindowController.getWindows(sceneName).length}`);
            $gameTemp.logWindowController.getWindows(sceneName).forEach(window => {
                Logger.d(`[log] add window: name = ${window.name()}`);
                this.addChild(window);
            });
        }
    };

    const _Scene_Base_prototype_stop = Scene_Base.prototype.stop;
    Scene_Base.prototype.stop = function() {
        if (!!$gameTemp && !!$gameTemp.logWindowController) {
            const sceneName = PluginManagerEx.findClassName(this);
            $gameTemp.logWindowController.getWindows(sceneName).forEach(window => {
                Logger.d(`[log] remove window: name = ${window.name()}`);
                this.removeChild(window);
            });
        }
        _Scene_Base_prototype_stop.apply(this, arguments);
    };

    //========================================================================
    // ログウィンドウ管理クラスの準備
    //========================================================================
    const _Game_Temp_initialize = Game_Temp.prototype.initialize;
    Game_Temp.prototype.initialize = function() {
        _Game_Temp_initialize.apply(this, arguments);
        this.logWindowController = new LogWindowController();
    };

    const _Game_System_onBeforeSave = Game_System.prototype.onBeforeSave;
    Game_System.prototype.onBeforeSave = function() {
        _Game_System_onBeforeSave.apply(this, arguments);
        this._saveLogData = $gameTemp.logWindowController.buildSaveData();
    };

    const _Game_System_onAfterLoad = Game_System.prototype.onAfterLoad;
    Game_System.prototype.onAfterLoad = function() {
        _Game_System_onAfterLoad.apply(this, arguments);
        $gameTemp.logWindowController.applySaveData(this._saveLogData);
    };

    //========================================================================
    // ログウィンドウ管理クラス
    //========================================================================
    class LogWindowController {
        constructor() {
            this._commonLogLines = [];
            this._windows = {};
            this._logWaitSpeed = 5;
            this.visible = {};
            this.windowItems = {};
            this._scenes = {};
        }

        buildSaveData() {
            return {
                commonLogLines: this._commonLogLines,
                logWaitSpeed: this._logWaitSpeed,
                visible: this.visible,
                windowItems: this.windowItems,
            };
        }

        applySaveData(data) {
            this._commonLogLines = data.commonLogLines;
            this._logWaitSpeed = data.logWaitSpeed;
            this.visible = data.visible;
            this.windowItems = data.windowItems;
        }

        setLogWaitSpeed(value) {
            if (param.logWaitVariable) {
                $gameVariables.setValue(param.logWaitVariable, value);
            }
            this._logWaitSpeed = value;
        }

        getWaitSpeed() {
            if (param.logWaitVariable) {
                this._logWaitSpeed = $gameVariables.value().clamp(1, 60);
            }
            const multi = (ConfigManager.getItemValue?.('logSpeed') ?? 100) / 100;
            return Math.trunc(this._logWaitSpeed * 60 * multi);
        }

        getWindows(sceneName) {
            if (!!this._scenes[sceneName]) {
                return this._scenes[sceneName];
            } else {
                return [];
            }
        }

        hasWindow(sceneName, windowName) {
            if (!this._scenes[sceneName]) {
                return this._scenes[sceneName].some(window => {
                    return window.name() === windowName;
                });
            }
            return false;
        }

        buildWindows(sceneName) {
            this._scenes[sceneName] = param.windows
                .map(p => p._parameter)
                .filter(p => p.targetScene === sceneName)
                .map(windowParam => {
                    return this._createWindow(windowParam);
                })
                .filter(window => !!window);
        }

        addWindow(sceneName, windowParam) {
            if (!!windowParam) {
                if (!this._scenes[sceneName]) {
                    this._scenes[sceneName] = this.buildWindows(sceneName);
                }
                this._scenes[sceneName].push(this._createWindow(windowParam));
            }
        }

        _createWindow(windowParam) {
            if (!!windowParam) {
                const count = Object.values(this._windows).filter(window => {
                    return windowParam.targetScene === window.scene;
                }).length;

                if (!windowParam.name || windowParam.name.length === 0) {
                    windowParam.name = windowParam.targetScene + '_' + count;
                }
                const window = new Window_ExtraLog(windowParam);
                this._windows[windowParam.name] = window;

                if (this.visible[windowParam.name] === false) {
                    this.visible[windowParam.name] = false;
                    window.hide();
                } else {
                    this.visible[windowParam.name] = true;
                    window.visible = true;
                }
                return window;
            }
            return null;
        }

        findWindow(windowName) {
            return this._windows[windowName];
        }

        getAllWindow() {
            return Object.values(this._windows);
        }

        addLog(label, icon, targetWindow) {
            this.addLogByText(TextResource.format(label), icon, targetWindow);
        }

        addLogByText(text, icon, targetWindow) {
            if (!!targetWindow && targetWindow.length > 0) {
                // ターゲット指定あり
                this._windows[targetWindow]?.addLog(text, icon);
            } else if (!!SceneManager._scene) {
                // 現シーンのすべてのウィンドウに
                this.getWindows(SceneManager._scene.constructor.name)
                        .forEach(window => window.addLog(text, icon));
            }
        }

        clearLog(targetWindow) {
            if (targetWindow && targetWindow.length > 0) {
                // ターゲット指定あり
                this._windows[targetWindow]?.clearLog();
            } else {
                // ターゲット指定無し
                this._windows.forEach(window => window.clearLog());
            }
        }

        getWindowParam(windowName) {
            return param.windows.map(p => p._parameter).find(p => p.name === windowName);
        }
    }

    const parseRgbaHexColorCode = (colorText) => {
        if (colorText.matchAll('^#[0-9A-F]{6}([0-9A-F]{2})?$')) {
            if (colorText.matchAll('^#[0-9A-F]{8}$')) {
                // RGBA構文
                const color = colorText.substring(0, 7);
                const opacity = parseInt(colorText.substring(7), 16)
                return {
                    color: color,
                    opacity: opacity,
                }
            } else {
                // RGB構文
                return {
                    color: colorText,
                    opacity: 255,
                };
            }
        }

    };

    //========================================================================
    // ログウィンドウクラス
    //========================================================================
    class Window_ExtraLog extends Window_Selectable {
        static LINE_HEIGHT = 44;

        static DesideWindowRect = (parameter) => {
            const lineHeight = Window_ExtraLog.LINE_HEIGHT;
            const width = parameter.windowWidth;
            const height = (lineHeight * (parameter.logLineCount + 1));
            let x = 0;

            switch (parameter.horizontalPos) {
                case 'left':
                    x = -8;
                    break;
                case 'center':
                    x = (Graphics.boxWidth - width) / 2;
                    break;
                case 'right':
                    x = (Graphics.boxWidth - width) + 8;
                    break;
            } // switch
            x += parameter.offsetX;

            let y = 0;
            switch (parameter.verticalPos) {
                case 'top':
                    y = -8;
                    break;
                case 'center':
                    y = (Graphics.boxHeight - height) / 2;
                    break;
                case 'bottom':
                    x = Graphics.boxHeight - height + 8;
                    break;
            } // switch
            y += parameter.offsetY;

            return new Rectangle(x, y, width, height);
        }

        //========================================================================
        // コンストラクタ
        //========================================================================
        constructor(parameter) {
            super(Window_ExtraLog.DesideWindowRect(parameter));

            const rect = Window_ExtraLog.DesideWindowRect(parameter);
            if (!$gameTemp.logWindowController.windowItems[parameter.name]) {
                $gameTemp.logWindowController.windowItems[parameter.name] = [];
            }
            this.opacity = 0;
            this.param = parameter;
            this._topIndex = 0;
            this._padding = 0;
            this._state = 'neutral';
            this._offset = 0;
            this._logQueue = [];
            this._items = [];
            this._frameCount = 0;
            this.contents = new Bitmap(this.contentsWidth(), this.contentsHeight());
            this.contentsBack = new Bitmap(rect.width, rect.height);

            this.zIndex = 50;
        }

        //========================================================================
        // 追加メソッド
        //========================================================================
        name() {
            return this.param.name;
        }

        scene() {
            return this.param.targetScene;
        }

        lineUpdateDuration() {
            return 15;
        }

        needsUpdate() {
            if (this._logQueue.length > 0) {
                return true;
            }
            return false;
        }

        addLog(text, icon) {
            log(`Log Added: text = ${text}`);
            const postLog = (iconPicture, line) => {
                this._logQueue.push({
                    text: line,
                    opacity: 255,
                    state: 'add',
                    frame: 0,
                    waitFrame: 0,
                    offsetX: 0,
                    offsetY: 0,
                    icon: iconPicture,
                });
            };

            if (!!icon && icon.length > 0) {
                ImageManager.loadPictureAsync(icon).then(picture => {
                    text.split('\n').reverse().forEach(line => {
                        postLog(picture, line);
                    })
                });
            } else {
                text.split('\n').reverse().forEach(line => {
                    postLog(null, line);
                })
            }
        }

        clearLog() {
            this._items.forEach(item => {
                item.state = 'remove';
            });
        }

        //========================================================================
        // 親クラスの処理変更・抑止したい系
        //========================================================================
        setCursorRect(x, y, width, height) {
            // nothing to do
        }

        processOk() {
            // nothing to do
        }

        maxItems() {
            return this.param.logLineCount + 1;
        }

        drawItemBackground(index) {
            const backgroundColor = this.param.backgroundColor;
            const bitmap = this.contentsBack;

            const item = this.item(index);
            const rect = this.itemRect(index);
            const colorCode = parseRgbaHexColorCode(backgroundColor);
            const itemOpacity = !!item ? item.opacity : 0;

            bitmap.paintOpacity = Math.min(colorCode.opacity, itemOpacity);
            bitmap.fillRect(rect.x, rect.y, rect.width, rect.height, colorCode.color);
            bitmap.paintOpacity = 255;
        }

        itemRect(index) {
            const rect = super.itemRect(index);
            const item = this.item(index);
            if (!!item) {
                rect.x += item.offsetX;
                rect.y += item.offsetY;
            }
            return rect;
        }

        drawItem(index) {
            const item = this.item(index);
            if (!!item) {
                const rect = this.itemRect(index);
                const textMergin = 22;
                const bitmap = new Bitmap(rect.width - textMergin, rect.height);
                bitmap.fontSize = this.param.fontSize;
                this.contents.paintOpacity = item.opacity;
                if (!!item.icon) {
                    const iw = item.icon.width;
                    const ih = item.icon.height;
                    const isize = rect.height - 4;
                    bitmap.blt(item.icon, 0, 0, iw, ih, 0, 2, isize, isize);
                    const textSx = isize + 4;
                    bitmap.drawText(item.text, textSx, 0, rect.width - textMergin - textSx, rect.height, 'left');
                } else {
                    bitmap.drawText(item.text, 0, 0, rect.width - textMergin, rect.height, 'left');
                }
                this.contents.blt(bitmap, 0, 0, rect.width, rect.height, rect.x + textMergin, rect.y - this._offset);
                bitmap.destroy();
            }
        }

        isCursorMovable() {
            return false;
        }

        rowSpacing() {
            return 6;
        }

        lineHeight() {
            return Window_ExtraLog.LINE_HEIGHT;
        }

        item(index) {
            if (index < 0 || index >= this._items.length) {
                return null;
            }
            return this._items[index];
        }

        itemPadding() {
            return 0;
        }

        updatePadding() {
            // nothing to do
        }

        //========================================================================
        // フレーム毎更新処理
        //========================================================================
        update() {
            // if (!this.needsUpdate()) {
            //     return;
            // }
            const lineHeight = this.lineHeight();
            const duration = this.lineUpdateDuration();

            const maxCount = this.param.logLineCount;
            const itemWidth = this.itemWidth();
            const eraceDuration = Math.max($gameTemp.logWindowController.getWaitSpeed(), 5);

            if (this._state === 'neutral') {
                if (this._logQueue.length > 0) {
                    const firstItem = this._logQueue.shift();
                    this._items.splice(0, 0, firstItem);
                    this._state = 'ensure';
                }
            }

            this._items.forEach((item, index) => {
                if (item.state === 'add') {
                    if (this._state === 'ensure') {
                        item.offsetX = ((itemWidth / duration) * item.frame) - itemWidth;
                        if (item.frame > duration) {
                            item.offsetX = 0;
                            item.state = 'active';
                            item.waitFrame = 0;

                            this._state = 'neutral';
                        } else {
                            item.frame++;
                        }
                    }
                } else if (item.state === 'active' || item.state === 'remove') {
                    if (this._state === 'ensure') {
                        if (item.offsetY >= 0) {
                            item.offsetY -= lineHeight;
                        }

                        item.offsetY += (lineHeight / duration);
                        if (item.frame > duration) {
                            item.offsetY = 0;
                        } else {
                            item.frame++;
                        }
                    } else if (this._state === 'neutral' && item.offsetY < 0) {
                        item.offsetY = 0;
                    }

                    if (item.state === 'active') {
                        item.waitFrame++;
                        if (item.waitFrame >= eraceDuration) {
                            item.state = 'remove';
                            item.waitFrame = 0;
                            item.frame = 0;
                        }

                        if (index >= maxCount) {
                            item.state = 'remove';
                            item.frame = 0;
                        }
                    } else {
                        item.offsetY += (lineHeight / duration);
                        item.opacity -= (256 / duration);
                        if (item.opacity < 0) {
                            item.opacity = 0;
                        }

                        if (item.frame > duration) {
                            item.opacity = 0;
                            item.state = 'removed';
                        } else {
                            item.frame++ ;
                        }
                    }

                }
            });
            this.refresh();
            this._items = this._items.filter(item => (item.state !== 'removed'));
        }

        paint() {
            if (this.contents) {
                this.contents.clear();
                this.contentsBack.clear();
                this.drawAllItems();
            }
        }
    }

    //========================================================================
    // プラグインコマンド
    //========================================================================

    // ウィンドウ名からウィンドウを検索
    const findWindow = (target, callback) => {
        if ($gameTemp.logWindowController) {
            if (target && target.length > 0) {
                const window = $gameTemp.logWindowController.findWindow(target);
                if (window) {
                    if (callback && typeof callback === 'function') {
                        callback(window);
                    }
                }
            } else {
                $gameTemp.logWindowController.getAllWindow().forEach(window => {
                    if (callback && typeof callback === 'function') {
                        callback(window);
                    }
                });
            }
        }
    };

    PluginManagerEx.registerCommand(script, 'showWindow', args => {
        findWindow(args.windowName, window => {
            window.show();
            $gameTemp.logWindowController.visible[args.windowName] = true;
        });
    });

    PluginManagerEx.registerCommand(script, 'hideWindow', args => {
        findWindow(args.windowName, window => {
            window.hide();
            $gameTemp.logWindowController.visible[args.windowName] = false;
        });
    });

    PluginManagerEx.registerCommand(script, 'addLog', args => {
        if ($gameTemp.logWindowController) {
            if (!!args.label && args.label !== '') {
                $gameTemp.logWindowController.addLog(args.label, args.icon, args.windowName);
            } else {
                $gameTemp.logWindowController.addLogByText(args.text, args.icon, args.windowName);
            }
        }
    });

    PluginManagerEx.registerCommand(script, 'clearLog', args => {
        findWindow(args.windowName, window => {
            $gameTemp.logWindowController.clearLog(window);
        });
    });

    PluginManagerEx.registerCommand(script, 'setLogWaitSpeed', args => {
        if ($gameTemp.logWindowController) {
            $gameTemp.logWindowController.setLogWaitSpeed(args.speed);
        }
    });

})();