//=============================================================================
// RPG Maker MZ - IM_BattleStateWindow
//
// Copyright 2025 I'm moralist/min-pub.com 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 DebugSwitch
 * @base PluginUtils
 * @base MPP_StateLevel
 * @base IM_MPP_StateLevelWindowPatch
 * @orderAfter DebugSwitch
 * @orderAfter PluginUtils
 * @orderAfter MPP_StateLevel
 * @orderAfter IM_MPP_StateLevelWindowPatch
 * 
 * @param targetActor
 * @text 対象アクター
 * @desc 表示対象のプレイヤー
 * @type actor
 * @default 0
 *
 * @param logLineCount
 * @text 一度に表示できるログの数
 * @desc 一度に表示可能なログの最大行です。
 *       これ以上のログは自動的に削除されます。
 *       ウィンドウの高さはこれにより自動決定されます。
 * @type number
 * @default 20
 * @min 1
 *
 * @param windowWidth
 * @text ログウィンドウの幅
 * @type number
 * @default 300
 * @min 1
 *
 * @param horizontalPos
 * @text 横位置
 * @desc ウィンドウの横位置です。
 * @type number
 * @default 0
 *
 * @param virticalPos
 * @text 縦位置
 * @desc ウィンドウの縦位置です。
 * @type number
 * @default 0
 *
 * @param comment
 * @text 備考欄
 * @desc プラグイン側では使用しません。
 *       メモ欄としてお使いください。
 * @type multiline_string
 * @default
 *
 * @requiredAssets img/system/battle_state/state_background
 * @requiredAssets img/system/battle_state/state_description
 * 
 * @help IM_BattleStateWindow.js
 *
 * Version: 0.0.1
 * 
 * 戦闘中のステート表示プラグイン
 *
 */
(() => {
	"use strict";

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

    //========================================================================
    // 定数定義
    //========================================================================
    const RES_TEXT_STATE_HELP = "0900-068-n068-0";

    //========================================================================
    // クラス定義 (GradientPoint)
    //========================================================================
    class Window_BattleState extends Window_Selectable {
        static LINE_HEIGHT = 40;
        static ROW_SPACING = 4;

        static DesideWindowRect = () => {
            const lineHeight = Window_BattleState.LINE_HEIGHT + (Window_BattleState.ROW_SPACING / 2);
            const width = param.windowWidth;
            const height = ((Window_BattleState.LINE_HEIGHT + Window_BattleState.ROW_SPACING) * (param.logLineCount + 1));
            return new Rectangle(param.horizontalPos, param.virticalPos, width, height);
        }

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

            const rect = Window_BattleState.DesideWindowRect();

            this.opacity = 0;
            this._topIndex = 0;
            this._padding = 0;
            this._commandState = 'neutral';
            this._updateIndex = 0;
            this._requestQueue = [];
            this._items = [];
            this._animationFrame = 0;
            this.contents = new Bitmap(this.contentsWidth(), this.contentsHeight());
            this.contentsBack = new Bitmap(rect.width, rect.height);

            ImageManager.loadSystemAsync('battle_state/state_background').then(bitmap => {
                this._itemBackgroundImage = bitmap;
            });

            this.zIndex = 50;
        }

        //========================================================================
        // 追加メソッド
        //========================================================================
        lineUpdateDuration() {
            return 8;
        }

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

        addState(stateId) {
            Logger.d(`Log Added: state Id = ${stateId}`);
            const state = $dataStates[stateId];
            if (!!state) {
                if (state.iconIndex === 0) {
                    // アイコン指定のないステートは表示対象としない
                    return;
                }

                const actor = $gameActors.actor(param.targetActor);
                const level = actor.stateLevel(stateId);

                // 既にスタックされているステートを捜索
                const found = this._items.findIndex(item => {
                    return (item.stateId === stateId)
                        && (item.state !== 'remove')
                        && (item.state !== 'removed');
                });

                if (found >= 0) {
                    const foundState = this._items[found];

                    // リクエストの中に削除要求きてないか探しておく
                    const removeRequested = this._requestQueue.find(request => {
                        if (request.command !== 'remove') {
                            return false;
                        }

                        const requestId = request.target;
                        if (requestId !== stateId) {
                            return false;
                        }

                        return true;
                    });

                    if (foundState.stateLevel === level) {
                        // レベル変動がないので追加処理そのものをしないが、
                        // 既にリクエストキューの中に削除要求が来ている場合は追加しないと
                        // スタックされないので足す
                        if (!removeRequested) {
                            // この場合純然に追加がかぶってるので無視
                            return;
                        }
                    }

                    if (!removeRequested) {
                        // 既存のStateを変更するためまず古いステートを削除
                        this._requestQueue.push({
                            command: 'remove',
                            target: stateId,
                            item: null,
                        });
                    }
                }

                // タイミングによってまだリクエスト中の追加が重なる可能性があるので一応捜索
                const requested = this._requestQueue.find(request => {
                    if (request.command !== 'add') {
                        return false;
                    }

                    const item = request.item;
                    if (item.stateId !== stateId) {
                        return false;
                    }

                    return true;
                });

                if (!!requested) {
                    if (requested.item.stateLevel === level) {
                        // 同じリクエストが既にされているので後段のリクエストは無視
                        return;
                    } else {
                        // 後からリクエストが更新されてるので先のリクエストを取り消し
                        this._requestQueue = this._requestQueue.filter(request => request !== requested);
                    }
                }

                // 追加
                // 「×レベル」表示は2個以上重ねがけされてるときだけ
                const text = (level > 1) ? `${state.name} ×${PluginUtils.alphabetToZenkaku(level)}` : state.name;
                const itemWidth = this.itemWidth();
                this._requestQueue.push({
                    command: 'add',
                    target: 0,
                    item: {
                        stateId: stateId,
                        stateLevel: level, 
                        text: text,
                        opacity: 0,
                        state: 'add',
                        frame: 0,
                        offsetX: itemWidth,
                        offsetY: 0,
                    },
                });
            } else {
                Logger.d(`addState: state not found, state id = ${stateId}`);
            }
        }

        removeState(stateId) {
            const found = this._items.findIndex(item => {
                return (item.stateId === stateId);
            });

            if (found >= 0) {
                this._requestQueue.push({
                    command: 'remove',
                    target: stateId,
                    item: null,
                });
            } else {
                Logger.d(`removeState: state not found, state id = ${stateId}`);
            }
        }

        clearState() {
            this._items.forEach((item, index) => {
                this._requestQueue.push({
                    command: 'remove',
                    target: item.stateId,
                    item: null,
                });
            });
        }

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

        processOk() {
            // nothing to do
        }

        maxItems() {
            return param.logLineCount;
        }

        drawItemBackground(index) {
            if (!this._itemBackgroundImage) {
                return;
            }

            const item = this.item(index);
            if (!!item) {
                const rect = this.itemRect(index);
                const bitmap = this.contentsBack;

                this.contentsBack.paintOpacity = item.opacity;
                bitmap.blt(this._itemBackgroundImage, 0, 0, this._itemBackgroundImage.width, this._itemBackgroundImage.height,
                    rect.x, rect.y, rect.width, rect.height);
                bitmap.paintOpacity = 255;
            }
        }

        itemRect(index) {
            const rect = super.itemRect(index);
            const item = this.item(index);
            if (!!item) {
                const maxCols = this.maxCols();
                const row = Math.floor(index / maxCols);
                const itemHeight = this.itemHeight();
                const lineHeight = this.oneLineHeight();
                rect.y = this.height - itemHeight - (row * lineHeight) - this.scrollBaseY();

                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 bitmap = new Bitmap(rect.width, rect.height);

                this.contents.paintOpacity = item.opacity;
                bitmap.fontSize = 24;
                bitmap.drawText(item.text, 0, 0, rect.width, rect.height, 'center');
                this.contents.blt(bitmap, 0, 0, rect.width, rect.height, rect.x, rect.y);
                bitmap.destroy();
            }
        }

        isCursorMovable() {
            return false;
        }

        rowSpacing() {
            return Window_BattleState.ROW_SPACING;
        }

        lineHeight() {
            return Window_BattleState.LINE_HEIGHT;
        }

        oneLineHeight() {
            // MZのLineHeightがItemHeightになっててまともに動作しないので
            // 本当に1行分のスペース（要素の高さ＋スペーシング）で出した一行分の高さ
            const itemHeight = this.itemHeight();
            const rowSpacing = this.rowSpacing();
            return itemHeight + (rowSpacing / 2);
        }

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

        itemPadding() {
            return 0;
        }

        updatePadding() {
            // nothing to do
        }

        //========================================================================
        // フレーム毎更新処理
        //========================================================================
        update() {
            this._items = this._items.filter(item => (item.state !== 'removed'));
            if (this._commandState === 'neutral') {
                if (this._requestQueue.length > 0) {
                    const request = this._requestQueue.shift();
                    switch (request.command) {
                        case 'add': {
                            const lineHeight = this.oneLineHeight();
                            this._items.forEach((item, index) => {
                                item.offsetY = lineHeight;
                            });
                            const firstItem = request.item;
                            this._items.splice(0, 0, firstItem);
                            this._commandState = 'ensure';
                            break;
                        }
                        case 'remove': {
                            const target = request.target;
                            if (target > 0) {
                                const found = this._items.findIndex(item => {
                                    return (item.stateId === target);
                                });
                                this._updateIndex = found;
                                if (!!this._items[found]) {
                                    this._items[found].state = 'remove';
                                    this._commandState = 'removing';
                                }
                            }
                            break;
                        }
                        default:
                            Logger.w(`update: unknown request = ${request.command}`);
                            break;
                    } // switch
                }
            }

            this.updateFrame();
            this.updateItems();
            this.refresh();
        }

        updateItemOffset(item, index, duration) {
            const lineHeight = this.oneLineHeight();
            switch (this._commandState) {
                case 'ensure': {
                    if (index <= this.updateIndex) {
                        return;
                    }

                    item.offsetY -= (lineHeight / duration);
                    if (this._animationFrame > duration) {
                        item.offsetY = 0;
                    }
                    break;
                }
                case 'reduce':
                    if (index <= this.updateIndex) {
                        return;
                    }

                    item.offsetY += (lineHeight / duration);
                    if (this._animationFrame > duration) {
                        item.offsetY = lineHeight;
                    }
                    break;
                default: {
                    item.offsetY = 0;
                    break;
                }
            } // switch

            if (item.offsetY < 0) {
                item.offsetY = 0;
            }
        }

        updateFrame() {
            const duration = this.lineUpdateDuration();
            switch (this._commandState) {
                case 'ensure':
                    if (this._animationFrame > duration) {
                        this.completeAnimation();
                    } else {
                        this._animationFrame++;
                    }
                    break;
                case 'reduce':
                    if (this._animationFrame > duration) {
                        this.completeAnimation();
                    } else {
                        this._animationFrame++;
                    }
                    break;
                default:
                    break;
            } // switch
        }

        updateItems() {
            const duration = this.lineUpdateDuration();
            const itemWidth = this.itemWidth();

            this._items.forEach((item, index) => {
                switch (item.state) {
                    case 'add':
                        if (this._commandState === 'insert') {
                            item.offsetX = itemWidth - ((itemWidth / duration) * item.frame);
                            if (item.frame > duration) {
                                item.offsetX = 0;
                                item.opacity = 255;
                                item.state = 'active';
                                this.completeAnimation();
                            } else {
                                item.frame++;
                                item.opacity += (256 / duration);
                            }
                        }
                        break;
                    case 'active':
                        this.updateItemOffset(item, index, duration);
                        break;
                    case 'remove':
                        item.offsetX = ((itemWidth / duration) * item.frame);
                        item.opacity -= (256 / duration);
                        if (item.opacity < 0) {
                            item.opacity = 0;
                        }

                        if (item.frame > duration) {
                            item.opacity = 0;
                            item.state = 'removed';
                            this.completeAnimation();
                        } else {
                            item.frame++;
                        }
                        break;
                    default:
                        // do nothing
                        break;
                } // switch
            });
        }

        completeAnimation() {
            this._updateIndex = 0;
            switch (this._commandState) {
                case 'insert':
                case 'reduce':
                    this._commandState = 'neutral';
                    break;
                case 'ensure':
                    this._commandState = 'insert';
                    break;
                case 'removing':
                    this._commandState = 'reduce';
                    const lineHeight = this.oneLineHeight();
                    this._items.forEach(item => {
                        if (item.state !== 'removed') {
                            item.offsetY = -lineHeight;
                        }
                    });
                    break;
            } // switch

            this._animationFrame = 0;
            this._items.forEach(item => {
                item.frame = 0;
            });
        }

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

    //========================================================================
    // コアスクリプト変更部 (Game_Actor)
    //========================================================================
    const _Game_Actor_prototype_clearStates = Game_Actor.prototype.clearStates;
    Game_Actor.prototype.clearStates = function() {
        _Game_Actor_prototype_clearStates.apply(this, arguments);

        if (!!this._stateHook) {
            this._stateHook.clearState();
        }
    };

    const _Game_Actor_prototype_eraseState = Game_Actor.prototype.eraseState;
    Game_Actor.prototype.eraseState = function(stateId) {
        _Game_Actor_prototype_eraseState.apply(this, arguments);

        if (!!this._stateHook && !this.isStickyState(stateId)) {
            this._stateHook.removeState(stateId);
        }
    };

    const _Game_Actor_prototype_resetStateCounts = Game_Actor.prototype.resetStateCounts;
    Game_Actor.prototype.resetStateCounts = function(stateId) {
        _Game_Actor_prototype_resetStateCounts.apply(this, arguments);

        if (!!this._stateHook && !this.isStickyState(stateId)) {
            this._stateHook.addState(stateId);
        }
    };

    Game_Actor.prototype.setStateHook = function(hook) {
        if (!!hook) {
            this.displayedStates().forEach(state => {
                hook.addState(state.id);
            });            
        }
        this._stateHook = hook;
    };

    //========================================================================
    // コアスクリプト変更部 (Scene_Battle)
    //========================================================================
    const _Scene_Battle_prototype_createDisplayObjects = Scene_Battle.prototype.createDisplayObjects;
    Scene_Battle.prototype.createDisplayObjects = function() {
        _Scene_Battle_prototype_createDisplayObjects.apply(this, arguments);

        ImageManager.loadSystemAsync('battle_state/state_description').then(bitmap => {
            this.stateDescriptionSprite = new Sprite(bitmap);
            this.stateDescriptionSprite.zIndex = 100;

            const x = (Graphics.width - bitmap.width) / 2;
            const y = (Graphics.height - bitmap.height) / 2;
            this.stateDescriptionSprite.move(x, y);
            this.stateDescriptionSprite.hide();
            this.addChild(this.stateDescriptionSprite);
        });

        const bitmap = new Bitmap(290, 84);
        bitmap.paintOpacity = PluginUtils.percent(255, 40);
        bitmap.fillAll('#000000');
        bitmap.paintOpacity = 255;
        bitmap.fontFace = $gameSystem.mainFontFace();
        bitmap.fontSize = 24;
        bitmap.textColor = '#FFFFFF';
        bitmap.outlineColor = '#000000';

        const textHeight = 36;
        const text = TextResource.getText(RES_TEXT_STATE_HELP);
        text.split('\n').forEach((line, index) => {
            const x = 10;
            const y = 4 + (index * textHeight);
            bitmap.drawText(line, x, y, 270, textHeight, 'left');
        });
        this.helpSprite = new Sprite(bitmap);
        this.helpSprite.move(1611, 17);
        this.addChild(this.helpSprite);
    };

    const _Scene_Battle_prototype_update = Scene_Battle.prototype.update;
    Scene_Battle.prototype.update = function() {
        _Scene_Battle_prototype_update.apply(this, arguments);

        if (this.isActive() && !this.isBusy() && !!this.stateDescriptionSprite) {
            if (Input.isPressed(InputClass.INPUT_CLASS_SELECT)) {
                if (!this.stateDescriptionSprite.visible) {
                    SoundManager.playOk();
                    this.stateDescriptionSprite.show();
                    this.helpSprite.hide();
                }
            } else if (this.stateDescriptionSprite.visible) {
                this.stateDescriptionSprite.hide();
                this.helpSprite.show();
            }
        }
    };

    const _Scene_Battle_createAllWindows = Scene_Battle.prototype.createAllWindows;
    Scene_Battle.prototype.createAllWindows = function() {
        _Scene_Battle_createAllWindows.apply(this, arguments);

        this._stateWindow = new Window_BattleState();
        this.addWindow(this._stateWindow);
        
        const actor = $gameActors.actor(param.targetActor);
        if (!!actor) {
            actor.setStateHook(this._stateWindow);
        }
    };

    const _Scene_Battle_prototype_terminate = Scene_Battle.prototype.terminate;
    Scene_Battle.prototype.terminate = function() {
        _Scene_Battle_prototype_terminate.apply(this, arguments);

        const actor = $gameActors.actor(param.targetActor);
        if (!!actor) {
            actor.setStateHook(null);
        }
    };
})();
