/**
 * Game transitions and dialog
 **/
namespace game {
    /**
     * Determines if diagnostics are shown
     */
    export let debug = false;
    export let stats = false;

    export enum ScoringType {
        //% block="high score"
        HighScore,
        //% block="low score"
        LowScore,
        //% block="none"
        None
    }

    // To stay synchronized with https://github.com/microsoft/pxt/blob/stable8.5/webapp/src/components/ImageEditor/sprite/Palette.tsx#L98.
    /**
     * The available colors for Arcade.
     * NOTE: If the color palette is changed, these values will change along with it.
     **/
    export enum Color {
        Transparent = 0,
        White = 1,
        Red = 2,
        Pink = 3,
        Orange = 4,
        Yellow = 5,
        Teal = 6,
        Green = 7,
        Blue = 8,
        LightBlue = 9,
        Purple = 0xa,
        LightPurple = 0xb,
        DarkPurple = 0xc,
        Tan = 0xd,
        Brown = 0xe,
        Black = 0xf
    }

    export class GameOverConfig {
        scoringType: ScoringType;
        winEffect: effects.BackgroundEffect;
        loseEffect: effects.BackgroundEffect;
        loseSound: music.Playable;
        winSound: music.Playable;
        loseSoundLooping: boolean;
        winSoundLooping: boolean;
        winMessage: string;
        winMessageMultiplayer: string;
        loseMessage: string;
        effectSetByUser: boolean;
        soundSetByUser: boolean;
        messageSetByUser: boolean;
        scoringTypeSetByUser: boolean;

        constructor() {
            this.init();
        }

        init() {
            this.scoringType = ScoringType.HighScore;
            this.winEffect = effects.confetti;
            this.loseEffect = effects.melt;
            this.winSound = music.melodyPlayable(music.powerUp);
            this.loseSound = music.melodyPlayable(music.wawawawaa);
            this.winSoundLooping = false;
            this.loseSoundLooping  = false;
            this.winMessage = "YOU WIN!";
            this.winMessageMultiplayer = "${WINNER} WINS!";
            this.loseMessage = "GAME OVER";
            this.effectSetByUser = false;
            this.soundSetByUser = false;
            this.messageSetByUser = false;
            this.scoringTypeSetByUser = false;
        }

        setScoringType(type: ScoringType, explicit: boolean) {
            if (!explicit && this.scoringTypeSetByUser) return;
            this.scoringType = type;
            if (explicit) this.scoringTypeSetByUser = true;
        }

        setEffect(win: boolean, effect: effects.BackgroundEffect, explicit: boolean) {
            if (!explicit && this.effectSetByUser) return;
            if (win) this.winEffect = effect;
            else this.loseEffect = effect;
            if (explicit) this.effectSetByUser = true;
        }
        getEffect(win: boolean) {
            return win ? this.winEffect : this.loseEffect;
        }

        setSound(win: boolean, sound: music.Playable, looping: boolean, explicit: boolean) {
            if (!explicit && this.soundSetByUser) return;
            if (win) {
                this.winSound = sound;
                this.winSoundLooping = looping;
            } else {
                this.loseSound = sound;
                this.loseSoundLooping = looping;
            }
            if (explicit) this.soundSetByUser = true;
        }
        getSound(win: boolean) {
            return win ? this.winSound : this.loseSound;
        }
        getSoundLooping(win: boolean) {
            return win ? this.winSoundLooping : this.loseSoundLooping;
        }

        setMessage(win: boolean, message: string, explicit: boolean) {
            if (!explicit && this.messageSetByUser) return;
            if (win) this.winMessage = message;
            else this.loseMessage = message;
            if (explicit) this.messageSetByUser = true;
        }
        getMessage(win: boolean, preferMultiplayer?: boolean) {
            if (this.messageSetByUser)
                return win ? this.winMessage : this.loseMessage;
            else if (preferMultiplayer)
                return win ? this.winMessageMultiplayer : this.loseMessage;
            else
                return win ? this.winMessage : this.loseMessage;
        }
    }

    let _gameOverConfig: GameOverConfig;
    export const gameOverConfig = () => {
        if (!_gameOverConfig) _gameOverConfig = new GameOverConfig();
        return _gameOverConfig;
    }

    let _scene: scene.Scene;
    let _sceneStack: scene.Scene[];

    let _scenePushHandlers: ((scene: scene.Scene) => void)[];
    let _scenePopHandlers: ((scene: scene.Scene) => void)[];

    export function currentScene(): scene.Scene {
        init();
        return _scene;
    }

    let __waitAnyButton: () => void;
    let __gameOverHandler: (win: boolean) => void;
    let __isOver = false;

    export function setWaitAnyButton(f: () => void) {
        __waitAnyButton = f
    }

    export function waitAnyButton() {
        if (__waitAnyButton) __waitAnyButton()
        else pause(3000)
    }

    export function eventContext(): control.EventContext {
        init();
        return _scene.eventContext;
    }

    function init(forceNewScene ?: boolean) {
        if (!_scene || forceNewScene) {
            _scene = new scene.Scene(control.pushEventContext(), _scene);
        }
        _scene.init();
    }

    export function pushScene() {
        const oldScene = game.currentScene()
        particles.clearAll();
        particles.disableAll();
        if (!_sceneStack) {
            _sceneStack = [];
            music._initializeSceneStack(game.addScenePushHandler, game.addScenePopHandler);
        }
        _sceneStack.push(_scene);
        init(/** forceNewScene **/ true);

        if (_scenePushHandlers) {
            _scenePushHandlers.forEach(cb => cb(oldScene));
        }
    }

    export function popScene() {
        const oldScene = game.currentScene()
        if (_sceneStack && _sceneStack.length) {
            // pop scenes from the stack
            _scene = _sceneStack.pop();
            control.popEventContext();
        } else if (_scene) {
            // post last scene
            control.popEventContext();
            _scene = undefined;
        }

        if (_scene)
            particles.enableAll();

        if (_scenePopHandlers) {
            _scenePopHandlers.forEach(cb => cb(oldScene));
        }
    }

    function showDialogBackground(h: number, c: number) {
        const top = (screen.height - h) >> 1;
        screen.fillRect(0, top, screen.width, h, 0)
        screen.drawLine(0, top, screen.width, top, 1)
        screen.drawLine(0, top + h - 1, screen.width, top + h - 1, 1)

        return top;
    }

    export function showDialog(title: string, subtitle: string, footer?: string) {
        init();
        const titleFont = image.getFontForText(title || "");
        const subFont = image.getFontForText(subtitle || "")
        const footerFont = image.getFontForText(footer || "");
        let h = 8;
        if (title)
            h += titleFont.charHeight;
        if (subtitle)
            h += 2 + subFont.charHeight
        h += 8;
        const top = showDialogBackground(h, 9)
        let y = top + 8;
        if (title) {
            screen.print(title, 8, y, screen.isMono ? 1 : 7, titleFont);
            y += titleFont.charHeight + 2;
        }
        if (subtitle) {
            screen.print(subtitle, 8, y, screen.isMono ? 1 : 6, subFont);
            y += subFont.charHeight + 2;
        }
        if (footer) {
            const footerTop = screen.height - footerFont.charHeight - 4;
            screen.fillRect(0, footerTop, screen.width, footerFont.charHeight + 4, 0);
            screen.drawLine(0, footerTop, screen.width, footerTop, 1);
            screen.print(
                footer,
                screen.width - footer.length * footerFont.charWidth - 8,
                screen.height - footerFont.charHeight - 2,
                1,
                footerFont
            )
        }
    }

    /**
     * Set the effect that occurs when the game is over
     * @param win whether the effect should run on a win (true) or lose (false)
     * @param effect
     */
    //% blockId=game_setgameovereffect
    //% block="use effect $effect for $win"
    //% effect.defl=effects.confetti
    //% win.shadow=toggleWinLose
    //% win.defl=true
    //% group="Game Over"
    //% weight=90
    //% blockGap=8
    //% help=game/set-game-over-effect
    export function setGameOverEffect(win: boolean, effect: effects.BackgroundEffect) {
        init();
        const goc = game.gameOverConfig();
        goc.setEffect(win, effect, true);
    }

    /**
     * Set the music that occurs when the game is over
     * @param win whether the sound should play on a win (true) or lose (false)
     * @param effect
     */
    //% blockId=game_setgameoverplayable
    //% block="use $sound looping $looping for $win"
    //% sound.shadow=music_melody_playable
    //% sound.defl=music.powerUp
    //% looping.shadow=toggleOnOff
    //% looping.defl=false
    //% win.shadow=toggleWinLose
    //% win.defl=true
    //% group="Game Over"
    //% weight=80
    //% blockGap=8
    //% help=game/set-game-over-playable
    export function setGameOverPlayable(win: boolean, sound: music.Playable, looping: boolean) {
        init();
        const goc = game.gameOverConfig();
        goc.setSound(win, sound, looping, true);
    }

    // Legacy api. Older extensions may still use this.
    export function setGameOverSound(win: boolean, sound: music.Melody) {
        init();
        const goc = game.gameOverConfig();
        goc.setSound(win, music.melodyPlayable(sound), false, true);
    }

    /**
     * Set the message that displays when the game is over
     * @param win whether the message should show on a win (true) or lose (false)
     * @param message
     */
    //% blockId=game_setgameovermessage
    //% block="use message $message for $win"
    //% message.defl="GAME OVER!"
    //% win.shadow=toggleWinLose
    //% win.defl=true
    //% group="Game Over"
    //% weight=70
    //% blockGap=8
    //% help=game/set-game-over-message
    export function setGameOverMessage(win: boolean, message: string) {
        init();
        const goc = game.gameOverConfig();
        goc.setMessage(win, message, true);
    }

    /**
     * Set the method of judging the best score for the game
     * @param type the scoring type
     */
    //% blockId=game_setgameoverscoringtype
    //% block="use $type as best score"
    //% type.defl=ScoringType.HighScore
    //% group="Game Over"
    //% weight=60
    //% blockGap=8
    //% help=game/set-game-over-scoring-type
    export function setGameOverScoringType(type: ScoringType) {
        init();
        const goc = game.gameOverConfig();
        goc.setScoringType(type, true);
    }

    /**
     * Set the function to call on game over. The 'win' boolean is
     * passed to the handler.
     * @param handler
     */
    export function onGameOver(handler: (win: boolean) => void) {
        __gameOverHandler = handler;
    }

    /**
     * Finish the game and display the score
     */
    //% group="Gameplay"
    //% blockId=gameOver block="game over %win=toggleWinLose || with %effect effect"
    //% weight=80 help=game/over
    //% deprecated=true
    export function over(win: boolean = false, effect?: effects.BackgroundEffect) {
        // Match legacy behavior unless effect was set by user
        const goc = game.gameOverConfig();
        goc.setEffect(win, effect, false);
        _gameOverImpl(win);
    }

    //% blockId=gameOver2 block="game over $win"
    //% win.shadow=toggleWinLose
    //% win.defl=true
    //% weight=100
    //% blockGap=8
    //% help=game/over
    //% group="Game Over"
    export function gameOver(win: boolean) {
        _gameOverImpl(win);
    }

    export function gameOverPlayerWin(player: number) {
        _gameOverImpl(true, player);
    }

    function _mapScoreTypeToString(scoreType: ScoringType): string {
        switch (scoreType) {
            case ScoringType.HighScore: return "highscore";
            case ScoringType.LowScore: return "lowscore";
            case ScoringType.None: return "none";
            default: return "none";
        }
    }

    function _gameOverImpl(win: boolean, winnerOverride?: number) {
        init();
        if (__isOver) return;
        __isOver = true;

        if (__gameOverHandler) {
            __gameOverHandler(win);
        } else {
            const goc = game.gameOverConfig();

            const judged = !winnerOverride && goc.scoringType !== ScoringType.None;
            const playersWithScores = info.playersWithScores();
            const prevBestScore = judged && info.highScore();
            const winner = judged && win && info.winningPlayer();
            const scores = playersWithScores.map(player => new GameOverPlayerScore(player.number, player.impl.score(), player === winner));

            // Save all scores. Dependency Note: this action triggers Kiosk to exit the simulator and show the high score screen.
            const scoreTypeString = _mapScoreTypeToString(goc.scoringType);
            info.saveAllScores(scoreTypeString);

            // Save high score if this was a judged game and there was a winner (don't save in the LOSE case).
            if (judged && winner) {
                info.saveHighScore();
            }

            const preferMultiplayer = !!winnerOverride || (judged && info.multiplayerScoring());
            const message = goc.getMessage(win, preferMultiplayer);
            const effect = goc.getEffect(win);
            const sound = goc.getSound(win);
            const looping = goc.getSoundLooping(win);
            const playbackMode = looping ? music.PlaybackMode.LoopingInBackground : music.PlaybackMode.InBackground;

            // releasing memory and clear fibers. Do not add anything that releases the fiber until background is set below,
            // or screen will be cleared on the new frame and will not appear as background in the game over screen.
            while (_sceneStack && _sceneStack.length) {
                _scene.destroy();
                popScene();
            }
            pushScene();
            scene.setBackgroundImage(screen.clone());

            if (sound) music.play(sound, playbackMode);
            if (effect) effect.startScreenEffect();

            pause(400);

            const overDialog = new GameOverDialog(win, message, judged, scores, prevBestScore, winnerOverride);
            scene.createRenderable(scene.HUD_Z, target => {
                overDialog.update();
                target.drawTransparentImage(
                    overDialog.image,
                    0,
                    (screen.height - overDialog.image.height) >> 1
                );
            });

            pause(500); // wait for users to stop pressing keys
            overDialog.displayCursor();
            waitAnyButton();
            control.reset();
        }
    }


    // Indicates whether the fiber needs to be created
    let foreverRunning = false;

    /**
     * Repeats the code forever in the background for this scene.
     * On each iteration, allows other codes to run.
     * @param body code to execute
     */
    export function forever(action: () => void): void {
        if (!foreverRunning) {
            foreverRunning = true;
            control.runInParallel(() => {
                while (1) {
                    const handlers = game.currentScene().gameForeverHandlers;
                    handlers.forEach(h => {
                        if (!h.lock) {
                            h.lock = true;
                            control.runInParallel(() => {
                                h.handler();
                                h.lock = false;
                            });
                        }
                    });
                    pause(20);
                }
            });
        }

        game.currentScene().gameForeverHandlers.push(
            new scene.GameForeverHandler(action)
        );
    }

    /**
     * Draw on screen before sprites, after background
     * @param body code to execute
     */
    //% group="Gameplay"
    //% help=game/paint weight=10 afterOnStart=true
    export function onPaint(a: () => void): void {
        init();
        if (!a) return;
        scene.createRenderable(scene.ON_PAINT_Z, a);
    }

    /**
     * Draw on screen after sprites
     * @param body code to execute
     */
    //% group="Gameplay"
    //% help=game/shade weight=10 afterOnStart=true
    export function onShade(a: () => void): void {
        init();
        if (!a) return;
        scene.createRenderable(scene.ON_SHADE_Z, a);
    }


    /**
     * Register a handler that runs whenever a scene is pushed onto the scene
     * stack. Useful for extensions that need to store/restore state as the
     * event context changes. The handler is run AFTER the push operation (i.e.
     * after game.currentScene() has changed)
     *
     * @param handler Code to run when a scene is pushed onto the stack
     */
    export function addScenePushHandler(handler: (oldScene: scene.Scene) => void) {
        if (!_scenePushHandlers) _scenePushHandlers = [];
        if (_scenePushHandlers.indexOf(handler) < 0)
            _scenePushHandlers.push(handler);
    }

    /**
     * Remove a scene push handler. Useful for extensions that need to store/restore state as the
     * event context changes.
     *
     * @param handler The handler to remove
     */
    export function removeScenePushHandler(handler: (oldScene: scene.Scene) => void) {
        if (_scenePushHandlers) _scenePushHandlers.removeElement(handler);
    }

    /**
     * Register a handler that runs whenever a scene is popped off of the scene
     * stack. Useful for extensions that need to store/restore state as the
     * event context changes. The handler is run AFTER the pop operation. (i.e.
     * after game.currentScene() has changed)
     *
     * @param handler Code to run when a scene is removed from the top of the stack
     */
    export function addScenePopHandler(handler: (oldScene: scene.Scene) => void) {
        if (!_scenePopHandlers) _scenePopHandlers = [];
        if (_scenePopHandlers.indexOf(handler) < 0)
            _scenePopHandlers.push(handler);
    }

    /**
     * Remove a scene pop handler. Useful for extensions that need to store/restore state as the
     * event context changes.
     *
     * @param handler The handler to remove
     */
    export function removeScenePopHandler(handler: (oldScene: scene.Scene) => void) {
        if (_scenePopHandlers) _scenePopHandlers.removeElement(handler);
    }
}
