/*
    This program is originated from http://nmi.jp/archives/386
    Copyright (c) 2012, Kihira Takuo. All rights reserved.

    Image resources are copied from the following web site:
    http://homepage2.nifty.com/hamcorossam/
    Copyright (c) 2012, HamCorossam. All rights reserved.
*/
import 'js/web.jsx';

final class Config {
    static const cols = 10;
    static const rows = 15;
    static const cellWidth  = 32;
    static const cellHeight = 32;
    static const bulletWidth  = 4;
    static const bulletHeight = 4;
    static const bulletSpeed = 20;
    static const reloadCount = 3;
    static const initialNumRocks = 5;

    static const FPS = 30;

    static const width  = Config.cols * Config.cellWidth;
    static const height = Config.rows * Config.cellHeight;

    static const imagePath = "img";
}

mixin Sprite {
    abstract var x : number;
    abstract var y : number;

    abstract var width : number;
    abstract var height : number;

    abstract var image : HTMLCanvasElement;

    function detectCollision(other : Sprite) : boolean {
        return Math.abs(this.x - other.x) < (Config.cellWidth  >> 1)
            && Math.abs(this.y - other.y) < (Config.cellHeight >> 1);

    }

    function draw(context : CanvasRenderingContext2D) : void {
        context.drawImage(this.image,
            this.x - (this.width  >> 1),
            this.y - (this.height >> 1));
    }
}

abstract class MovingObject implements Sprite {
    var x : number;
    var y : number;

    var dx : number;
    var dy : number;

    var image : HTMLCanvasElement;

    function constructor(x : number, y : number, dx : number, dy : number, image : HTMLCanvasElement) {
        this.x = x;
        this.y = y;
        this.dx = dx;
        this.dy = dy;
        this.image = image;
    }

    function update() : boolean {
        this.x += this.dx;
        this.y += this.dy;
        return this._inDisplay();
    }

    function _inDisplay() : boolean {
        return !( this.x <= 0 || this.x >= Config.width
            || this.y <= 0 || this.y >= Config.height);

    }
}

final class Bullet extends MovingObject {
    var width : number  = Config.bulletWidth;
    var height : number = Config.bulletHeight;

    function constructor(x : number, y : number, dx : number, dy : number, image : HTMLCanvasElement) {
        super(x, y, dx, dy, image);
    }

    function update(st : Stage) : boolean {
        var inDisplay = super.update();

        this.draw(st.ctx);

        for(var rockKey in st.rocks) {
            var rock = st.rocks[rockKey];

            if(this.detectCollision(rock)) {
                if(rock.hp == 0) return false;

                inDisplay = false;

                if(--rock.hp == 0) {
                    st.score = Math.min(st.score + rock.score, 999999999);

                    st.updateScore();

                    rock.dx = rock.dy = 0;
                    rock.setState(st, "bomb1");
                }
                else {
                    var newState = (rock.state +  "w").substring(0, 6);
                    rock.setState(st, newState);
                }
            }
        }
        return inDisplay;
    }
}

final class Rock extends MovingObject {
    var width  = Config.cellWidth;
    var height = Config.cellHeight;

    var hp : number;
    var score : number;
    var state : string;

    function constructor(
        x : number, y : number, dx : number, dy : number,
        hp : number, score : number, state : string,
        image : HTMLCanvasElement
        ) {
        super(x, y, dx, dy, image);
        this.hp = hp;
        this.score = score;
        this.state = state;
    }

    function update(st : Stage) : boolean {
        var inDisplay = super.update();

        this.draw(st.ctx);

        if(this.hp == 0) {
            var next = (this.state.substring(4) as int) + 1;
            if(next > 10) {
                return false;
            }
            else {
                this.setState(st, "bomb" + next as string);
            }
        }
        else {
            this.setState(st, this.state.substring(0, 5));

            if(st.isGaming() && this.detectCollision(st.ship)) {
                st.changeStateToBeDying();
                st.dying = 1;
            }
        }
        return inDisplay;
    }

    function setState(stage : Stage, state : string) : void {
        this.state = state;
        this.image = stage.images[state];
    }
}

final class SpaceShip implements Sprite {
    var x : number;
    var y : number;

    var width  = Config.cellWidth;
    var height = Config.cellHeight;

    var image : HTMLCanvasElement;

    function constructor(x : number, y : number, image : HTMLCanvasElement) {
        this.x = x;
        this.y = y;
        this.image = image;
    }
}

final class Stage {

    var imageName : Array.<string>;
    var images : Map.<HTMLCanvasElement>;

    var state = "loading";

    var ship : SpaceShip;
    var dying = 0;

    var lastX : number = -1;
    var lastY : number = -1;
    var frameCount : number = 0;
    var currentTop : number;

    var ctx   : CanvasRenderingContext2D;
    var bgCtx : CanvasRenderingContext2D;

    var bullets : Map.<Bullet>;

    var rocks : Map.<Rock>;
    var numRocks : number;

    var score : number;
    var scoreElement : HTMLElement;

    function changeStateToBeLoading() : void {
        this.state = "loading";
    }
    function isLoading() : boolean {
        return this.state == "loading";
    }

    function changeStateToBeGaming() : void {
        this.state = "gaming";
    }
    function isGaming() : boolean {
        return this.state == "gaming";
    }

    function changeStateToBeDying() : void {
        this.state = "dying";
    }
    function isDying() : boolean {
        return this.state == "dying";
    }

    function changeStateToBeGameOver() : void {
        this.state = "gameover";
    }
    function isGameOver() : boolean {
        return this.state == "gameover";
    }

    function level() : int {
        return this.frameCount / 500;
    }


    function drawBackground() : void {
        var bottom = Config.height + Config.cellHeight - this.currentTop;
        if(bottom > 0) {
            this.ctx.drawImage(this.bgCtx.canvas, 0, this.currentTop,
                Config.width, bottom, 0, 0, Config.width, bottom);
        }
        if(Math.abs(Config.height - bottom) > 0) {
            this.ctx.drawImage(this.bgCtx.canvas, 0, bottom);
        }
    }

    function draw() : void {
        this.drawBackground();

        var ship = this.ship;

        if(this.isGaming()) {
            ship.draw(this.ctx);
        }
        else if(this.isDying()) {
            ship.image = this.images["bomb" + this.dying as string];
            ship.draw(this.ctx);

            if(++this.dying > 10) {
                this.initialize(); // restart the game
                //this.changeStateToBeGameOver();
            }
        }
    }

    function drawSpace(px : number, py : number) : void {
        var spaceType = (Math.random() * 10 + 1) as int as string;
        var image = this.images["space" + spaceType];
        assert image != null;

        this.bgCtx.drawImage(image,
            px * Config.cellWidth,
            py * Config.cellHeight);
    }

    function createBullet(dx : number, dy : number) : Bullet {
        return new Bullet(
            this.ship.x, this.ship.y,
            dx * Config.bulletSpeed,
            dy * Config.bulletSpeed,
            this.images["bullet"]
        );
    }

    function createRock() : Rock {
        var level = this.level();

        var px = this.ship.x + Math.random() * 100 - 50;
        var py = this.ship.y + Math.random() * 100 - 50;
        var fx = Math.random() * Config.width;
        var fy = (level >= 4) ? (Math.random() * 2) * Config.height : 0;

        var r = Math.atan2(py - fy, px - fx);
        var d = Math.max(Math.random() * (5.5 + level) + 1.5, 10);

        var hp = (Math.random() * Math.random()
            * ((5 + level / 4) as int)) | 1;

        var rockId = (Math.random() * 3 + 1) as int as string;
        return new Rock(
            fx,
            fy,
            Math.cos(r) * d,
            Math.sin(r) * d,
            hp,
            hp * hp * 100,
            "rock" + rockId,
            this.images["rock" + rockId]
        );
    }


    function tick() : void {
        ++this.frameCount;

        dom.window.setTimeout(function() : void {
            this.tick();
        }, (1000 / Config.FPS) | 0);

        this.watchFPS();

        if(this.isLoading()) {
            return;
        }

        if(--this.currentTop == 0) {
            this.currentTop = Config.height + Config.cellHeight;
        }
        if( (this.currentTop % Config.cellHeight) == 0) {
            var line = this.currentTop / Config.cellHeight - 1;
            for(var px = 0; px < Config.cols; ++px) {
                this.drawSpace(px, line);
            }
        }

        this.draw();

        var fc = this.frameCount as string;
        if(this.isGaming() && (this.frameCount % Config.reloadCount) == 0) {
            this.bullets[fc + "a"] = this.createBullet(-1, -1);
            this.bullets[fc + "b"] = this.createBullet( 0, -1);
            this.bullets[fc + "c"] = this.createBullet( 1, -1);
            this.bullets[fc + "d"] = this.createBullet(-1,  1);
            this.bullets[fc + "e"] = this.createBullet( 1,  1);
        }

        if(this.numRocks < (Config.initialNumRocks + this.level())) {
            this.rocks[fc + "r"] = this.createRock();
            ++this.numRocks;
        }

        for(var bulletKey in this.bullets) {
            if(!this.bullets[bulletKey].update(this)) {
                delete this.bullets[bulletKey];
            }
        }

        for(var rockKey in this.rocks) {
            if(!this.rocks[rockKey].update(this)) {
                delete this.rocks[rockKey];
                --this.numRocks;
            }
        }
    }

    function initialize() : void {

        for(var px = 0; px < Config.cols; ++px) {
            for(var py = 0; py < Config.rows + 1; ++py) {
                this.drawSpace(px, py);
            }
        }

        for(var i = 0; i < 3; ++i) {
            var canvas = dom.createElement("canvas") as HTMLCanvasElement;

            canvas.width  = Config.cellWidth;
            canvas.height = Config.cellHeight;

            // prepare flashing rock images
            var rctx = canvas.getContext("2d") as CanvasRenderingContext2D;

            var k = "rock" + (i+1) as string;
            rctx.drawImage(this.images[k], 0, 0);
            rctx.globalCompositeOperation = "source-in";
            rctx.fillStyle = "#fff";
            rctx.fillRect(0, 0, canvas.width, canvas.height);
            this.images[k + "w"] = canvas;
        }

        this.currentTop = Config.height + Config.cellHeight;

        this.ship = new SpaceShip(
             Config.width >> 2,
            (Config.height * 3/4) as int,
            this.images["my"]);

        this.score      = 0;

        this.bullets = {} : Map.<Bullet>;
        this.rocks   = {} : Map.<Rock>;
        this.numRocks = 0;

        this.changeStateToBeGaming();

        dom.window.setTimeout(function() : void {
            dom.window.scrollTo(0, 0);
        }, 250);
    }

    function constructor(stageCanvas : HTMLCanvasElement, scoreboard : HTMLElement) {
        // initialize properties
        this.changeStateToBeLoading();

        this.imageName = ["my", "bullet", "rock1", "rock2", "rock3"];
        this.images    = {} : Map.<HTMLCanvasElement>;

        scoreboard.style.width = Config.width as string + "px";
        this.scoreElement = scoreboard;

        stageCanvas.width  = Config.width;
        stageCanvas.height = Config.height;
        this.ctx = stageCanvas.getContext("2d") as CanvasRenderingContext2D;

        var bg = dom.createElement("canvas") as HTMLCanvasElement;
        bg.width  = Config.width;
        bg.height = Config.height + Config.cellHeight;
        this.bgCtx = bg.getContext("2d") as CanvasRenderingContext2D;

        for(var i = 0; i < 10; ++i) {
            this.imageName.push("space" + (i + 1) as string);
            this.imageName.push("bomb"  + (i + 1) as string);
        }

        // preload
        var loadedCount = 0;
        var checkLoad = function(e : Event) : void {
            var image = e.target as HTMLImageElement;

            var canvas = dom.createElement("canvas") as HTMLCanvasElement;
            var cx = canvas.getContext("2d") as CanvasRenderingContext2D;
            cx.drawImage(image, 0, 0);
            this.images[image.name] = canvas;

            if(++loadedCount == this.imageName.length) {
                this.initialize();
            }
        };
        for(var i = 0; i < this.imageName.length; ++i) {
            var name = this.imageName[i];
            var image = dom.createElement("img") as HTMLImageElement;
            image.addEventListener("load", checkLoad);
            image.src = Config.imagePath + "/" + name + ".png";
            image.name = name;
        }

        var touchStart = function(e : Event) : void {
            e.preventDefault();

            var p = this.getPoint(e);

            this.lastX = p[0];
            this.lastY = p[1];

            if(this.isGameOver()) {
                this.initialize();
            }
        };

        var body = dom.window.document.body;

        body.addEventListener("mousedown",  touchStart);
        body.addEventListener("touchstart", touchStart);

        var touchMove = function(e : Event) : void {
            e.preventDefault();

            var p = this.getPoint(e);

            if(this.isGaming() && this.lastX != -1) {
                var ship = this.ship;
                ship.x += ((p[0] - this.lastX) * 2.5) as int;
                ship.y += ((p[1] - this.lastY) * 3.0) as int;

                ship.x = Math.max(ship.x, 0);
                ship.x = Math.min(ship.x, Config.width);

                ship.y = Math.max(ship.y, 0);
                ship.y = Math.min(ship.y, Config.height);
            }

            this.lastX = p[0];
            this.lastY = p[1];
        };

        body.addEventListener("mousemove", touchMove);
        body.addEventListener("touchmove", touchMove);
    }

    function getPoint(e : Event/*UIEvent*/) : number[] {
        var px = 0;
        var py = 0;
        if(e instanceof MouseEvent) {
            var me = e as MouseEvent;
            px = me.clientX;
            py = me.clientY;
        }
        else if(e instanceof TouchEvent) {
            var te = e as TouchEvent;
            px = te.touches[0].pageX;
            py = te.touches[0].pageY;
        }
        return [ px, py ];
    }

    var start = Date.now();
    var fps = 0;
    function watchFPS() : void {
        if((this.frameCount % Config.FPS) == 0) {
            this.fps = (this.frameCount / (Date.now() - this.start) * 1000) as int;
            this.updateScore();
        }
    }

    function updateScore() : void {
        var scoreStr = this.score as string;
        var fillz = "000000000".substring(
            0, 9 - scoreStr.length
        );
        this.scoreElement.innerHTML
            = fillz + scoreStr + "<br/>\n"
            + this.fps as string + " FPS";
    }

}

final class _Main {
    static function main(args : string[]) : void {
        var stageCanvas = dom.id(args[0]) as HTMLCanvasElement;
        var scoreboard = dom.id(args[1]);

        var stage = new Stage(stageCanvas, scoreboard);
        stage.tick();
    }
}

// vim: set expandtab:
