/* eslint-disable no-underscore-dangle */
import { i18n } from './i18n';

/* Each value has a type.
 *
 * A type is a tree, represented with instances of Type (or its subclasses).
 * We write:
 *   r(c1, ..., cN)
 * for a tree whose root is r and whose children are c1, ..., cN.
 *
 * The type of a value may be one of the following:
 *   new TypeAny()                      (unknown)
 *   new TypeInteger()
 *   new TypeString()
 *   new TypeTuple([t1, ..., tN])
 *     where ti is the type of the i-th component.
 *   new TypeList(t)
 *     where t is the type of the elements.
 *   new TypeStructure(typeName, cases)
 *     where typeName is the name of the type (e.g. 'Bool').
 *     Moreover, cases is an object of the following "type":
 *       Map String (Map String Type)
 *     more precisely,
 *     - cases is dictionary indexed by constructor names,
 *     - if c is a constructor name, cases[c] is a dictionary
 *       indexed by field name,
 *     - if f is a field name, cases[c][f] is the type of the
 *       field f for the constructor c.
 *
 *     For example, consider the following type definition:
 *       type A is variant {
 *         case B {
 *           field x
 *           field y
 *         }
 *         case C {
 *           field z
 *         }
 *       }
 *
 *    Then the following expression in Gobstones:
 *      [B(x <- 1, y <- "foo")]
 *    is a list whose type is represented as:
 *      new TypeList(
 *        new TypeStructure('A', {
 *          'B': {'x': new TypeInteger(), 'y': new TypeString()}
 *        })
 *      )
 *
 *    The following expression in Gobstones:
 *      [B(x <- 1, y <- "foo"), C(z <- "bar")]
 *    is a list whose type is represented as:
 *      new TypeList(
 *        new TypeStructure('A', {
 *          'B': {'x': new TypeInteger(), 'y': new TypeString()},
 *          'C': {'z': new TypeString()},
 *        })
 *      )
 */
const Ty_Any = Symbol.for('Ty_Any');
const Ty_Integer = Symbol.for('Ty_Integer');
const Ty_String = Symbol.for('Ty_String');
const Ty_Tuple = Symbol.for('Ty_Tuple');
const Ty_List = Symbol.for('Ty_List');
const Ty_Structure = Symbol.for('Ty_Structure');
const Ty_Unkown = Symbol.for('?');

export class Type {
    private _tag: symbol;

    public constructor(tag: symbol) {
        this._tag = tag;
    }

    public get tag(): symbol {
        return this._tag;
    }

    public isAny(): boolean {
        return false;
    }

    public isInteger(): boolean {
        return false;
    }

    public isString(): boolean {
        return false;
    }

    public isTuple(): boolean {
        return false;
    }

    public isList(): boolean {
        return false;
    }

    public isStructure(): boolean {
        return false;
    }

    public isBoolean(): boolean {
        return false;
    }

    public isColor(): boolean {
        return false;
    }

    public isDirection(): boolean {
        return false;
    }
}

export class TypeAny extends Type {
    public constructor() {
        super(Ty_Any);
    }

    public toString(): string {
        return '?';
    }

    public isAny(): boolean {
        return true;
    }
}

export class TypeInteger extends Type {
    public constructor() {
        super(Ty_Integer);
    }

    public toString(): string {
        return i18n('TYPE:Integer');
    }

    public isInteger(): boolean {
        return true;
    }
}

export class TypeString extends Type {
    public constructor() {
        super(Ty_String);
    }

    public toString(): string {
        return i18n('TYPE:String');
    }

    public isString(): boolean {
        return true;
    }
}

export class TypeTuple extends Type {
    private _componentTypes: any;

    public constructor(componentTypes: Type[]) {
        super(Ty_Tuple);
        this._componentTypes = componentTypes;
    }

    public get componentTypes(): Type[] {
        return this._componentTypes;
    }

    public toString(): string {
        const strings = [];
        for (const t of this._componentTypes) {
            strings.push(t.toString());
        }
        return i18n('TYPE:Tuple') + '(' + strings.join(', ') + ')';
    }

    public isTuple(): boolean {
        return true;
    }
}

export class TypeList extends Type {
    private _contentType: any;

    public constructor(contentType: Type) {
        super(Ty_List);
        this._contentType = contentType;
    }

    public get contentType(): Type {
        return this._contentType;
    }

    public toString(): string {
        let suffix = '';
        if (!this._contentType.isAny()) {
            suffix = '(' + this._contentType.toString() + ')';
        }
        return i18n('TYPE:List') + suffix;
    }

    public isList(): boolean {
        return true;
    }
}

export class TypeStructure extends Type {
    private _typeName: any;
    private _cases: any;
    public constructor(typeName: string, cases: Record<string, Record<string, Type>>) {
        super(Ty_Structure);
        this._typeName = typeName;
        this._cases = cases;
    }

    public get typeName(): string {
        return this._typeName;
    }

    public get cases(): Record<string, Record<string, Type>> {
        return this._cases;
    }

    public toString(): string {
        const caseStrings = [];
        for (const constructorName of sortedKeys(this._cases)) {
            const fieldTypes = this._cases[constructorName];
            const fieldStrings = [];
            for (const fieldName of sortedKeys(fieldTypes)) {
                fieldStrings.push(fieldName + ' <- ' + fieldTypes[fieldName].toString());
            }
            if (fieldStrings.length !== 0) {
                caseStrings.push(constructorName + '(' + fieldStrings.join(', ') + ')');
            }
        }
        if (caseStrings.length === 0) {
            return this._typeName;
        } else {
            return this._typeName + ' { ' + caseStrings.join(' | ') + ' }';
        }
    }

    public isStructure(): boolean {
        return true;
    }

    public isBoolean(): boolean {
        return this._typeName === i18n('TYPE:Bool');
    }

    public isColor(): boolean {
        return this._typeName === i18n('TYPE:Color');
    }

    public isDirection(): boolean {
        return this._typeName === i18n('TYPE:Dir');
    }
}

/* Attempts to calculate the "join" of two types.
 *
 * To join two types:
 * - any occurrence of TypeAny() may be replaced by an arbitrary type,
 * - structures of the same type built with different constructors
 *   are joinable,
 * - structures of the same type built with the same constructors
 *   are joinable if their matching fields are joinable.
 *
 * If the types are joinable, return their join.
 * If the types are not joinable, return undefined.
 */

export function joinTypes(type1: Type, type2: Type): Type {
    if (type1 === undefined || type2 === undefined) {
        return undefined;
    } else if (type1.tag === Ty_Any) {
        return type2;
    } else if (type2.tag === Ty_Any) {
        return type1;
    } else if (type1.tag === Ty_Integer && type2.tag === Ty_Integer) {
        return type1;
    } else if (type1.tag === Ty_String && type2.tag === Ty_String) {
        return type1;
    } else if (type1.tag === Ty_Tuple && type2.tag === Ty_Tuple) {
        return joinTupleTypes(type1 as TypeTuple, type2 as TypeTuple);
    } else if (type1.tag === Ty_List && type2.tag === Ty_List) {
        return joinListTypes(type1 as TypeList, type2 as TypeList);
    } else if (type1.tag === Ty_Structure && type2.tag === Ty_Structure) {
        return joinStructureTypes(type1 as TypeStructure, type2 as TypeStructure);
    } else {
        /* Otherwise the types are not joinable */
        return undefined;
    }
}

const joinTupleTypes = (type1: TypeTuple, type2: TypeTuple): TypeTuple => {
    if (type1.componentTypes.length !== type2.componentTypes.length) {
        /* Tuples are of different length */
        return undefined;
    }
    const joinedComponents = [];
    for (let i = 0; i < type1.componentTypes.length; i++) {
        const t1 = type1.componentTypes[i];
        const t2 = type2.componentTypes[i];
        const tj = joinTypes(t1, t2);
        if (tj === undefined) {
            /* Cannot join the i-th component */
            return undefined;
        }
        joinedComponents.push(tj);
    }
    return new TypeTuple(joinedComponents);
};

const joinListTypes = (type1: TypeList, type2: TypeList): TypeList => {
    const joinedContent = joinTypes(type1.contentType, type2.contentType);
    if (joinedContent === undefined) {
        /* Cannot join the contents of the lists */
        return undefined;
    }
    return new TypeList(joinedContent);
};

/*
 * The join of two structures is quite like a least common multiple.
 * We must:
 * - Check that they are structures of the same type.
 * - Include all the non-common constructors verbatim
 *   (with "non-common" we mean those that are in type1
 *   but not in type2 or vice-versa).
 * - For all common constructors, we must recursively join
 *   the types of their respective fields.
 */
const joinStructureTypes = (type1: TypeStructure, type2: TypeStructure): TypeStructure => {
    if (type1.typeName !== type2.typeName) {
        return undefined;
    }

    const joinedCases = {};

    /* Include all the non-common constructors */
    function joinCommon(typeA, typeB): void {
        for (const constructorName in typeA.cases) {
            if (!(constructorName in typeB.cases)) {
                joinedCases[constructorName] = typeA.cases[constructorName];
            }
        }
    }
    joinCommon(type1, type2);
    joinCommon(type2, type1);

    /* Include all the common constructors */
    for (const constructorName in type1.cases) {
        if (constructorName in type2.cases) {
            const joinedFields = joinFields(
                type1.cases[constructorName],
                type2.cases[constructorName]
            );
            if (joinedFields === undefined) {
                return undefined;
            }
            joinedCases[constructorName] = joinedFields;
        }
    }

    return new TypeStructure(type1.typeName, joinedCases);
};

const joinFields = (
    fields1: Record<string, Type>,
    fields2: Record<string, Type>
): Record<string, Type> => {
    /* Ensure that they have the same set of fields */
    function checkIncluded(fieldsA: Record<string, Type>, fieldsB: Record<string, Type>): void {
        for (const fieldName in fieldsA) {
            if (!(fieldName in fieldsB)) {
                throw Error(
                    'Join fields: structures built using the same constructor ' +
                        'should have the same set of fields.'
                );
            }
        }
    }
    checkIncluded(fields1, fields2);
    checkIncluded(fields2, fields1);

    /* Recursively join the types of the common fields */
    const joinedFields = {};
    for (const fieldName in fields1) {
        const type1 = fields1[fieldName];
        const type2 = fields2[fieldName];
        const joinedTypes = joinTypes(type1, type2);
        if (joinedTypes === undefined) {
            return undefined;
        }
        joinedFields[fieldName] = joinedTypes;
    }
    return joinedFields;
};

/* Helper function */

function sortedKeys(dictionary: Record<string, unknown>): string[] {
    const keys = [];
    for (const key in dictionary) {
        keys.push(key);
    }
    return keys.sort();
}

/* Value tags */
export const V_Integer = Symbol.for('V_Integer');
export const V_String = Symbol.for('V_String');
export const V_Tuple = Symbol.for('V_Tuple');
export const V_List = Symbol.for('V_List');
export const V_Structure = Symbol.for('V_Structure');

export class Value {
    private _tag: symbol;

    public constructor(tag: symbol) {
        this._tag = tag;
    }

    public get tag(): symbol {
        return this._tag;
    }

    public type(): Type {
        return new Type(Ty_Unkown);
    }

    public isInteger(): boolean {
        return this.type().isInteger();
    }

    public isString(): boolean {
        return this.type().isString();
    }

    public isTuple(): boolean {
        return this.type().isTuple();
    }

    public isList(): boolean {
        return this.type().isList();
    }

    public isStructure(): boolean {
        return this.type().isStructure();
    }

    public isBoolean(): boolean {
        return this.type().isBoolean();
    }

    public equal(other: Value): boolean {
        return false;
    }
}

export class ValueInteger extends Value {
    private _number: number;

    public constructor(number: number | string) {
        super(V_Integer);
        this._number = typeof number === 'number' ? number : parseInt(number, 10);
    }

    public toString(): string {
        return this._number.toString();
    }

    public get number(): number {
        return this._number;
    }

    public type(): Type {
        return new TypeInteger();
    }

    public equal(other: Value): boolean {
        return other.tag === V_Integer && this.number === (other as ValueInteger).number;
    }

    public add(other: ValueInteger): ValueInteger {
        const a = this._number;
        const b = other._number;
        return new ValueInteger(a + b);
    }

    public sub(other: ValueInteger): ValueInteger {
        const a = this._number;
        const b = other._number;
        return new ValueInteger(a - b);
    }

    public mul(other: ValueInteger): ValueInteger {
        const a = this._number;
        const b = other._number;
        return new ValueInteger(a * b === 0 ? 0 : a * b);
    }

    /* Gobstones calculates quotients using
     * modulo (i.e.truncating towards minus infinity)
     * rather than
     * remainder (i.e.truncating towards 0).
     *
     * We need to adjust the result to match the standard Gobstones
     * semantics, namely:
     *
     * if a and b have the same sign, then
     *   a div b  =  abs(a) / abs(b)
     *
     * if a and b have different signs, then
     *   a div b  =  -((abs(a) + abs(b) - 1) / abs(b))
     *
     * Here "div" denotes the official Gobstones division operator,
     * while "/" denotes the JavaScript/bigint implementation.
     */
    public div(other: ValueInteger): ValueInteger {
        const z = new ValueInteger(0);
        if (this.gt(z) === other.gt(z)) {
            /* Same sign */
            const a = this.abs()._number;
            const b = other.abs()._number;
            const c = Math.floor(a / b) === 0 ? 0 : Math.floor(a / b);
            return new ValueInteger(c);
        } else {
            /* Different sign */
            const inc = other.abs().sub(new ValueInteger(1));
            const a = this.abs().add(inc)._number;
            const b = other.abs()._number;
            const c = Math.floor(a / b) * -1 === 0 ? 0 : Math.floor(a / b) * -1;
            return new ValueInteger(c);
        }
    }

    /* Calculate the modulus from the equation a = qb + r,
     * i.e.  r = a - qb */
    public mod(other: ValueInteger): ValueInteger {
        const q = this.div(other);
        return this.sub(q.mul(other));
    }

    /* Assumes that 'other' is non-negative */
    public pow(other: ValueInteger): ValueInteger {
        const a = this._number;
        const b = other._number;
        return new ValueInteger(a ** b);
    }

    public eq(other: ValueInteger): boolean {
        return this.equal(other);
    }

    public ne(other: ValueInteger): boolean {
        return !this.equal(other);
    }

    public le(other: ValueInteger): boolean {
        const a = this._number;
        const b = other._number;
        return a <= b;
    }

    public lt(other: ValueInteger): boolean {
        const a = this._number;
        const b = other._number;
        return a < b;
    }

    public ge(other: ValueInteger): boolean {
        const a = this._number;
        const b = other._number;
        return a >= b;
    }

    public gt(other: ValueInteger): boolean {
        const a = this._number;
        const b = other._number;
        return a > b;
    }

    public negate(): ValueInteger {
        const a = this._number;
        let x = a * -1;
        x = x === 0 ? 0 : x;
        return new ValueInteger(x);
    }

    public abs(): ValueInteger {
        if (this.gt(new ValueInteger(0))) {
            return this;
        } else {
            return this.negate();
        }
    }

    public asNumber(): number {
        return this._number;
    }
}

export class ValueString extends Value {
    private _string: string;

    public constructor(string: string) {
        super(V_String);
        this._string = string;
    }

    public toString(): string {
        const res = ['"'];
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < this._string.length; i++) {
            const chr = this._string[i];
            switch (chr) {
                case '"':
                    res.push('\\');
                    res.push('"');
                    break;
                case '\\':
                    res.push('\\');
                    res.push('\\');
                    break;
                case '\u0007':
                    res.push('\\');
                    res.push('a');
                    break;
                case '\b':
                    res.push('\\');
                    res.push('b');
                    break;
                case '\f':
                    res.push('\\');
                    res.push('f');
                    break;
                case '\n':
                    res.push('\\');
                    res.push('n');
                    break;
                case '\r':
                    res.push('\\');
                    res.push('r');
                    break;
                case '\t':
                    res.push('\\');
                    res.push('t');
                    break;
                case '\v':
                    res.push('\\');
                    res.push('v');
                    break;
                default:
                    res.push(chr);
                    break;
            }
        }
        res.push('"');
        return res.join('');
    }

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

    public equal(other: Value): boolean {
        return other.tag === V_String && this.string === (other as ValueString).string;
    }

    public type(): Type {
        return new TypeString();
    }
}

export class ValueTuple extends Value {
    private _components: Value[];
    private _type: TypeTuple;

    public constructor(components: Value[]) {
        super(V_Tuple);
        this._components = components;
        this._type = this._inferType();
    }

    public toString(): string {
        const res = [];
        for (const component of this._components) {
            res.push(component.toString());
        }
        return '(' + res.join(', ') + ')';
    }

    public get components(): Value[] {
        return this._components;
    }

    public size(): number {
        return this._components.length;
    }

    public equal(other: Value): boolean {
        if (other.tag !== V_Tuple) {
            return false;
        }
        if (this.components.length !== (other as ValueTuple).components.length) {
            return false;
        }
        for (let i = 0; i < this.components.length; i++) {
            if (!this.components[i].equal((other as ValueTuple).components[i])) {
                return false;
            }
        }
        return true;
    }

    public type(): Type {
        return this._type;
    }

    public _inferType(): TypeTuple {
        const componentTypes = [];
        for (const component of this._components) {
            componentTypes.push(component.type());
        }
        return new TypeTuple(componentTypes);
    }
}

export class ValueList extends Value {
    private _elements: Value[];
    private _type: Type;
    public constructor(elements: Value[]) {
        super(V_List);
        this._elements = elements;
        this._type = this._inferType();
    }

    public toString(): string {
        const res = [];
        for (const element of this._elements) {
            res.push(element.toString());
        }
        return '[' + res.join(', ') + ']';
    }

    public get elements(): Value[] {
        return this._elements;
    }

    public equal(other: Value): boolean {
        if (other.tag !== V_List) {
            return false;
        }
        if (this.elements.length !== (other as ValueList).elements.length) {
            return false;
        }
        for (let i = 0; i < this.elements.length; i++) {
            if (!this.elements[i].equal((other as ValueList).elements[i])) {
                return false;
            }
        }
        return true;
    }

    public type(): Type {
        return this._type;
    }

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

    public _inferType(): Type {
        let contentType = new TypeAny();
        for (const element of this._elements) {
            contentType = joinTypes(contentType, element.type());
        }
        return new TypeList(contentType);
    }

    public append(other: ValueList): ValueList {
        const allElements = [];
        for (const elem of this.elements) {
            allElements.push(elem);
        }
        for (const elem of other.elements) {
            allElements.push(elem);
        }
        return new ValueList(allElements);
    }

    public head(): Value {
        return this.elements[0];
    }

    public tail(): ValueList {
        const elements = [];
        for (let i = 1; i < this.elements.length; i++) {
            elements.push(this.elements[i]);
        }
        return new ValueList(elements);
    }

    public init(): ValueList {
        const elements = [];
        for (let i = 0; i < this.elements.length - 1; i++) {
            elements.push(this.elements[i]);
        }
        return new ValueList(elements);
    }

    public last(): Value {
        return this.elements[this.elements.length - 1];
    }
}

/* An instance of ValueStructure represents a 'structure' i.e.  a value
 * inhabiting an 'inductive' datatype.
 *
 * This includes built-in enumerations (e.g. booleans), the "event" type
 * received by an interactive program, and user-defined records and variants.
 *
 * The second parameter "fields" should be a dictionary mapping field names to
 * values
 */
export class ValueStructure extends Value {
    private _typeName: string;
    private _constructorName: string;
    private _fields: Record<string, Value>;
    public constructor(typeName: string, constructorName: string, fields: Record<string, Value>) {
        super(V_Structure);
        this._typeName = typeName;
        this._constructorName = constructorName;
        this._fields = fields;
    }

    public toString(): string {
        const res = [];
        const fieldNames = this.fieldNames();
        if (fieldNames.length === 0) {
            return this._constructorName;
        }
        for (const fieldName of fieldNames) {
            res.push(fieldName + ' <- ' + this.fields[fieldName].toString());
        }
        return this._constructorName + '(' + res.join(', ') + ')';
    }

    public get typeName(): string {
        return this._typeName;
    }

    public get constructorName(): string {
        return this._constructorName;
    }

    public get fields(): Record<string, Value> {
        return this._fields;
    }

    public fieldNames(): string[] {
        return sortedKeys(this._fields);
    }

    public _clone(): ValueStructure {
        const newFields = {};
        for (const fieldName in this._fields) {
            newFields[fieldName] = this._fields[fieldName];
        }
        return new ValueStructure(this._typeName, this._constructorName, newFields);
    }

    public updateFields(fields: Record<string, Value>): ValueStructure {
        const newStructure = this._clone();
        for (const fieldName in fields) {
            newStructure.fields[fieldName] = fields[fieldName];
        }
        return newStructure;
    }

    public equal(other: Value): boolean {
        if (other.tag !== V_Structure) {
            return false;
        }
        if (this.constructorName !== (other as ValueStructure).constructorName) {
            return false;
        }
        const fieldNames = this.fieldNames();
        for (const fieldName of fieldNames) {
            if (!this.fields[fieldName].equal((other as ValueStructure).fields[fieldName])) {
                return false;
            }
        }
        return true;
    }

    public type(): Type {
        const fieldTypes = {};
        for (const fieldName in this._fields) {
            fieldTypes[fieldName] = this._fields[fieldName].type();
        }
        const cases = {};
        cases[this._constructorName] = fieldTypes;
        return new TypeStructure(this._typeName, cases);
    }
}
