/* eslint-disable no-underscore-dangle */
import fs from 'fs';

/*
 * A board format FMT is a pair of two functions:
 *
 *   fromJboard : jboard -> string
 *   toJboard   : string -> jboard
 *
 * where "string" is a string in the given format, and
 * "jboard" is the internal format produced/consumed by
 * the RuntimeState class in src/runtime.js.
 *
 * Internal format:
 *   jboard.width  = width of the board
 *   jboard.height = height of the board
 *   jboard.head   = array [x, y] with the position of the head
 *   jboard.board  = array of <width> elements,
 *                   each of which is an array of <height> elements,
 *                   each of which is a cell, of the form
 *                     {"a": na, "n": nn, "r": nr, "v": nv}
 *                   in such a way that:
 *                     jboard.board[x][y].a = number of blue  stones at (x, y)
 *                     jboard.board[x][y].n = number of black stones at (x, y)
 *                     jboard.board[x][y].r = number of red   stones at (x, y)
 *                     jboard.board[x][y].v = number of green stones at (x, y)
 */

export interface JBoard {
    width: number;
    height: number;
    head: number[];
    board: { a: number; n: number; r: number; v: number }[][];
}

class BoardFormat {
    private _formatName: string;
    private _description: string;
    private _extension: string;
    private _toJboard: (board: string) => JBoard;
    private _fromJboard: (board: JBoard) => string;

    public constructor(
        formatName: string,
        description: string,
        extension: string,
        fromJboard,
        toJboard
    ) {
        this._formatName = formatName;
        this._description = description;
        this._extension = extension;
        this._fromJboard = fromJboard;
        this._toJboard = toJboard;
    }

    public get formatName(): string {
        return this._formatName;
    }

    public get description(): string {
        return this._description;
    }

    public get extension(): string {
        return this._extension;
    }

    public get fromJboard(): (board: JBoard) => string {
        return this._fromJboard;
    }

    public get toJboard(): (board: string) => JBoard {
        return this._toJboard;
    }
}

export interface Cell {
    blue?: number;
    black?: number;
    red?: number;
    green?: number;
}

export interface APIJBoard {
    width: number;
    height: number;
    head: { x: number; y: number };
    table: Cell[][];
}

export function apiboardFromJboard(jboard: JBoard): APIJBoard {
    const apiboard: APIJBoard = {
        head: { x: jboard.head[0], y: jboard.head[1] },
        width: jboard.width,
        height: jboard.height,
        table: []
    };
    for (let y = 0; y < jboard.height; y++) {
        const row: Cell[] = [];
        for (let x = 0; x < jboard.width; x++) {
            const cellO = jboard.board[x][y];
            const cell = {
                blue: cellO.a > 0 ? cellO.a : undefined,
                black: cellO.n > 0 ? cellO.n : undefined,
                red: cellO.r > 0 ? cellO.r : undefined,
                green: cellO.v > 0 ? cellO.v : undefined
            };
            if (cell.blue === undefined) delete cell.blue;
            if (cell.black === undefined) delete cell.black;
            if (cell.red === undefined) delete cell.red;
            if (cell.green === undefined) delete cell.green;
            row.push(cell);
        }
        apiboard.table.unshift(row);
    }
    return apiboard;
}

export function apiboardToJboard(apiboard: APIJBoard): JBoard {
    const jboard: JBoard = {
        head: [apiboard.head.x, apiboard.head.y],
        width: apiboard.width,
        height: apiboard.height,
        board: []
    };
    for (let x = 0; x < jboard.width; x++) {
        const column = [];
        for (let y = 0; y < jboard.height; y++) {
            const cell = apiboard.table[jboard.height - y - 1][x];
            const ca = 'blue' in cell ? cell.blue : 0;
            const cn = 'black' in cell ? cell.black : 0;
            const cr = 'red' in cell ? cell.red : 0;
            const cv = 'green' in cell ? cell.green : 0;
            column.push({
                a: ca,
                n: cn,
                r: cr,
                v: cv
            });
        }
        jboard.board.push(column);
    }
    return jboard;
}

export interface GSBoard {
    sizeX: number;
    sizeY: number;
    x: number;
    y: number;
    table: Cell[][];
}

function gsboardFromJboard(jboard: JBoard): string {
    const gsboard: GSBoard = {
        x: jboard.head[0],
        y: jboard.head[1],
        sizeX: jboard.width,
        sizeY: jboard.height,
        table: []
    };
    for (let y = 0; y < jboard.height; y++) {
        const row = [];
        for (let x = 0; x < jboard.width; x++) {
            const cell = jboard.board[x][y];
            row.push({
                blue: cell.a,
                black: cell.n,
                red: cell.r,
                green: cell.v
            });
        }
        gsboard.table.unshift(row);
    }
    return JSON.stringify(gsboard);
}

function gsboardToJboard(gsBoardString: string): JBoard {
    const gsboard = JSON.parse(gsBoardString);
    const jboard = {
        head: [gsboard.x, gsboard.y],
        width: gsboard.sizeX,
        height: gsboard.sizeY,
        board: []
    };
    for (let x = 0; x < jboard.width; x++) {
        const column = [];
        for (let y = 0; y < jboard.height; y++) {
            const cell = gsboard.table[jboard.height - y - 1][x];
            column.push({
                a: cell.blue,
                n: cell.black,
                r: cell.red,
                v: cell.green
            });
        }
        jboard.board.push(column);
    }
    return jboard;
}

export function gbbFromJboard(jboard: JBoard): string {
    const gbb = [];
    gbb.push('GBB/1.0');
    gbb.push(`size ${jboard.width.toString()} ${jboard.height.toString()}`);
    for (let y = 0; y < jboard.height; y++) {
        for (let x = 0; x < jboard.width; x++) {
            const cell = jboard.board[x][y];
            if (cell.a + cell.n + cell.r + cell.v === 0) {
                continue;
            }
            let c = 'cell ' + x.toString() + ' ' + y.toString();
            if (cell.a > 0) {
                c += ' Azul ' + cell.a.toString();
            }
            if (cell.n > 0) {
                c += ' Negro ' + cell.n.toString();
            }
            if (cell.r > 0) {
                c += ' Rojo ' + cell.r.toString();
            }
            if (cell.v > 0) {
                c += ' Verde ' + cell.v.toString();
            }
            gbb.push(c);
        }
    }
    gbb.push(`head ${jboard.head[0].toString()} ${jboard.head[1].toString()}`);
    return gbb.join('\n') + '\n';
}

export function gbbToJboard(gbb: string): JBoard {
    let i = 0;

    // DUMMY
    const jboard: JBoard = {
        width: 0,
        height: 0,
        head: [],
        board: []
    };

    const isWhitespace = (x: string): boolean =>
        x === ' ' || x === '\t' || x === '\r' || x === '\n';

    function isNumeric(str: string): boolean {
        for (const char of str) {
            if ('0123456789'.indexOf(char) === -1) {
                return false;
            }
        }
        return str.length > 0;
    }

    function skipWhitespace(): void {
        /* Skip whitespace */
        while (i < gbb.length && isWhitespace(gbb[i])) {
            i++;
        }
    }

    function readToken(): string {
        const t = [];
        skipWhitespace();
        while (i < gbb.length && !isWhitespace(gbb[i])) {
            t.push(gbb[i]);
            i++;
        }
        return t.join('');
    }

    function readN(errmsg: string): number {
        const t = readToken();
        if (!isNumeric(t)) {
            throw Error(errmsg);
        }
        const ti = parseInt(t, 10);
        if (ti < 0) {
            throw Error(errmsg);
        }
        return ti;
    }

    function readRange(a: number, b: number, errmsg: string): number {
        const t = readN(errmsg);
        if (t < a || t >= b) {
            throw Error(errmsg);
        }
        return t;
    }

    if (readToken() !== 'GBB/1.0') {
        throw Error('GBB/1.0: Board not in GBB/1.0 format.');
    }
    if (readToken() !== 'size') {
        throw Error('GBB/1.0: Board lacks a size declaration.');
    }
    jboard.width = readN('GBB/1.0: Board width is not a number.');
    jboard.height = readN('GBB/1.0: Board height is not a number.');
    if (jboard.width <= 0 || jboard.height <= 0) {
        throw Error('GBB/1.0: Board size should be positive.');
    }
    jboard.head = [0, 0];
    jboard.board = [];
    for (let w = 0; w < jboard.width; w++) {
        const row = [];
        for (let j = 0; j < jboard.height; j++) {
            row.push({ a: 0, n: 0, r: 0, v: 0 });
        }
        jboard.board.push(row);
    }

    let headDeclared = false;
    const colores = {
        Azul: 'a',
        A: 'a',
        Negro: 'n',
        N: 'n',
        Rojo: 'r',
        R: 'r',
        Verde: 'v',
        V: 'v'
    };

    while (i < gbb.length) {
        const op = readToken();
        if (op === '') {
            break;
        } else if (op === 'head') {
            if (headDeclared) {
                throw Error('GBB/1.0: Head position cannot be declared twice.');
            }
            headDeclared = true;
            const hx = readRange(0, jboard.width, 'GBB/1.0: Invalid head position.');
            const hy = readRange(0, jboard.height, 'GBB/1.0: Invalid head position.');
            jboard.head = [hx, hy];
        } else if (op === 'cell') {
            const cx = readRange(0, jboard.width, 'GBB/1.0: Invalid cell position.');
            const cy = readRange(0, jboard.height, 'GBB/1.0: Invalid cell position.');
            const colorDeclared = {};
            while (i < gbb.length) {
                const color = readToken();
                if (!(color in colores)) {
                    i -= color.length;
                    break;
                }
                const colorId = colores[color];
                if (colorId in colorDeclared) {
                    throw Error('GBB/1.0: Color cannot be declared twice.');
                }
                const n = readN('GBB/1.0: Invalid amount of stones.');
                jboard.board[cx][cy][colorId] = n;
            }
        } else {
            throw Error('GBB/1.0: Malformed board: unknown command "' + op + '".');
        }
    }
    return jboard;
}

const BOARD_FORMAT_LIST = [
    new BoardFormat(
        'jboard',
        'Representation of a board as a JavaScript object for internal usage.',
        'jboard',
        JSON.stringify,
        JSON.parse
    ),

    new BoardFormat(
        'gs-weblang-cli-json-board',
        `Representation of a board as a Javascript object used by the gs-weblang-cli tool.`,
        'json',
        gsboardFromJboard,
        gsboardToJboard
    ),

    new BoardFormat('gbb', 'GBB/1.0', 'gbb', gbbFromJboard, gbbToJboard)
];

export const DEFAULT_FORMAT = 'gs-weblang-cli-json-board';
export const BOARD_FORMATS = {};

for (const boardFormat of BOARD_FORMAT_LIST) {
    BOARD_FORMATS[boardFormat.formatName] = boardFormat;
}

function fileExtension(filename: string): string {
    const parts = filename.split('.');
    return parts[parts.length - 1];
}

function fileBoardFormat(filename: string): BoardFormat {
    const extension = fileExtension(filename);
    for (const fmt of BOARD_FORMAT_LIST) {
        if (extension === fmt.extension) {
            return fmt;
        }
    }
    return BOARD_FORMATS[DEFAULT_FORMAT];
}

export function readJboardFromFile(filename: string): JBoard {
    const format = fileBoardFormat(filename);
    const contents = fs.readFileSync(filename, 'utf8');
    return format.toJboard(contents);
}

export function writeJboardToFile(filename: string, jboard: JBoard): void {
    const format = fileBoardFormat(filename);
    const contents = format.fromJboard(jboard);
    fs.writeFileSync(filename, contents, 'utf8');
}
