//============================================================================
// SU_ExpandedPictureSpine.js
//============================================================================
/*:
    * @plugindesc PictureSpine.js の機能を拡張する
    * @author Su
    *
    * @help
    * ============================================================================
    * 概要
    * ============================================================================
    * PictureSpine.js を機能拡張し、プラグインコマンドを追加します。
    * このプラグインは PictureSpine.js のすぐ下に置いてください
    * 
    * ピクチャの表示とアニメーション指定を同時に行えるコマンドを追加しています。
    * 
    * ============================================================================
    * プラグインコマンド
    * ============================================================================
    * 値は \v[変数番号] とすることで変数の中身に置き換えることができます。
    *
    * - S_setAnimation [pictureId] [skeletonId] (animation:[trackId]:[animationName] <mix:[mixValue]>)
    *  指定のピクチャ番号のスケルトンにアニメーションをセットします。
    *  指定番号のピクチャが表示されていない場合、ピクチャの表示を同時に行います。
    *  値は [ ] で囲った部分です
    *  < > で囲った中身は省略可能
    *   pictureId : ピクチャ番号
    *   skeletonId : プラグインパラメータのスケルトンリスト内の番号
    *   trackId : アニメーションをセットしたいトラック番号
    *   animationName :  セットしたいアニメーション名
    *   mixValue : 手前で指定したアニメーション遷移に対するミックス値
    *  [ ]、< >, ( ) は入力しないでください。
    *  
    * 例）１０番のピクチャに「走る」というアニメーションをトラック番号１に指定する場合
    *   S_setAnimation 10 0 animation:1:走る
    *
    * 例）上記に加え、ミックス値を 0.5 に設定する場合
    *   S_setAnimation 10 0 animation:1:走る mix:0.5
    *
    *  また ( ) で囲っている animation と mix の指定は繰り返し行うことができます
    * 例）上記の例に加え、「狙う」 というアニメーションをトラック番号３にミックス値０．３で指定する場合
    *   S_setAnimation 10 0 animation:1:走る mix:0.5 animation:3:狙う mix:0.3
    *
    * 例）トラック番号に変数１番、アニメーション名に変数２番の値を使用する場合
    *   S_setAnimation 10 0 animation:\v[1]:\v[2]
    *
    * ============================================================================
    * 開発者向けに
    * ============================================================================
    * Game_Spineクラスにイベント登録・削除を行うメンバ関数を追加しました。
    * アニメーション中のイベント（Spine側で設定するものではなく）に関数を登録することができます。
    * - start
    * - interrupt
    * - dispose
    * - end
    * - complete
    * - event
    *
    * イベントリスナー の登録
    * addEventListener(eventName:string, listener:function, <trackId:number>, <options:object>)
    * イベントリスナー の削除
    * removeEventListener(eventName:string, listener:function, <trackId:number>)
    *
    *
    * @param Skeletons
    * @type struct<SpineData>[]
    * @default []
    * @desc ピクチャが表示されていない場合に表示するピクチャ、スケルトンについて設定します
    * @text スケルトン
*/

/*~struct~Initialization:
    * @param PositionX
    * @number
    * @default 0
    * @text 位置X
    *
    * @param PositionY
    * @number
    * @default 0
    * @text 位置Y
    *
    * @param ScaleX
    * @number
    * @default 100
    * @desc １００で等倍表示
    * @text 拡大率X
    *
    * @param ScaleY
    * @number
    * @default 100
    * @desc １００で等倍表示
    * @text 拡大率Y
*/

/*~struct~SpineData:
    * @param Skeleton
    * @type string
    * @text スケルトン名
    * 
    * @param Skin
    * @type string[]
    * @text スキン名
    *
    * @param NumInitializeTrack
    * @type number
    * @default 10
    * @desc スケルトンをセットする際に指定個数のトラックを初期化します
    * @text トラック初期化数
    *
    * @param ResetAnimation
    * @type string
    * @default 000
    * @desc 前項の初期化に使用するアニメーション名を設定します
    * @text 初期化用アニメーション
    *
    * @param Mix
    * @type number
    * @decimals 2
    *
    * @param InitialPicSetting
    * @type struct<Initialization>
    * @desc 予めピクチャの表示がされていない場合のピクチャ設定
    * @text ピクチャ初期化設定
    * @default {"PositionX":"0","PositionY":"0","ScaleX":"100","ScaleY":"100"}
    *
*/

/*~struct~Animation:
    * @param Track
    * @ytpe number
    *
    * @param Animation
    * @type string
*/

(() => {
    'use strict';

    //============================================================================
    // パラメータ取得
    //============================================================================
    const parsedParams = (() => {
        const pluginName = document.currentScript.src.split('/').pop().replace(/\.js$/, '');
        const pluginParams = PluginManager.parameters(pluginName);

        return JSON.parse(JSON.stringify(
            pluginParams,
            (key, value) => {
                try { return JSON.parse(value); } catch (e) { }
                return value
            }
        ));
    })();

    const initSpineData = parsedParams['Skeletons'];

    //============================================================================
    // プラグイン本体
    //============================================================================
    (_pluginCommand_ => {
        Game_Interpreter.prototype.pluginCommand = function (command, args) {
            _pluginCommand_.apply(this, arguments);

            const com = command.toUpperCase();
            const convArgs = args.map(arg => convert2Var(arg));

            switch (com) {
                // S_Animation [picId] [skeletonId] [animation:trackId:AnimationName] [mix:value]
                case 'S_SETANIMATION':
                    console.log('execute S_Animation');
                    const picId = convArgs[0];
                    const spineData = initSpineData[convArgs[1] - 1];

                    // ピクチャが表示されていなければ用意する
                    if (spineData) {
                        if (!$gameScreen.picture(picId)) {
                            const initData = spineData['InitialPicSetting'];
                            const initPos = { x: initData['PositionX'], y: initData['PositionY'] };
                            const initScale = { x: initData['ScaleX'], y: initData['ScaleY'] };
                            $gameScreen.showPicture(
                                picId, "", 0,
                                initPos.x, initPos.y,
                                initScale.x, initScale.y,
                                255, 0
                            );

                            const skeleton = spineData['Skeleton'];
                            const skin = spineData['Skin'];
                            const mix = spineData['Mix'];
                            const spine = $gameScreen.spine(picId);
                            spine.setSkeleton(skeleton);
                            if (skin) spine.setSkin(skin)
                            if (mix) spine.setMix(mix);
                        }
                    }

                    if (!$gameScreen.picture(picId)) return;
                    const spine = $gameScreen.spine(picId);

                    let track = 0;
                    let animation = '';

                    const currentTrackList = spine.track;

                    for (let i = 2; i < convArgs.length; i++) {
                        let options = convArgs[i].split(':');
                        switch (options[0]) {
                            case 'animation':
                                track = options[1];
                                animation = options[2].split(',');
                                spine.setAnimation(track, animation, 'sequential', 'continue', true);
                                break;
                            case 'mix':
                                const value = Number(options[1]);
                                let currentAnimation = (() => {
                                    if (!!currentTrackList[track]) {
                                        const list = currentTrackList[track].list;
                                        return list[list.length - 1].name;
                                    }
                                    else {
                                        return '';
                                    }
                                })();
                                const nextAnimation = animation[0];
                                const transition = `${currentAnimation}/${nextAnimation}`;
                                let currentMix = spine.mix[transition] || null;
                                if (currentAnimation) {
                                    spine.setMix(currentAnimation, nextAnimation, value);

                                    const createCallback = (spine, track, prevMix, transition) => {
                                        const baseSpine = spine;
                                        const targetTrack = track;
                                        const callback = () => {
                                            baseSpine.mix[transition] = prevMix;
                                            console.log(transition);
                                            baseSpine.removeEventListener('end', callback, targetTrack);
                                        };

                                        return callback;
                                    };

                                    const callback = createCallback(spine, track, currentMix, transition);
                                    spine.addEventListener('end', callback, track)
                                }
                                break;
                        }
                    }
            }
        }
    })(Game_Interpreter.prototype.pluginCommand);

    const convert2Var = (text) => {
        if (typeof text !== 'string') return text;
        return text.replace(/\\v\[(\d+)\]/gi, (match, p1) => $gameVariables.value(Number(p1)));
    }


    (_init_ => {
        Game_Spine.prototype.init = function () {
            _init_.call(this);

            this.initListeners();
        }
    })(Game_Spine.prototype.init);


    //============================================================================
    // トラックを指定数初期化
    //============================================================================
    Game_Spine.prototype.initTracks = function () {
        const refData = initSpineData.filter(params => params['Skeleton'] === this.skeleton.split('_')[0]).shift();
        if (!refData) return;

        const numInitializedTrack = refData['NumInitializeTrack'] || 0;
        this.resetAnimation = refData['ResetAnimation'];
        for (let i = 0; i < numInitializedTrack; i++) {
            this.setAnimation(i, this.resetAnimation, 'none');
        }
    };

    // overwrite
    (_setSkeleton_ => {
        Game_Spine.prototype.setSkeleton = function (name = '') {
            _setSkeleton_.apply(this, arguments);

            this.initTracks();

            return this;
        }
    })(Game_Spine.prototype.setSkeleton);

    //============================================================================
    // イベントを登録できるようにする
    //============================================================================
    Object.defineProperty(Game_Spine, 'listeners', {
        get() { return [this._globalListener, ...this._trackListeners]; }
    });

    Game_Spine.prototype.initListeners = function () {
        this._globalListener = {
            start: [],
            interrupt: [],
            dispose: [],
            end: [],
            complete: [],
            event: []
        };
        this._trackListeners = [];
    }

    /**
     * アニメーション中の各イベント(Spine で設定するイベントとは異なる）が起こった時に呼び出される関数を設定します
     * @param {string} eventName イベント名
     * @param {Function} listener 呼び出される関数
     * @param {number} trackIndex 対象のトラック番号、指定しなければ全てのトラックを対象にする
     * @param {object} options 追加動作を指定する
     */
    Game_Spine.prototype.addEventListener = function (eventName, listener, trackIndex = -1, options = {}) {
        if (trackIndex >= 0) {
            if (!this._trackListeners[trackIndex]) this._trackListeners[trackIndex] = {
                start: [],
                interrupt: [],
                dispose: [],
                end: [],
                complete: [],
                event: []
            };
            this._trackListeners[trackIndex][eventName].push(Object.assign(options, { listener: listener }));
        }
        else
            this._globalListener[eventName].push(Object.assign(options, { listener: listener }));
    }

    /**
     * 登録したイベントを削除します
     * @param {string} eventName イベント名
     * @param {Function} listener 登録した関数
     * @param {number} trackIndex 対象のトラック番号、指定しなければ全てのトラックを対象にする
     */
    Game_Spine.prototype.removeEventListener = function (eventName, listener, trackIndex) {
        const listeners = trackIndex ? this._trackListeners[trackIndex][eventName] : this._globalListener[eventName];
        const index = listeners.map(e => e.listener).indexOf(listener);
        if (index !== -1) {
            listeners.splice(index, 1);
        }
    };

    /**
     * イベントに登録された関数を呼び出します
     * @param {string} eventName イベント名
     * @param {number} trackIndex トラック番号
     */
    Game_Spine.prototype.dispatchEventListener = function (eventName, trackIndex) {
        const globalCallbacks = this._globalListener[eventName];
        if (globalCallbacks)
            globalCallbacks.forEach(e => {
                e.listener();
                if (e.once) this.removeEventListener(eventName, e.listener, trackIndex);
            });


        if (trackIndex >= 0 && this._trackListeners[trackIndex]) {
            const trackCallbacks = this._trackListeners[trackIndex][eventName];

            if (trackCallbacks) {
                trackCallbacks.forEach(e => {
                    e.listener();

                    if (e.once) this.removeEventListener(eventName, e.listener, trackIndex);
                });
            }
        }
    };

    // overwrite
    // end と dispose のコメントアウトを外しただけです
    Game_Spine.prototype.updateSkeleton = function (spine) {
        if (spine) {
            if (spine.skeleton != this._skeleton) {
                this.init();
                let skeleton = spine.skeleton.replace(/_\d+$/, '');
                if (skeleton) {
                    let fullName = Game_Spine.fullName(skeleton);
                    let data = Game_Spine.spineData()[fullName];
                    if (data) {
                        if ('animations' in data) {
                            this._data = new PIXI.spine.Spine(data);
                            this._data.destroy = function () { };
                            this._data.state.addListener({
                                start: this.onStart.bind(this),
                                interrupt: this.onInterrupt.bind(this),
                                end: this.onEnd.bind(this),
                                dispose: this.onDispose.bind(this),
                                complete: this.onComplete.bind(this),
                                event: this.onEvent.bind(this)
                            });
                            this.addChild(this._data);
                            if (spine.playData.length > 0) {
                                this._isRestore = true;
                            }
                            this.registerCallback(spine);
                            this.createNames();
                        } else {
                            Game_Spine.loadSkeleton(skeleton);
                        }
                    } else if (fullName in Game_Spine.spineData() == false) {
                        throw Error(`'${skeleton}' is unknown model.`);
                    }
                }
                if (!skeleton || this._data) {
                    this._skeleton = spine.skeleton;
                }
            }
        } else if (this._skeleton) {
            this.init();
        }
    };

    (_onStart_ => {
        Sprite_Spine.prototype.onStart = function (entry) {
            this.spine().dispatchEventListener('start', entry.trackIndex);

            _onStart_.apply(this, arguments);
        }
    })(Sprite_Spine.prototype.onStart);

    (_onInterrupt_ => {
        Sprite_Spine.prototype.onInterrupt = function (entry) {
            this.spine().dispatchEventListener('interrupt', entry.trackIndex);

            _onInterrupt_.apply(this, arguments);
        }
    })(Sprite_Spine.prototype.onInterrupt);
    (_onEnd_ => {
        Sprite_Spine.prototype.onEnd = function (entry) {
            this.spine().dispatchEventListener('end', entry.trackIndex);

            _onEnd_.apply(this, arguments);
        }
    })(Sprite_Spine.prototype.onEnd);
    (_onDispose_ => {
        Sprite_Spine.prototype.onDispose = function (entry) {
            this.spine().dispatchEventListener('dispose', entry.trackIndex);

            _onDispose_.apply(this, arguments);
        }
    })(Sprite_Spine.prototype.onDispose);
    (_onComplete_ => {
        Sprite_Spine.prototype.onComplete = function (entry) {
            this.spine().dispatchEventListener('complete', entry.trackIndex);

            _onComplete_.apply(this, arguments);
        }
    })(Sprite_Spine.prototype.onComplete);
    (_onEvent_ => {
        Sprite_Spine.prototype.onEvent = function (entry, event) {
            this.spine().dispatchEventListener('event', entry.trackIndex);

            _onEvent_.apply(this, arguments);
        }
    })(Sprite_Spine.prototype.onEvent);
})();