class GameMusicNode {
    game: Game;
    soundPlayer: PH.SoundPlayer;
    track: AudioBuffer;
    gainNode: GainNode;

    constructor(game: Game, track:AudioBuffer, connectToNode: AudioNode, startTime: number) {
        this.game = game;
        this.soundPlayer = game.soundPlayer;
        this.track = track;

        let ac = this.soundPlayer.audioContext;
        this.gainNode = ac.createGain();
        this.gainNode.gain.value = 0;
        
        this.gainNode.connect(connectToNode);
        
        let source = ac.createBufferSource();
        source.buffer = track;
        source.loop = true;
        source.connect(this.gainNode);
        source.start(startTime);
    }
}

class GameMusicPlayer {
    game: Game;
    soundPlayer: PH.SoundPlayer;
    gainNode: GainNode;
    trackANode: GameMusicNode;
    trackBNode: GameMusicNode;
    trackDrumsNode: GameMusicNode;

    constructor(game: Game) {
        this.game = game;
        this.soundPlayer = game.soundPlayer;

        let ac = this.soundPlayer.audioContext;
        this.gainNode = ac.createGain();
        this.gainNode.gain.value = 0.5;

        this.gainNode.connect(this.soundPlayer.fader?.gainNode!);

        let startTime = 3;
        this.trackANode = new GameMusicNode(game, game.data['LD48'], this.gainNode, startTime);
        this.trackBNode = new GameMusicNode(game, game.data['LD48b'], this.gainNode, startTime);
        this.trackDrumsNode = new GameMusicNode(game, game.data['LD48drums'], this.gainNode, startTime);
    }

    setTargetGain(a: number, b: number, drums: number) {
        let startTime = this.soundPlayer.audioContext.currentTime + 0.2;
        let timeConstant = 1;
        this.trackANode.gainNode.gain.setTargetAtTime(a, startTime, timeConstant);
        this.trackBNode.gainNode.gain.setTargetAtTime(b, startTime, timeConstant);
        this.trackDrumsNode.gainNode.gain.setTargetAtTime(drums, startTime, timeConstant);
    }

}

class Game {

    data: { [key: string]: any };
    mainFont: PH.CanvasFont;
    ctx: CanvasRenderingContext2D;

    soundPlayer: PH.SoundPlayer;

    layerManager = new PH.LayerManager();

    gameLayer: GameLayer | null = null;

    constructor(data: { [key: string]: any }, audioContext: AudioContext, mainFont: PH.CanvasFont, ctx: CanvasRenderingContext2D, pixelationLayer: PH.PixelationLayer) {
        this.data = data;
        this.soundPlayer = new PH.SoundPlayer(audioContext, {});
        this.mainFont = mainFont;
        this.ctx = ctx;
        
    
        let spriteBoxButton = new PH.SpriteBox(this.ctx, this.data['buttonbox'], 4, 0);
        let spriteBoxPressed = new PH.SpriteBox(this.ctx, this.data['buttonbox'], 4, 1);
        let buttonDrawer = new PH.CanvasButtonSpriteDrawer(spriteBoxButton, spriteBoxPressed, mainFont);
        let uiLayer = new PH.CanvasUILayer();
        let mouseButtons = [0, 2]; // Handle mouse buttons 0 (LMB) and 2 (RMB)
        
        let b = new PH.CanvasButton(ctx, 142, 130, 36, 16, () => this.startGame(), "Start", buttonDrawer, mouseButtons);
        uiLayer.addButton(b);
        if(PH.localStorageIsSaved('save')) {
            let b = new PH.CanvasButton(ctx, 134, 150, 52, 16, () => this.continueGame(), "Continue", buttonDrawer, mouseButtons);
            uiLayer.addButton(b);
        }

        this.layerManager.setupMouseListeners(pixelationLayer.destCtx.canvas);
        this.layerManager.setupKeyboardListeners(window);

        this.layerManager.setMainLayers(new MenuLayer(this), uiLayer);
        this.layerManager.setTopLayers(pixelationLayer);

        this.gameLayer = new GameLayer(this);
        // Start animation frames.
        let fm = new PH.FrameManager({
            frameCallback: (deltat) => this.frame(deltat)
        });

        fm.start();
    }

    frame(deltat: number) {
        // Update step.
        this.layerManager.update(deltat);

        // Graphics step.
        PH.fillCanvas(this.ctx, "#000");
        this.layerManager.draw();
    }

    startGame() {
        this.gameLayer = new GameLayer(this);
        this.layerManager.setMainLayers(this.gameLayer);
    }

    continueGame() {
        this.startGame();
        this.gameLayer?.loadState();
    }

    endGame(numColl: number, numMsg: number) {
        this.layerManager.setMainLayers(new EndingLayer(this, numColl, numMsg));
    }

}

enum TileType {
    Space,
    Sky,
    Wall,
    Barrier,
    Panel,
    Source,
    Sink,
    Spikes,
    Ending
}

class GameTile {
    t: TileType;
    section: string;

    constructor(t: TileType, section: string) {
        this.t = t;
        this.section = section;
    }

}

class TextPoint {
    pos: [number, number];
    name: string;
    whisper: boolean;
    text: string;

    constructor(pos: [number, number], name: string, whisper: boolean, text: string) {
        this.pos = pos;
        this.name = name;
        this.whisper = whisper;
        this.text = text;
    }
}

class Treasure {
    pos: [number, number]
    name: string;

    constructor(pos: [number, number], name: string) {
        this.pos = pos;
        this.name = name;
    }
}

class GameMap {
    w: number;
    h: number;
    grid: GameTile[][];
    playerStartPos: [number, number];
    sectionSource: {[key: string]: [number, number]};
    savePointSections: {[key: string]: boolean};
    treasures: Treasure[];
    sectionHeightRange: {[key: string]: [number, number]};
    textPoints: TextPoint[];

    constructor(txt: string) {
        // Create a game map from a string containing CSV data.
        let lines = txt.split('\n');
        let result = []
        let playerStartPos: [number, number] | null = null;
        let savepointTiles: [number, number][] = [];
        this.treasures = [];
        this.textPoints = []

        for (let j = 0; j < lines.length; j++) {
            let pieces = lines[j].split(',');
            let curResultLine = [];
            for (let i = 0; i < pieces.length; i++) {
                let t: TileType;
                let tokens = pieces[i].trim().split(' ');
                for(let k = 0; k < tokens.length; k++) {
                    if(tokens[k] == 'text') {
                        let curText = tokens.slice(k + 1).join(' ');
                        this.textPoints.push(new TextPoint([i, j], '', false, curText));
                    }
                    else if(tokens[k] == 'whisper') {
                        let wName = tokens[k + 1];
                        let curText = tokens.slice(k + 2).join(' ');
                        this.textPoints.push(new TextPoint([i, j], wName, true, curText));
                        if(k == 0) {
                            tokens.splice(0, 0, '');
                            break;
                        }
                    }
                }
                let c = tokens[0];
                let section = '';
                if (c == '') {
                    t = TileType.Space;
                }
                else if (c == 'section') {
                    t = TileType.Space;
                    section = tokens[1];
                }
                else if(c == 'save') {
                    t = TileType.Space;
                    savepointTiles.push([i, j]);
                }
                else if (c == 'k') { // Treasure
                    t = TileType.Space;
                    if(tokens.length < 2) {
                        throw new Error("Treasure without name at " + [i, j].toString());
                    }
                    let tName = tokens[1];
                    this.treasures.push(new Treasure([i, j], tName));
                }
                else if(c == 'y') { // Sky
                    t = TileType.Sky;
                }
                else if (c == 'x') { // wall
                    t = TileType.Wall;
                }
                else if (c == 'b') { // barrier
                    t = TileType.Barrier;
                    section = tokens[1];
                }
                else if (c == '@') { // player start position
                    let typeAbove = result[j-1][i].t;
                    if(typeAbove === TileType.Sky) {
                        t = TileType.Sky;
                    }
                    else {
                        t = TileType.Space;
                    }
                    if (playerStartPos !== null) {
                        throw new Error("Player start position is not unique.");
                    }
                    playerStartPos = [i, j];
                }
                else if (c == 'p') { // panel
                    t = TileType.Panel;
                }
                else if (c == 's') { // source
                    t = TileType.Source;
                }
                else if (c == 'e') { // sink
                    t = TileType.Sink;
                }
                else if (c == 'm') { // spikes
                    t = TileType.Spikes;
                }
                else if(c == 'end') { // Ending
                    t = TileType.Ending;
                }
                else {
                    throw new Error("Unknown tile character: " + c + " at " + [i, j].toString());
                }
                curResultLine.push(new GameTile(t, section));
            }
            if (curResultLine.length > 1) {
                result.push(curResultLine);
            }
        }
        this.grid = result;
        this.h = result.length;
        this.w = result[0].length;
        if (playerStartPos == null) {
            throw new Error("Player start position not placed.");
        }
        else {
            this.playerStartPos = playerStartPos;
        }

        // compute the sections for each tile, by flood-fill
        let numberSectionMap: {[key: number]: string} = {};
        let islands = getIslands(this);
        for(let j = 0; j < this.h; j++) {
            for(let i = 0; i < this.w; i++) {
                let s = this.grid[j][i].section;
                if(s !== '' && islands[j][i] !== null) {
                    if(islands[j][i] in numberSectionMap) {
                        throw new Error("Tried to specify the section name twice.");
                    }
                    else {
                        numberSectionMap[islands[j][i]] = s;
                    }
                }
            }
        }
        for(let j = 0; j < this.h; j++) {
            for(let i = 0; i < this.w; i++) {
                if(islands[j][i] != null) {
                    if(!(islands[j][i] in numberSectionMap)) {
                        throw new Error("Section with no section name.");
                    }
                    if(this.grid[j][i].section === '') {
                        this.grid[j][i].section = numberSectionMap[islands[j][i]];
                    }
                    if(this.grid[j][i].section === '') {
                        throw new Error("Failed to assign a section.");
                    }
                }
            }
        }

        // Compute the height range for each section
        let shr: {[key: string]: [number, number]} = {};
        for(let j = 0; j < this.h; j++) {
            for(let i = 0; i < this.w; i++) {
                let s = this.grid[j][i].section;
                if(s != '') {
                    if(!(s in shr)) {
                        shr[s] = [j, j];
                    }
                    else {
                        shr[s][1] = j;
                    }
                }
            }
        }
        this.sectionHeightRange = shr;

        // Keep record of which sections are save points
        this.savePointSections = {};
        for(let k = 0; k < savepointTiles.length; k++) {
            let tc = savepointTiles[k]
            this.savePointSections[this.grid[tc[1]][tc[0]].section] = true;
        }

        // Keep record of where the source is for each section
        this.sectionSource = {};
        let sectionSink: {[key: string]: [number, number]} = {};
        for(let j = 0; j < this.h; j++) {
            for(let i = 0; i < this.w; i++) {
                let tile = this.grid[j][i];
                if(tile.t === TileType.Source) {
                    if(tile.section in this.sectionSource) {
                        throw new Error("Not designed to handle multiple sources in a section.");
                    }
                    this.sectionSource[tile.section] = [i, j];
                }
                if(tile.t === TileType.Sink) {
                    if(tile.section in sectionSink) {
                        throw new Error("Not designed to handle multiple sinks in a section.");
                    }
                    sectionSink[tile.section] = [i, j];
                }
            }
        }
    }
}

function loadMap(buffer: ArrayBuffer): GameMap {
    // Load a map for this game
    let decoder = new TextDecoder();
    let txt = decoder.decode(buffer);
    return new GameMap(txt);
}

async function start() {
    window.onerror = null;
    let mainGameCanvas = PH.createCanvas(320, 200);
    let outGameCanvas = <HTMLCanvasElement>document.getElementById('outGameCanvas')!;
    outGameCanvas.hidden = false;
    let audioContext = new AudioContext();
    let loader = new PH.Loader(audioContext);
    loader.addExtensionHandler("csv", loadMap);
    let ctx = mainGameCanvas.getContext('2d')!;
    let outCtx = outGameCanvas.getContext('2d')!;
    let pixelationLayer = new PH.PixelationLayer(ctx, outCtx, true, false);

    // Load a font
    let mainFont = <PH.PixelFont>await loader.getFile('m5x7.bff');
    mainFont.img = PH.changeImageColor(mainFont.img, [200, 200, 200]);
    mainFont.yOffset = -4;
    mainFont.lineHeight = 10;

    // Set up loading sceen
    let loadingScreen = new LoadingScreen(loader, ctx, mainFont);

    // Start animation frames for while the game is loading.
    let fm = new PH.FrameManager({
        frameCallback: (deltat) => {
            pixelationLayer.update(deltat);
            PH.fillCanvas(ctx, "#000");
            loadingScreen.draw();
            pixelationLayer.draw();
        }
    });
    fm.start();

    // Load the main contents of the game.
    let data = await loader.getFile('game.dat', (bytes, totalBytes) => loadingScreen.setProgress(bytes, totalBytes));

    fm.stop();

    new Game(data, audioContext, mainFont, ctx, pixelationLayer);
}

window.onload = start;
