"use strict";

/**
 * @author nuark (github: NuarkNoir, tg: nuark)
 */

/**
 * Double PI, yeah
 */
const DOUBLE_PI = Math.PI * 2;

/**
 * Base class for dynamic objects
 */
class Dynamic {
    /** @type {Number} */ x;
    /** @type {Number} */ y;

    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    /**
     * Updates some data
     * @param {Nanvas} nanvas 
     */
    update(nanvas) {
        throw new Error("[Dynamic] Not implemented yet!");
    }

    /**
     * Draws dynamic and all
     * @param {Nanvas} nanvas 
     */
    draw(nanvas) {
        throw new Error("[Dynamic] Not implemented yet!");
    }
}

/**
 * Particle class
 */
class Particle extends Dynamic {
    /** @type {Number} */ id;
    /** @type {Number} */ x;
    /** @type {Number} */ y;
    /** @type {Boolean} */ alive = false;
    /** @type {Number} */ aliveFor = 0.0;
}

/**
 * Particle emitter class
 */
class ParticleEmmiter extends Dynamic {
    /** @type {Number} */ emitterSize = 20;
    /** @type {Number} */ poolSize = 10;
    /** @type {Number} */ particleLifeTime = 1.0 * 1000;
    /** @type {Number} */ spawnRate = 200.0;
    /** @type {Number} */ timeSinceLastSpawn = 0.0;

    /** @type {Array.<Particle>} */ pool = new Array(this.poolSize);

    /**
     * Constructs object
     * @param {Number} x position on x
     * @param {Number} y Position on y
     * @param {Number} dx Speed over x
     * @param {Number} dy Speed over y
     */
    constructor(x, y, dx, dy) {
        super(x, y);
        this.dx = dx;
        this.dy = dy;
        this.lastFrameTime = Date.now();

        for (let i = 0; i < this.poolSize; i++) {
            this.pool[i] = new Particle(this.x, this.y);
            this.pool[i].id = i;
        }
    }

    /**
     * Updates particles data
     * @param {Nanvas} nanvas 
     */
    update(nanvas) {
        const now = Date.now();
        const dt = now - this.lastFrameTime;
        this.lastFrameTime = now;
        this.timeSinceLastSpawn += dt;

        if (this.timeSinceLastSpawn >= this.spawnRate) {
            this.timeSinceLastSpawn = 0.0;
            for (let i = 0; i < this.poolSize; i++) {
                if (!this.pool[i].alive) {
                    this.pool[i].x = this.x + this.emitterSize/2;
                    this.pool[i].y = this.y + this.emitterSize/2;
                    this.pool[i].alive = true;
                    this.pool[i].aliveFor = 0.0;
                    console.log(`Spawned ${this.pool[i].id}`);
                    break;
                }
            }
        }
        
        for (let i = 0; i < this.poolSize; i++) {
            if (this.pool[i].alive) {
                this.pool[i].x += this.dx*dt;
                this.pool[i].y += this.dy*dt;
                this.pool[i].aliveFor += dt;
                if (this.pool[i].aliveFor >= this.particleLifeTime) {
                    this.pool[i].alive = false;
                    console.log(`Killed ${this.pool[i].id}`);
                }
            }
        }
    }

    /**
     * Draws emitter and particles
     * @param {Nanvas} nanvas 
     */
    draw(nanvas) {
        nanvas.square(this.x, this.y, this.emitterSize);
        for (let i = 0; i < this.poolSize; i++) {
            if (this.pool[i].alive) {
                nanvas.circle(this.pool[i].x, this.pool[i].y, 2);
            }
        }
    }
}

/**
 * Class for working with canvas
 */
class Nanvas {
    /** @type {CanvasRenderingContext2D} */
    ctx = null;

    /** @type {Number} */
    width;

    /** @type {Number} */
    height;

    /** @type {Array.<Array.<String, ...Number>>} */
    commands = [];

    /** @type {Array.<Dynamic>} */
    dynamics = [];

    /**
     * Pushes drawing style on stack
     */
    push() {
        this.ctx.save();
    }

    /**
     * Pops drawing style from stack
     */
    pop() {
        this.ctx.restore();
    }

    /**
     * Fill background with colour
     * @param {String} color Fill color
     */
    background(color) {
        this.push();

        this.ctx.fillStyle = color;
        this.ctx.fillRect(0, 0, this.width, this.height);

        this.pop();
    }

    /**
     * Clears canvas background
     */
    clear() {
        this.background("white");
    }

    /**
     * Draws nifty grid
     */
    grid() {
        this.push();

        this.ctx.beginPath();
        for (var x = 0; x < this.width; x += 10) {
            this.ctx.moveTo(x, 0);
            this.ctx.lineTo(x, this.height);
        }
        for (var y = 0; y < this.height; y += 10) {
            this.ctx.moveTo(0, y);
            this.ctx.lineTo(this.width, y);
        }
        this.ctx.strokeStyle = "#eee";
        this.ctx.stroke();
        
        this.pop();
    }

    /**
     * Pushes commands into canvas
     * @param {Array.<Array.<String, ...Number>>} commands Commands array
     */
    pushCommands(commands) {
        this.commands = commands;
    }

    /**
     * Clears canvas commands
     */
    clearCommands() {
        this.commands = [];
    }

    /**
     * Draws static objects (commands)
     */
    drawStatic() {
        this.commands.forEach(command => {
            let [cmd, ...args] = command;
            if (typeof (this[cmd]) !== typeof (() => { })) {
                throw new Error(`No such function: ${cmd}`);
            }
            this[cmd](...args);
        });
    }

    /**
     * Draws circle at postion `(x,y)` with radius `r`
     * @param {Number} x Position on x
     * @param {Number} y Position on y
     * @param {Number} r Radius
     */
    circle(x, y, r) {
        this.ctx.beginPath();
        this.ctx.arc(x, y, r, 0, DOUBLE_PI);
        this.ctx.stroke();
    }

    /**
     * Draws square at postion `(x,y)` with width `w`
     * @param {Number} x Position on x
     * @param {Number} y Position on y
     * @param {Number} r Width
     */
    square(x, y, w) {
        this.ctx.beginPath();
        this.ctx.rect(x, y, w, w);
        this.ctx.stroke();
    }

    /**
     * Draws Ellipse at postion `(x,y)` with radiais as `r1` and `r2`
     * @param {Number} x Position on x
     * @param {Number} y Position on y
     * @param {Number} r1 Radius 1
     * @param {Number} r2 Radius 2
     */
    ellipse(x, y, r1, r2) {
        this.ctx.beginPath();
        this.ctx.ellipse(x, y, r1, r2, 0, 0, DOUBLE_PI);
        this.ctx.stroke();
    }

    /**
     * Draws bezier curve with following parameters
     * @param {Number} h1x Position of first handle on x
     * @param {Number} h1y Position of first handle on y
     * @param {Number} h2x Position of second handle on x
     * @param {Number} h2y Position of second handle on y
     * @param {Number} x Position of end point on x
     * @param {Number} y Position of end point on y
     */
    bezier(h1x, h1y, h2x, h2y, x, y) {
        this.ctx.beginPath();
        this.ctx.bezierCurveTo(h1x, h1y, h2x, h2y, x, y);
        this.ctx.stroke();
    }

    /**
     * Add dynamic object to canvas
     * @param {Dynamic} dynamic Dynamic object
     */
    addDynamic(dynamic) {
        this.dynamics.push(dynamic);
    }

    /**
     * Updates and draws dynamics
     */
    updateDynamics() {
        this.dynamics.forEach(dyn => dyn.update(this));
        this.dynamics.forEach(dyn => dyn.draw(this));
    }

    /**
     * Mounts Nanvas on existing canvas element and fixes some things
     * @param {HTMLElement} canvasElement 
     * @returns {Nanvas}
     */
    static mount(canvasElement, width = null, height = null) {
        const n = new Nanvas();
        n.ctx = canvasElement.getContext("2d");
        // fixing blurrynessssssssssssssss
        n.ctx.translate(0.5, 0.5);
        n.width = width === null ? canvasElement.width : width;
        n.height = height === null ? canvasElement.height : height;
        return n;
    }
}