/* eslint-disable no-underscore-dangle */
import { i18n } from './i18n';
import {
    ValueInteger,
    ValueList,
    ValueStructure,
    TypeAny,
    TypeInteger,
    TypeString,
    TypeStructure,
    TypeList,
    joinTypes,
    Type,
    Value
} from './value';
import { GbsRuntimeError, SourceReader } from '@gobstones/gobstones-parser';
import { JBoard } from './board_formats';

/*
 * This module provides the runtime support for the execution of a program.
 *
 * The runtime support includes:
 *
 * - A definition of a class RuntimeState, representing the global state
 *   of a program.
 *
 * - A definition of a class RuntimePrimitives, representing the available
 *   primitive functions.
 *
 * This file is a particular implementation, in which RuntimeState
 * represents a Gobstones board, and RuntimePrimitives are the primitives
 * functions and procedures available in Gobstones.
 *
 * Potential variants of the language might have a different notion of
 * global state, and different available primitives.
 */

function fail(startPos: SourceReader, endPos: SourceReader, reason: string, args: any[]): void {
    throw new GbsRuntimeError(startPos, endPos, reason, args);
}

const boolEnum = (): string[] => [i18n('CONS:False'), i18n('CONS:True')];

const colorEnum = (): string[] => [
    i18n('CONS:Color0'),
    i18n('CONS:Color1'),
    i18n('CONS:Color2'),
    i18n('CONS:Color3')
];

const dirEnum = (): string[] => [
    i18n('CONS:Dir0'),
    i18n('CONS:Dir1'),
    i18n('CONS:Dir2'),
    i18n('CONS:Dir3')
];

/* Enumeration of all the constructors of the Event type, including
 * INIT and TIMEOUT. */
function keyEventEnum(): string[] {
    const modifiers = [
        '',
        'CTRL_',
        'ALT_',
        'SHIFT_',
        'CTRL_ALT_',
        'CTRL_SHIFT_',
        'ALT_SHIFT_',
        'CTRL_ALT_SHIFT_'
    ];
    const charKeys = [
        'A',
        'B',
        'C',
        'D',
        'E',
        'F',
        'G',
        'H',
        'I',
        'J',
        'K',
        'L',
        'M',
        'N',
        'O',
        'P',
        'Q',
        'R',
        'S',
        'T',
        'U',
        'V',
        'W',
        'X',
        'Y',
        'Z',
        '0',
        '1',
        '2',
        '3',
        '4',
        '5',
        '6',
        '7',
        '8',
        '9'
    ];
    const specialKeys = [
        'SPACE',
        'RETURN',
        'TAB',
        'BACKSPACE',
        'ESCAPE',
        'INSERT',
        'DELETE',
        'HOME',
        'END',
        'PAGEUP',
        'PAGEDOWN',
        'F1',
        'F2',
        'F3',
        'F4',
        'F5',
        'F6',
        'F7',
        'F8',
        'F9',
        'F10',
        'F11',
        'F12'
    ];
    const symbolKeys = [
        'AMPERSAND',
        'ASTERISK',
        'AT',
        'BACKSLASH',
        'CARET',
        'COLON',
        'DOLLAR',
        'EQUALS',
        'EXCLAIM',
        'GREATER',
        'HASH',
        'LESS',
        'PERCENT',
        'PLUS',
        'SEMICOLON',
        'SLASH',
        'QUESTION',
        'QUOTE',
        'QUOTEDBL',
        'UNDERSCORE',
        'LEFTPAREN',
        'RIGHTPAREN',
        'LEFTBRACKET',
        'RIGHTBRACKET',
        'LEFTBRACE',
        'RIGHTBRACE'
    ];
    const arrowKeys = ['LEFT', 'RIGHT', 'UP', 'DOWN'];
    const keys = charKeys.concat(specialKeys).concat(symbolKeys).concat(arrowKeys);

    const eventNames = [];
    for (const modifier of modifiers) {
        for (const key of keys) {
            eventNames.push('K_' + modifier + key);
        }
    }
    return eventNames;
}

const KEY_EVENT_ENUM = keyEventEnum();

const eventEnum = (): string[] => [i18n('CONS:INIT'), i18n('CONS:TIMEOUT')].concat(KEY_EVENT_ENUM);

const toEnum = (enumeration: string[], name: string): number => enumeration.indexOf(name);

const fromEnum = (enumeration: string[], index: number): string => enumeration[index];

const dirOpposite = (dirName: string): string =>
    fromEnum(dirEnum(), (toEnum(dirEnum(), dirName) + 2) % 4);

const dirNext = (dirName: string): string =>
    fromEnum(dirEnum(), (toEnum(dirEnum(), dirName) + 1) % 4);

const dirPrev = (dirName: string): string =>
    fromEnum(dirEnum(), (toEnum(dirEnum(), dirName) + 3) % 4);

const colorNext = (colorName: string): string =>
    fromEnum(colorEnum(), (toEnum(colorEnum(), colorName) + 1) % 4);

const colorPrev = (colorName: string): string =>
    fromEnum(colorEnum(), (toEnum(colorEnum(), colorName) + 3) % 4);

/*
 * An instance of RuntimeState represents the current global state of
 * a program. In the case of Gobstones, it is a Gobstones board.
 *
 * It MUST implement the following methods:
 *
 *   this.clone() ~~> returns a copy of the state
 *
 */
export class RuntimeState {
    private _width: number;
    private _height: number;
    private _board: Record<string, ValueInteger>[][];
    private _head: { x: number; y: number };
    public constructor() {
        /*
         * The board is represented as a list of columns, so that board[x] is the
         * x-th column and board[x][y] is the cell at (x, y).
         *
         * By default, create an empty 9x9 board.
         */
        this._width = 11;
        this._height = 7;
        this._board = [];
        for (let x = 0; x < this._width; x++) {
            const column = [];
            for (let y = 0; y < this._height; y++) {
                column.push(this._emptyCell());
            }
            this._board.push(column);
        }
        this._head = { x: 0, y: 0 };
    }

    public clone(): RuntimeState {
        const newState = new RuntimeState();
        newState._width = this._width;
        newState._height = this._height;
        newState._board = [];
        for (let x = 0; x < this._width; x++) {
            const column = [];
            for (let y = 0; y < this._height; y++) {
                const cell = {};
                for (const colorName of colorEnum()) {
                    cell[colorName] = this._board[x][y][colorName];
                }
                column.push(cell);
            }
            newState._board.push(column);
        }
        newState._head = { x: this._head.x, y: this._head.y };
        return newState;
    }

    /* Dump the state to a Jboard data structure */
    public dump(): JBoard {
        const jboard: JBoard = {
            head: [],
            width: 0,
            height: 0,
            board: []
        };
        jboard.width = this._width;
        jboard.height = this._height;
        jboard.head = [this._head.x, this._head.y];
        jboard.board = [];
        for (let x = 0; x < this._width; x++) {
            const column = [];
            for (let y = 0; y < this._height; y++) {
                const cell = {};
                cell['a'] = this._board[x][y][i18n('CONS:Color0')].asNumber();
                cell['n'] = this._board[x][y][i18n('CONS:Color1')].asNumber();
                cell['r'] = this._board[x][y][i18n('CONS:Color2')].asNumber();
                cell['v'] = this._board[x][y][i18n('CONS:Color3')].asNumber();
                column.push(cell);
            }
            jboard.board.push(column);
        }
        return jboard;
    }

    /* Load the state from a Jboard data structure */
    public load(jboard: JBoard): void {
        this._width = jboard.width;
        this._height = jboard.height;
        this._head.x = jboard.head[0];
        this._head.y = jboard.head[1];
        this._board = [];
        for (let x = 0; x < this._width; x++) {
            const row = [];
            for (let y = 0; y < this._height; y++) {
                const cell = jboard.board[x][y];
                const newCell = {};
                newCell[i18n('CONS:Color0')] = new ValueInteger(cell['a']);
                newCell[i18n('CONS:Color1')] = new ValueInteger(cell['n']);
                newCell[i18n('CONS:Color2')] = new ValueInteger(cell['r']);
                newCell[i18n('CONS:Color3')] = new ValueInteger(cell['v']);
                row.push(newCell);
            }
            this._board.push(row);
        }
    }

    /* Gobstones specific methods */

    public putStone(colorName: string): void {
        let n = this._board[this._head.x][this._head.y][colorName];
        n = n.add(new ValueInteger(1));
        this._board[this._head.x][this._head.y][colorName] = n;
    }

    public removeStone(colorName: string): void {
        let n = this._board[this._head.x][this._head.y][colorName];
        if (n.le(new ValueInteger(0))) {
            throw Error('Cannot remove stone.');
        }
        n = n.sub(new ValueInteger(1));
        this._board[this._head.x][this._head.y][colorName] = n;
    }

    public numStones(colorName: string): ValueInteger {
        return this._board[this._head.x][this._head.y][colorName];
    }

    public move(dirName: string): void {
        if (!this.canMove(dirName)) {
            throw Error('Cannot move.');
        }
        const delta = this._deltaForDirection(dirName);
        this._head.x += delta[0];
        this._head.y += delta[1];
    }

    public goToEdge(dirName: string): void {
        if (dirName === i18n('CONS:Dir0')) {
            this._head.y = this._height - 1;
        } else if (dirName === i18n('CONS:Dir1')) {
            this._head.x = this._width - 1;
        } else if (dirName === i18n('CONS:Dir2')) {
            this._head.y = 0;
        } else if (dirName === i18n('CONS:Dir3')) {
            this._head.x = 0;
        } else {
            throw Error('Invalid direction: ' + dirName);
        }
    }

    public emptyBoardContents(): void {
        for (let x = 0; x < this._width; x++) {
            for (let y = 0; y < this._height; y++) {
                this._board[x][y] = this._emptyCell();
            }
        }
    }

    public canMove(dirName: string): boolean {
        const delta = this._deltaForDirection(dirName);
        const x: number = this._head.x + delta[0];
        const y: number = this._head.y + delta[1];
        return x >= 0 && x < this._width && y >= 0 && y < this._height;
    }

    public _deltaForDirection(dirName: string): number[] {
        let delta;
        if (dirName === i18n('CONS:Dir0')) {
            delta = [0, 1];
        } else if (dirName === i18n('CONS:Dir1')) {
            delta = [1, 0];
        } else if (dirName === i18n('CONS:Dir2')) {
            delta = [0, -1];
        } else if (dirName === i18n('CONS:Dir3')) {
            delta = [-1, 0];
        } else {
            throw Error('Invalid direction: ' + dirName);
        }
        return delta;
    }

    public _emptyCell(): Record<string, ValueInteger> {
        const cell = {};
        for (const colorName of colorEnum()) {
            cell[colorName] = new ValueInteger(0);
        }
        return cell;
    }
}

class PrimitiveOperation {
    private _argumentTypes: Type[];
    // eslint-disable-next-line @typescript-eslint/ban-types
    private _argumentValidator: Function;
    // eslint-disable-next-line @typescript-eslint/ban-types
    private _implementation: Function;

    public constructor(argumentTypes: Type[], argumentValidator, implementation) {
        this._argumentTypes = argumentTypes;
        this._argumentValidator = argumentValidator;
        this._implementation = implementation;
    }

    public get argumentTypes(): Type[] {
        return this._argumentTypes;
    }

    public nargs(): number {
        return this._argumentTypes.length;
    }

    public call(globalState: RuntimeState, args: any[]): any {
        return this._implementation.apply(undefined, [globalState].concat(args));
    }

    /* Check that the arguments are valid according to the validator.
     * The validator should be a function receiving a start and end
     * positions, and a list of arguments.
     * It should throw a GbsRuntimeError if the arguments are invalid.
     */
    public validateArguments(
        startPos: SourceReader,
        endPos: SourceReader,
        globalState: RuntimeState,
        args: any[]
    ): void {
        this._argumentValidator(startPos, endPos, globalState, args);
    }
}

/* Casting Gobstones values to JavaScript values and vice-versa */

const typeAny = new TypeAny();

const typeInteger = new TypeInteger();

const typeString = new TypeString();

const typeBool = (): TypeStructure => new TypeStructure(i18n('TYPE:Bool'), {});

const typeListAny = new TypeList(new TypeAny());

function valueFromBool(bool: boolean): ValueStructure {
    if (bool) {
        return new ValueStructure(i18n('TYPE:Bool'), i18n('CONS:True'), {});
    } else {
        return new ValueStructure(i18n('TYPE:Bool'), i18n('CONS:False'), {});
    }
}

export const boolFromValue = (value: ValueStructure): boolean =>
    value.constructorName === i18n('CONS:True');

const typeColor = (): TypeStructure => new TypeStructure(i18n('TYPE:Color'), {});

const valueFromColor = (colorName: string): ValueStructure =>
    new ValueStructure(i18n('TYPE:Color'), colorName, {});

const colorFromValue = (value: ValueStructure): string => value.constructorName;

const typeDir = (): TypeStructure => new TypeStructure(i18n('TYPE:Dir'), {});

const valueFromDir = (dirName: string): ValueStructure =>
    new ValueStructure(i18n('TYPE:Dir'), dirName, {});

const dirFromValue = (value: ValueStructure): string => value.constructorName;

/* Argument validators */

function noValidation(): void {
    /* No validation */
}

const isInteger = (x: Value): boolean => joinTypes(x.type(), typeInteger) !== undefined;

const isBool = (x: Value): boolean => joinTypes(x.type(), typeBool()) !== undefined;

const isColor = (x: Value): boolean => joinTypes(x.type(), typeColor()) !== undefined;

const isDir = (x: Value): boolean => joinTypes(x.type(), typeDir()) !== undefined;

export const typesWithOpposite = (): Type[] => [typeInteger, typeBool(), typeDir()];

export const typesWithOrder = (): Type[] => [typeInteger, typeBool(), typeColor(), typeDir()];

/* Generic operations */

function enumIndex(value: ValueStructure): number {
    if (isBool(value)) {
        if (boolFromValue(value)) {
            return 1;
        } else {
            return 0;
        }
    } else if (isColor(value)) {
        return toEnum(colorEnum(), colorFromValue(value));
    } else if (isDir(value)) {
        return toEnum(dirEnum(), dirFromValue(value));
    } else {
        throw Error('Value should be Bool, Color or Dir.');
    }
}

function genericLE(a: Value, b: Value): ValueStructure {
    if (isInteger(a)) {
        return valueFromBool((a as ValueInteger).le(b as ValueInteger));
    } else {
        const indexA = enumIndex(a as ValueStructure);
        const indexB = enumIndex(b as ValueStructure);
        return valueFromBool(indexA <= indexB);
    }
}

function genericGE(a: Value, b: Value): ValueStructure {
    if (isInteger(a)) {
        return valueFromBool((a as ValueInteger).ge(b as ValueInteger));
    } else {
        const indexA = enumIndex(a as ValueStructure);
        const indexB = enumIndex(b as ValueStructure);
        return valueFromBool(indexA >= indexB);
    }
}

function genericLT(a: Value, b: Value): ValueStructure {
    if (isInteger(a)) {
        return valueFromBool((a as ValueInteger).lt(b as ValueInteger));
    } else {
        const indexA = enumIndex(a as ValueStructure);
        const indexB = enumIndex(b as ValueStructure);
        return valueFromBool(indexA < indexB);
    }
}

function genericGT(a: Value, b: Value): ValueStructure {
    if (isInteger(a)) {
        return valueFromBool((a as ValueInteger).gt(b as ValueInteger));
    } else {
        const indexA = enumIndex(a as ValueStructure);
        const indexB = enumIndex(b as ValueStructure);
        return valueFromBool(indexA > indexB);
    }
}

function genericNext(a: Value): Value {
    if (isInteger(a)) {
        return (a as ValueInteger).add(new ValueInteger(1));
    } else if (isBool(a)) {
        if (boolFromValue(a as ValueStructure)) {
            return valueFromBool(false);
        } else {
            return valueFromBool(true);
        }
    } else if (isColor(a)) {
        return valueFromColor(colorNext(colorFromValue(a as ValueStructure)));
    } else if (isDir(a)) {
        return valueFromDir(dirNext(dirFromValue(a as ValueStructure)));
    } else {
        throw Error('genericNext: value has no next.');
    }
}

function genericPrev(a: Value): Value {
    if (isInteger(a)) {
        return (a as ValueInteger).sub(new ValueInteger(1));
    } else if (isBool(a)) {
        if (boolFromValue(a as ValueStructure)) {
            return valueFromBool(false);
        } else {
            return valueFromBool(true);
        }
    } else if (isColor(a)) {
        return valueFromColor(colorPrev(colorFromValue(a as ValueStructure)));
    } else if (isDir(a)) {
        return valueFromDir(dirPrev(dirFromValue(a as ValueStructure)));
    } else {
        throw Error('genericPrev: value has no prev.');
    }
}

function genericOpposite(a: Value): Value {
    if (isInteger(a)) {
        return (a as ValueInteger).negate();
    } else if (isBool(a)) {
        return valueFromBool(!boolFromValue(a as ValueStructure));
    } else if (isDir(a)) {
        return valueFromDir(dirOpposite(dirFromValue(a as ValueStructure)));
    } else {
        throw Error('genericOpposite: value has no opposite.');
    }
}
/* Validate that the type of 'x' is among the given list of types */
function validateTypeAmong(
    startPos: SourceReader,
    endPos: SourceReader,
    x: Value,
    types: Type[]
): void {
    /* Succeed if the type of x is in the list 'types' */
    for (const type of types) {
        if (joinTypes(x.type(), type) !== undefined) {
            return;
        }
    }
    /* Report error */
    fail(startPos, endPos, 'expected-value-of-some-type-but-got', [types, x.type()]);
}

/* Validate that the types of 'x' and 'y' are compatible */
function validateCompatibleTypes(
    startPos: SourceReader,
    endPos: SourceReader,
    x: Value,
    y: Value
): void {
    if (joinTypes(x.type(), y.type()) === undefined) {
        fail(startPos, endPos, 'expected-values-to-have-compatible-types', [x.type(), y.type()]);
    }
}

/* Runtime primitives */

export class RuntimePrimitives {
    private _primitiveTypes: Record<string, Record<string, string[]>>;
    private _primitiveProcedures: Record<string, PrimitiveOperation>;
    private _primitiveFunctions: Record<string, PrimitiveOperation>;

    public constructor() {
        /* this._primitiveTypes is a dictionary indexed by type names.
         *
         * this._primitiveTypes[typeName] is a dictionary indexed by
         * the constructor names of the given type.
         *
         * this._primitiveTypes[typeName][constructorName]
         * is a list of field names.
         */
        this._primitiveTypes = {};

        /* this._primitiveProcedures and this._primitiveFunctions
         * are dictionaries indexed by the name of the primitive operation
         * (procedure or function). Their value is an instance of
         * PrimitiveOperation.
         */
        this._primitiveProcedures = {};
        this._primitiveFunctions = {};

        /* --Primitive types-- */

        /* Booleans */
        this._primitiveTypes[i18n('TYPE:Bool')] = {};
        for (const boolName of boolEnum()) {
            this._primitiveTypes[i18n('TYPE:Bool')][boolName] = [];
        }

        /* Colors */
        this._primitiveTypes[i18n('TYPE:Color')] = {};
        for (const colorName of colorEnum()) {
            this._primitiveTypes[i18n('TYPE:Color')][colorName] = [];
        }

        /* Directions */
        this._primitiveTypes[i18n('TYPE:Dir')] = {};
        for (const dirName of dirEnum()) {
            this._primitiveTypes[i18n('TYPE:Dir')][dirName] = [];
        }

        /* Events */
        this._primitiveTypes[i18n('TYPE:Event')] = {};
        for (const eventName of eventEnum()) {
            this._primitiveTypes[i18n('TYPE:Event')][eventName] = [];
        }

        /* --Primitive procedures-- */

        this._primitiveProcedures[i18n('PRIM:TypeCheck')] = new PrimitiveOperation(
            [typeAny, typeAny, typeString],
            (startPos: SourceReader, endPos: SourceReader, globalState, args) => {
                const v1 = args[0];
                const v2 = args[1];
                const errorMessage = args[2];
                if (joinTypes(v1.type(), v2.type()) === undefined) {
                    fail(startPos, endPos, 'typecheck-failed', [
                        errorMessage.string,
                        v1.type(),
                        v2.type()
                    ]);
                }
            },
            (globalState, color) => undefined
        );

        this._primitiveProcedures[i18n('PRIM:PutStone')] = new PrimitiveOperation(
            [typeColor()],
            noValidation,
            (globalState, color) => {
                globalState.putStone(colorFromValue(color));
                return undefined;
            }
        );

        this._primitiveProcedures[i18n('PRIM:RemoveStone')] = new PrimitiveOperation(
            [typeColor()],
            (startPos, endPos, globalState, args) => {
                const colorName = colorFromValue(args[0]);
                if (globalState.numStones(colorName).le(new ValueInteger(0))) {
                    fail(startPos, endPos, 'cannot-remove-stone', [colorName]);
                }
            },
            (globalState, color) => {
                globalState.removeStone(colorFromValue(color));
                return undefined;
            }
        );

        this._primitiveProcedures[i18n('PRIM:Move')] = new PrimitiveOperation(
            [typeDir()],
            (startPos, endPos, globalState, args) => {
                const dirName = dirFromValue(args[0]);
                if (!globalState.canMove(dirName)) {
                    fail(startPos, endPos, 'cannot-move-to', [dirName]);
                }
            },
            (globalState, dir) => {
                globalState.move(dirFromValue(dir));
                return undefined;
            }
        );

        this._primitiveProcedures[i18n('PRIM:GoToEdge')] = new PrimitiveOperation(
            [typeDir()],
            noValidation,
            (globalState, dir) => {
                globalState.goToEdge(dirFromValue(dir));
                return undefined;
            }
        );

        this._primitiveProcedures[i18n('PRIM:EmptyBoardContents')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState, dir) => {
                globalState.emptyBoardContents();
                return undefined;
            }
        );

        this._primitiveProcedures['_FAIL'] =
            /* Procedure that always fails */
            new PrimitiveOperation(
                [typeString],
                (startPos, endPos, globalState, args) => {
                    fail(startPos, endPos, args[0].string, []);
                },
                (globalState, errMsg /* Unreachable */) => undefined
            );

        /* --Primitive functions-- */

        this._primitiveFunctions['_makeRange'] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const first = args[0];
                const last = args[1];
                validateCompatibleTypes(startPos, endPos, first, last);
                validateTypeAmong(startPos, endPos, first, typesWithOrder());
                validateTypeAmong(startPos, endPos, last, typesWithOrder());
            },
            (globalState, first, last) => {
                let current = first;
                if (boolFromValue(genericGT(current, last))) {
                    return new ValueList([]);
                }
                const result = [];
                while (boolFromValue(genericLT(current, last))) {
                    result.push(current);
                    current = genericNext(current);
                }
                result.push(current);
                return new ValueList(result);
            }
        );

        this._primitiveFunctions['not'] = new PrimitiveOperation(
            [typeBool()],
            noValidation,
            (globalState, x) => valueFromBool(!boolFromValue(x))
        );

        this._primitiveFunctions['&&'] = new PrimitiveOperation(
            [typeAny, typeAny],
            noValidation,
            /*
             * This function is a stub so the linter recognizes '&&'
             * as a defined primitive function of arity 2.
             *
             * The implementation of '&&' is treated specially by the
             * compiler to account for short-circuiting.
             */
            (globalState, x, y) => {
                throw Error('The function "&&" should never be called');
            }
        );

        this._primitiveFunctions['||'] = new PrimitiveOperation(
            [typeAny, typeAny],
            noValidation,
            /*
             * This function is a stub so the linter recognizes '||'
             * as a defined primitive function of arity 2.
             *
             * The implementation of '||' is treated specially by the
             * compiler to account for short-circuiting.
             */
            (globalState, x, y) => {
                throw Error('The function "||" should never be called');
            }
        );

        this._primitiveFunctions['_makeRangeWithSecond'] = new PrimitiveOperation(
            [typeAny, typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const first = args[0];
                const last = args[1];
                const second = args[2];
                validateTypeAmong(startPos, endPos, first, [typeInteger]);
                validateTypeAmong(startPos, endPos, last, [typeInteger]);
                validateTypeAmong(startPos, endPos, second, [typeInteger]);
            },
            (globalState, first, last, second) => {
                const delta = second.sub(first);
                if (delta.lt(new ValueInteger(1))) {
                    return new ValueList([]);
                }
                let current = first;
                const result = [];
                while (current.le(last)) {
                    result.push(current);
                    current = current.add(delta);
                }
                return new ValueList(result);
            }
        );

        this._primitiveFunctions['_unsafeListLength'] = new PrimitiveOperation(
            [typeAny],
            noValidation,
            (globalState, list) => new ValueInteger(list.length())
        );

        this._primitiveFunctions['_unsafeListNth'] = new PrimitiveOperation(
            [typeAny, typeAny],
            noValidation,
            (globalState, list, index) => list.elements[index.asNumber()]
        );

        this._primitiveFunctions[i18n('PRIM:numStones')] = new PrimitiveOperation(
            [typeColor()],
            noValidation,
            (globalState, color) => globalState.numStones(colorFromValue(color))
        );

        this._primitiveFunctions[i18n('PRIM:anyStones')] = new PrimitiveOperation(
            [typeColor()],
            noValidation,
            (globalState, color) => {
                const num = globalState.numStones(colorFromValue(color));
                return valueFromBool(num.gt(new ValueInteger(0)));
            }
        );

        this._primitiveFunctions[i18n('PRIM:canMove')] = new PrimitiveOperation(
            [typeDir()],
            noValidation,
            (globalState, dir) => valueFromBool(globalState.canMove(dirFromValue(dir)))
        );

        this._primitiveFunctions[i18n('PRIM:next')] = new PrimitiveOperation(
            [typeAny],
            (startPos, endPos, globalState, args) => {
                const value = args[0];
                validateTypeAmong(startPos, endPos, value, typesWithOrder());
            },
            (globalState, value) => genericNext(value)
        );

        this._primitiveFunctions[i18n('PRIM:prev')] = new PrimitiveOperation(
            [typeAny],
            (startPos, endPos, globalState, args) => {
                const value = args[0];
                validateTypeAmong(startPos, endPos, value, typesWithOrder());
            },
            (globalState, value) => genericPrev(value)
        );

        this._primitiveFunctions[i18n('PRIM:opposite')] = new PrimitiveOperation(
            [typeAny],
            (startPos, endPos, globalState, args) => {
                const value = args[0];
                validateTypeAmong(startPos, endPos, value, typesWithOpposite());
            },
            (globalState, value) => genericOpposite(value)
        );

        this._primitiveFunctions[i18n('PRIM:minBool')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState) => valueFromBool(false)
        );

        this._primitiveFunctions[i18n('PRIM:maxBool')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState) => valueFromBool(true)
        );

        this._primitiveFunctions[i18n('PRIM:minColor')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState) => valueFromColor(colorEnum()[0])
        );

        this._primitiveFunctions[i18n('PRIM:maxColor')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState) => valueFromColor(colorEnum()[colorEnum().length - 1])
        );

        this._primitiveFunctions[i18n('PRIM:minDir')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState) => valueFromDir(dirEnum()[0])
        );

        this._primitiveFunctions[i18n('PRIM:maxDir')] = new PrimitiveOperation(
            [],
            noValidation,
            (globalState) => valueFromDir(dirEnum()[dirEnum().length - 1])
        );

        /* Arithmetic operators */

        this._primitiveFunctions['+'] = new PrimitiveOperation(
            [typeInteger, typeInteger],
            noValidation,
            (globalState, a, b) => a.add(b)
        );

        this._primitiveFunctions['-'] = new PrimitiveOperation(
            [typeInteger, typeInteger],
            noValidation,
            (globalState, a, b) => a.sub(b)
        );

        this._primitiveFunctions['*'] = new PrimitiveOperation(
            [typeInteger, typeInteger],
            noValidation,
            (globalState, a, b) => a.mul(b)
        );

        this._primitiveFunctions['div'] = new PrimitiveOperation(
            [typeInteger, typeInteger],
            (startPos, endPos, globalState, args) => {
                const b = args[1];
                if (b.eq(new ValueInteger(0))) {
                    fail(startPos, endPos, 'cannot-divide-by-zero', []);
                }
            },
            (globalState, a, b) => a.div(b)
        );

        this._primitiveFunctions['mod'] = new PrimitiveOperation(
            [typeInteger, typeInteger],
            (startPos, endPos, globalState, args) => {
                const b = args[1];
                if (b.eq(new ValueInteger(0))) {
                    fail(startPos, endPos, 'cannot-divide-by-zero', []);
                }
            },
            (globalState, a, b) => a.mod(b)
        );

        this._primitiveFunctions['^'] = new PrimitiveOperation(
            [typeInteger, typeInteger],
            (startPos, endPos, globalState, args) => {
                const b = args[1];
                if (b.lt(new ValueInteger(0))) {
                    fail(startPos, endPos, 'negative-exponent', []);
                }
            },
            (globalState, a, b) => a.pow(b)
        );

        this._primitiveFunctions['-(unary)'] = new PrimitiveOperation(
            [typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                validateTypeAmong(startPos, endPos, a, typesWithOpposite());
            },
            (globalState, a) => genericOpposite(a)
        );

        /* Relational operators */

        this._primitiveFunctions['=='] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
            },
            (globalState, a, b) => valueFromBool(a.equal(b))
        );

        this._primitiveFunctions['/='] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
            },
            (globalState, a, b) => valueFromBool(!a.equal(b))
        );

        this._primitiveFunctions['<='] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
                validateTypeAmong(startPos, endPos, a, typesWithOrder());
                validateTypeAmong(startPos, endPos, b, typesWithOrder());
            },
            (globalState, a, b) => genericLE(a, b)
        );

        this._primitiveFunctions['>='] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
                validateTypeAmong(startPos, endPos, a, typesWithOrder());
                validateTypeAmong(startPos, endPos, b, typesWithOrder());
            },
            (globalState, a, b) => genericGE(a, b)
        );

        this._primitiveFunctions['<'] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
                validateTypeAmong(startPos, endPos, a, typesWithOrder());
                validateTypeAmong(startPos, endPos, b, typesWithOrder());
            },
            (globalState, a, b) => genericLT(a, b)
        );

        this._primitiveFunctions['>'] = new PrimitiveOperation(
            [typeAny, typeAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
                validateTypeAmong(startPos, endPos, a, typesWithOrder());
                validateTypeAmong(startPos, endPos, b, typesWithOrder());
            },
            (globalState, a, b) => genericGT(a, b)
        );

        /* User-triggered failure */

        this._primitiveProcedures[i18n('PRIM:BOOM')] = new PrimitiveOperation(
            [typeString],
            (startPos, endPos, globalState, args) => {
                fail(startPos, endPos, 'boom-called', [args[0].string]);
            },
            (globalState, msg) => {
                throw Error('Should not be reachable.');
            }
        );

        this._primitiveFunctions[i18n('PRIM:boom')] = this._primitiveProcedures[i18n('PRIM:BOOM')];

        /* List operators */
        this._primitiveFunctions['++'] = new PrimitiveOperation(
            [typeListAny, typeListAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                const b = args[1];
                validateCompatibleTypes(startPos, endPos, a, b);
            },
            (globalState, a, b) => a.append(b)
        );

        this._primitiveFunctions[i18n('PRIM:isEmpty')] = new PrimitiveOperation(
            [typeListAny],
            noValidation,
            (globalState, a) => valueFromBool(a.length() === 0)
        );

        this._primitiveFunctions[i18n('PRIM:head')] = new PrimitiveOperation(
            [typeListAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                if (a.length() === 0) {
                    fail(startPos, endPos, 'list-cannot-be-empty', []);
                }
            },
            (globalState, a) => a.head()
        );

        this._primitiveFunctions[i18n('PRIM:tail')] = new PrimitiveOperation(
            [typeListAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                if (a.length() === 0) {
                    fail(startPos, endPos, 'list-cannot-be-empty', []);
                }
            },
            (globalState, a) => a.tail()
        );

        this._primitiveFunctions[i18n('PRIM:oldTail')] = this._primitiveFunctions[
            i18n('PRIM:tail')
        ];

        this._primitiveFunctions[i18n('PRIM:init')] = new PrimitiveOperation(
            [typeListAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                if (a.length() === 0) {
                    fail(startPos, endPos, 'list-cannot-be-empty', []);
                }
            },
            (globalState, a) => a.init()
        );

        this._primitiveFunctions[i18n('PRIM:last')] = new PrimitiveOperation(
            [typeListAny],
            (startPos, endPos, globalState, args) => {
                const a = args[0];
                if (a.length() === 0) {
                    fail(startPos, endPos, 'list-cannot-be-empty', []);
                }
            },
            (globalState, a) => a.last()
        );
    }

    /* Types */

    public types(): string[] {
        const typeNames = [];
        for (const typeName in this._primitiveTypes) {
            typeNames.push(typeName);
        }
        return typeNames;
    }

    public typeConstructors(typeName: string): string[] {
        if (!(typeName in this._primitiveTypes)) {
            throw Error('Not a primitive type: ' + typeName);
        }
        const constructorNames = [];
        for (const constructorName in this._primitiveTypes[typeName]) {
            constructorNames.push(constructorName);
        }
        return constructorNames;
    }

    public constructorFields(typeName: string, constructorName: string): string[] {
        if (!(typeName in this._primitiveTypes)) {
            throw Error('Not a primitive type: ' + typeName);
        }
        if (!(constructorName in this._primitiveTypes[typeName])) {
            throw Error('Not a primitive constructor: ' + constructorName);
        }
        return this._primitiveTypes[typeName][constructorName];
    }

    /* Operations */

    public isOperation(primitiveName: string): boolean {
        return (
            primitiveName in this._primitiveProcedures || primitiveName in this._primitiveFunctions
        );
    }

    public getOperation(primitiveName: string): PrimitiveOperation {
        if (primitiveName in this._primitiveProcedures) {
            return this._primitiveProcedures[primitiveName];
        } else if (primitiveName in this._primitiveFunctions) {
            return this._primitiveFunctions[primitiveName];
        } else {
            throw Error(primitiveName + ' is not a primitive.');
        }
    }

    /* Procedures */

    public procedures(): string[] {
        const procedureNames = [];
        for (const procedureName in this._primitiveProcedures) {
            procedureNames.push(procedureName);
        }
        return procedureNames;
    }

    public isProcedure(primitiveName: string): boolean {
        return primitiveName in this._primitiveProcedures;
    }

    /* Functions */

    public functions(): string[] {
        const functionNames = [];
        for (const functionName in this._primitiveFunctions) {
            functionNames.push(functionName);
        }
        return functionNames;
    }

    public isFunction(primitiveName: string): boolean {
        return primitiveName in this._primitiveFunctions;
    }
}
