// Big in Japan
const util = require(__base+'core/util.js');
const discord = require(__base+'core/discord.js');
const config = require(__base+'core/config.js');
const storage = require(__base+'core/storage.js');
const { Thumbnail } = require('./helpers/canvas.js');
const Canvas = require('canvas');
const GIFEncoder = require('gifencoder');
const fs = require('fs');
const imagemin = require('imagemin');
const imageminGifsicle = require('imagemin-gifsicle');
const download = require('download');

const _commands = {};

const IMAGE_PATH = storage.getStoragePath('play.gif');

const RES = 1; // Output image scale
const WIDTH = 200;
const HEIGHT = 300;
const TOP_SPACE = 40; // Before pegs
const BOTTOM_SPACE = 50; // After pegs
const SLOTS = 6; // Deploy positions
const SLOT_WIDTH = WIDTH / (SLOTS + 1);
const GOALS = 8;
const GOAL_HEIGHT = 20;
const PEG_RADIUS = 2.5;
const BUMPER_RADIUS = 5;
const BALL_RADIUS = 9;
const PEG_SPACING = Math.ceil(PEG_RADIUS * 2 + BALL_RADIUS * 2.5);
const PEG_AREA_HEIGHT = HEIGHT - TOP_SPACE - BOTTOM_SPACE;
const PEG_AREA_WIDTH = WIDTH - PEG_SPACING * 2;

const GRAVITY = 0.012;
const FRICTION = 0.9;
const ELASTICITY = 0.7;

const FRAME_DIVIDER = 6;
const FPS = 30;
const MAX_SUBFRAMES = 150 * 7.5 * FRAME_DIVIDER; // Avg 150 frames per megabyte, max 7.5mb upload

const DIM_COLOR = '#888888';
const BORDER_COLOR = '#AAAAAA';
const PEG_COLOR = '#CCCCCC';
const DEFAULT_USER_COLOR = '#AAABAD';
const DEFAULT_BALL_COLOR = '#CCCCCC';
const BG_COLOR = '#202225';

const AVATAR_URL = 'https://cdn.discordapp.com/avatars/';

const SLOT_IMAGE = new Canvas(WIDTH * RES, TOP_SPACE * RES);

function drawSlots() {
    let slotCtx = SLOT_IMAGE.getContext('2d');
    slotCtx.strokeStyle = DIM_COLOR;
    slotCtx.lineWidth = RES * 3;
    slotCtx.lineCap = 'round';
    slotCtx.fillStyle = PEG_COLOR;
    slotCtx.font = `${Math.floor(TOP_SPACE * 0.45 * RES)}px Roboto`;
    slotCtx.textBaseline = 'top';
    slotCtx.textAlign = 'center';
    for(let s = 0; s < SLOTS; s++) {
        let x = Math.floor((s + 1) * SLOT_WIDTH * RES);
        slotCtx.beginPath();
        slotCtx.moveTo(x + 0.5 - SLOT_WIDTH * 0.2 * RES, TOP_SPACE * 0.61 * RES);
        slotCtx.lineTo(x + 0.5, TOP_SPACE * 0.75 * RES);
        slotCtx.lineTo(x + 0.5 + SLOT_WIDTH * 0.2 * RES, TOP_SPACE * 0.61 * RES);
        slotCtx.stroke();
        slotCtx.fillText((s + 1).toString(), x + RES - (s + 1 === 1 ? 2 : 0) * RES, 0);
    }
}
drawSlots();

let game = false;

function generateMap() {
    let pegs = [];
    let map = { width: WIDTH, height: HEIGHT, pegs };
    let pegRows = Math.floor(PEG_AREA_HEIGHT / PEG_SPACING) + 1;
    let pegRowHeight = Math.floor(PEG_AREA_HEIGHT / (pegRows - 1));
    let maxXPegs = Math.floor(PEG_AREA_WIDTH / PEG_SPACING) + 1;
    let prevXPegCount = 0;
    let prevBumpers = false;
    for(let r = 0; r < pegRows; r++) {
        let pegY = TOP_SPACE + pegRowHeight * r;
        let xPegCount;
        do {
            xPegCount = util.randomInt(Math.max(2, Math.ceil(maxXPegs * 2 / 3)), maxXPegs);
        } while(xPegCount % 2 === prevXPegCount % 2 || (r === 0 && xPegCount === SLOTS));
        prevXPegCount = xPegCount;
        let xPegWidth = util.random(PEG_SPACING, PEG_AREA_WIDTH / (xPegCount - 1));
        let xPegPadding = (WIDTH - (xPegCount - 1) * xPegWidth) / 2;
        if(xPegPadding > BUMPER_RADIUS + PEG_SPACING && !prevBumpers && r + 1 < pegRows) {
            pegs.push([0, pegY + pegRowHeight / 2, BUMPER_RADIUS]);
            pegs.push([WIDTH, pegY + pegRowHeight / 2, BUMPER_RADIUS]);
            prevBumpers = true;
        } else prevBumpers = false;
        for(let x = 0; x < xPegCount; x++) {
            pegs.push([Math.round(xPegPadding + xPegWidth * x), pegY, PEG_RADIUS]);
        }
    }
    return map;
}

function drawMap(map) {
    let canvas = new Canvas(WIDTH * RES, HEIGHT * RES);
    let ctx = canvas.getContext('2d');
    ctx.fillStyle = BG_COLOR;
    ctx.fillRect(0, 0, WIDTH * RES, HEIGHT * RES);
    ctx.fillStyle = PEG_COLOR;
    for(let [pegX, pegY, pegRadius] of map.pegs) {
        ctx.beginPath();
        ctx.arc(pegX * RES, pegY * RES, pegRadius * RES, 0, 2 * Math.PI);
        ctx.fill();
    }
    ctx.strokeStyle = BORDER_COLOR;
    ctx.lineWidth = RES;
    ctx.beginPath();
    ctx.moveTo(RES / 2, RES / 2);
    ctx.lineTo(WIDTH * RES - RES / 2, RES / 2);
    ctx.lineTo(WIDTH * RES - RES / 2, HEIGHT * RES - RES / 2);
    ctx.lineTo(RES / 2, HEIGHT * RES - RES / 2);
    ctx.closePath();
    ctx.stroke();
    return canvas;
}

let dotProduct = (a, b) => a.x * b.x + a.y * b.y;
let subtract = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
let multiply = (a, f) => ({ x: a.x * f, y: a.y * f });

function simulate(map, cb) {
    let frame = 0;
    let canvas = new Canvas(WIDTH * RES, HEIGHT * RES);
    let ctx = canvas.getContext('2d');
    let encoder = new GIFEncoder(WIDTH * RES, HEIGHT * RES);
    encoder.createReadStream().pipe(fs.createWriteStream(IMAGE_PATH));
    encoder.start();
    encoder.setRepeat(0);
    encoder.setDelay(Math.round(1000 / FPS));
    encoder.setQuality(4);
    //console.time('simulating result');
    //util.timer.reset();
    ctx.drawImage(map.image, 0, 0);
    ctx.beginPath();
    ctx.moveTo(RES, RES);
    ctx.lineTo(WIDTH * RES - RES, RES);
    ctx.lineTo(WIDTH * RES - RES, HEIGHT * RES - RES);
    ctx.lineTo(RES, HEIGHT * RES - RES);
    ctx.closePath();
    ctx.clip(); // Don't draw over map border
    let playersDone = 0;
    let slotRound = 0;
    let roundFrames = Math.ceil(Math.sqrt(2 * HEIGHT / GRAVITY)); // t = sqrt(2d/a)
    function nextRound() {
        if(slotRound + 1 > game.maxSlotStack) return;
        game.slots.forEach(slot => {
            if(game.players.has(slot[slotRound])) game.players.get(slot[slotRound]).status.falling = true;
        });
        slotRound++;
    }
    nextRound();
    while((playersDone < game.players.size || slotRound < game.maxSlotStack) && frame < MAX_SUBFRAMES) {
        //util.timer.start('drawing frame');
        let subFrame = frame % FRAME_DIVIDER;
        //util.timer.stop('drawing frame');
        game.players.forEach(({ p, v, status, color, avatarImg }, playerID) => {
            if(!status.falling) return;
            ctx.globalAlpha = subFrame === 0 ? 1 : Math.pow(0.5, FRAME_DIVIDER - subFrame);
            ctx.fillStyle = color;
            ctx.beginPath();
            ctx.arc(p.x * RES, p.y * RES, BALL_RADIUS * RES + RES, 0, 2 * Math.PI);
            ctx.fill();
            ctx.save();
            ctx.shadowColor = color;
            ctx.shadowBlur = 3 * RES;
            ctx.drawImage(avatarImg, (p.x - BALL_RADIUS) * RES, (p.y - BALL_RADIUS) * RES);
            ctx.restore();
            v.y += GRAVITY;
            if(p.x < BALL_RADIUS || p.x > WIDTH - BALL_RADIUS) {
                v.x *= -ELASTICITY;
                p.x = p.x < BALL_RADIUS ? BALL_RADIUS : WIDTH - BALL_RADIUS;
            }
            game.players.forEach(({ p: p2, v: v2, status: status2 }, player2ID) => {
                if(playerID === player2ID || !status2.falling) return;
                let playerCollisionDist = Math.pow(BALL_RADIUS * 2, 2);
                if(Math.abs(p.x - p2.x) > BALL_RADIUS * 2 || Math.abs(p.y - p2.y) > BALL_RADIUS * 2
                    || Math.pow(p.x - p2.x, 2) + Math.pow(p.y - p2.y, 2) > playerCollisionDist) return;
                let n = { x: p2.x - p.x, y: p2.y - p.y }; // Collision normal
                let rv = subtract(v, v2); // Relative velocity
                let dp = dotProduct(rv, n);
                if(dp <= 0) return; // Already moving away
                let u = multiply(n, dp / dotProduct(n, n)); // Perpendicular
                let u2 = multiply(n, -dp / dotProduct(n, n)); // Perpendicular
                let w = subtract(v, u); // Parallel
                let w2 = subtract(v2, u2); // Parallel
                let nv = subtract(multiply(w, FRICTION / 2), multiply(u, ELASTICITY / 2)); // New velocity
                let nv2 = subtract(multiply(w2, FRICTION / 2), multiply(u2, ELASTICITY / 2)); // New velocity
                v.x = nv.x; // Set new velocity
                v.y = nv.y;
                v2.x = nv2.x; // Set new velocity
                v2.y = nv2.y;
            });
            for(let [pegX, pegY, pegRadius] of map.pegs) { // Detect peg collisions
                let pegCollisionDist = Math.pow(pegRadius + BALL_RADIUS, 2);
                if(p.x + BALL_RADIUS <= pegX - pegRadius || p.x - BALL_RADIUS > pegX + pegRadius
                    || p.y + BALL_RADIUS <= pegY - pegRadius || p.y - BALL_RADIUS > pegY + pegRadius
                    || Math.pow(p.x - pegX, 2) + Math.pow(p.y - pegY, 2) > pegCollisionDist) continue;
                //console.log('old velocity:', v.x, v.y);
                let n = { x: pegX - p.x, y: pegY - p.y }; // Collision normal
                let dp = dotProduct(v, n);
                if(dp <= 0) return; // Already moving away
                let u = multiply(n, dp / dotProduct(n, n)); // Perpendicular
                let w = subtract(v, u); // Parallel
                let nv = subtract(multiply(w, FRICTION), multiply(u, ELASTICITY)); // New velocity
                v.x = nv.x; // Set new velocity
                v.y = nv.y;
                //console.log('new velocity:', v.x, v.y);
            }
        });
        game.players.forEach(({ p, v, status }) => {
            if(!status.falling) return;
            if(Math.abs(v.x) + Math.abs(v.y) <= GRAVITY) status.stillFrames++;
            else status.stillFrames = 0;
            p.x += v.x;
            p.y += v.y;
            if(p.y >= HEIGHT + BALL_RADIUS || status.stillFrames > 30) {
                playersDone++;
                status.falling = false;
            }
        });
        if(subFrame === 0) {
            encoder.addFrame(ctx);
            ctx.drawImage(map.image, 0, 0);
        }
        frame++;
        if(frame % roundFrames === 0) nextRound();
    }
    encoder.finish();
    //util.timer.results();
    //console.timeEnd('simulating result');
    //console.time('optimizing gif');
    imagemin([IMAGE_PATH], { use: [imageminGifsicle({ optimizationLevel: 2 })] }).then(() => {
        setTimeout(() => fs.readFile(IMAGE_PATH, cb), 300); // Delay file read
    }).catch(console.log);
}

_commands.pachinko = function(data) {
    game = false;
    if(game) return data.reply('There is already a pachinko game in progress!');
    game = { players: new Map(), channel: data.channel, slots: [], maxSlotStack: 0 };
    for(let i = 0; i <= SLOTS; i++) game.slots.push([]);
    game.map = generateMap();
    game.map.image = drawMap(game.map);
    let canvas = new Canvas(game.map.image.width, game.map.image.height);
    let ctx = canvas.getContext('2d');
    ctx.drawImage(game.map.image, 0, 0);
    ctx.drawImage(SLOT_IMAGE, 0, 0);
    discord.bot.uploadFile({
        to: data.channel, filename: `pachinko-${Date.now()}.png`, file: canvas.toBuffer(),
        message: `**__Pachinko!__**\nUse \`${config.prefixes[0]}p\` to choose a slot #`
    });
};

function testRun() {
    _commands.pachinko({
        channel: '209177876975583232',
        reply: (msg, polite, cb) => discord.sendMessage('209177876975583232', msg, polite, cb)
    });
}
// if(!discord.bot.connected) discord.bot.on('ready', testRun);
// else testRun();

module.exports = {
    listen(data) {
        if(!game || game.channel !== data.channel || data.command !== 'p') return;
        let slot = +data.paramStr;
        if(!(slot > 0 && slot <= SLOTS)) return data.reply(`Pick a slot from 1 to ${SLOTS}`);
        let user = discord.bot.servers[data.server].members[data.userID];
        let avatarImg = new Canvas(BALL_RADIUS * 2 * RES, BALL_RADIUS * 2 * RES);
        let avatarCtx = avatarImg.getContext('2d');
        game.players.set(data.userID, {
            slot, color: user.color !== null ? ('#' + user.color.toString(16)) : DEFAULT_USER_COLOR, avatarImg,
            v: { x: util.random(GRAVITY * -20, GRAVITY * 20), y: 0 },
            p: { x: slot * SLOT_WIDTH, y: 0 },
            status: { stillFrames: 0 }
        });
        for(let i = 0; i < 12; i++) {
            game.players.set('test' + i, {
                slot, color: '#eaa5f3', avatarImg,
                v: { x: util.random(GRAVITY * -20, GRAVITY * 20), y: 0 },
                p: { x: ((i % 6) + 1) * SLOT_WIDTH, y: 0 },
                status: { stillFrames: 0 }
            });
            game.slots[i % 6 + 1].push('test' + i);
        }
        game.slots[slot].push(data.userID);
        game.maxSlotStack = Math.max(game.maxSlotStack, game.slots[slot].length);
        avatarCtx.beginPath();
        avatarCtx.arc(BALL_RADIUS * RES, BALL_RADIUS * RES, BALL_RADIUS * RES, 0, 2 * Math.PI);
        let avatarURL = `${AVATAR_URL}${data.userID}/${user.avatar}.png`;
        download(avatarURL).then(imgData => {
            let img = new Canvas.Image;
            img.src = imgData;
            avatarCtx.clip();
            let resizedAvatar = new Thumbnail(img, BALL_RADIUS * 2 * RES, 3);
            avatarCtx.drawImage(resizedAvatar, 0, 0);
            simulate(game.map, (err, file) => {
                if(err) return console.log(err);
                discord.bot.uploadFile({
                    to: data.channel, filename: `pachinko-${Date.now()}.gif`, file
                });
            });
        }).catch(err => {
            console.log(err);
            avatarCtx.fillStyle = DEFAULT_BALL_COLOR;
            avatarCtx.fill();
        });
    },
    dev: true,
    commands: _commands
};
