/* eslint-disable no-underscore-dangle */
import { UnknownPosition, SourceReader } from '@gobstones/gobstones-parser';
import { Type } from './value';

/* Opcodes are constant symbols */
export const I_PushInteger = Symbol.for('I_PushInteger');
export const I_PushString = Symbol.for('I_PushString');
export const I_PushVariable = Symbol.for('I_PushVariable');
export const I_SetVariable = Symbol.for('I_SetVariable');
export const I_UnsetVariable = Symbol.for('I_UnsetVariable');
export const I_Label = Symbol.for('I_Label');
export const I_Jump = Symbol.for('I_Jump');
export const I_JumpIfFalse = Symbol.for('I_JumpIfFalse');
export const I_JumpIfStructure = Symbol.for('I_JumpIfStructure');
export const I_JumpIfTuple = Symbol.for('I_JumpIfTuple');
export const I_Call = Symbol.for('I_Call');
export const I_Return = Symbol.for('I_Return');
export const I_MakeTuple = Symbol.for('I_MakeTuple');
export const I_MakeList = Symbol.for('I_MakeList');
export const I_MakeStructure = Symbol.for('I_MakeStructure');
export const I_UpdateStructure = Symbol.for('I_UpdateStructure');
export const I_ReadTupleComponent = Symbol.for('I_ReadTupleComponent');
export const I_ReadStructureField = Symbol.for('I_ReadStructureField');
export const I_ReadStructureFieldPop = Symbol.for('I_ReadStructureFieldPop');
export const I_Add = Symbol.for('I_Add');
export const I_Dup = Symbol.for('I_Dup');
export const I_Pop = Symbol.for('I_Pop');
export const I_PrimitiveCall = Symbol.for('I_PrimitiveCall');
export const I_SaveState = Symbol.for('I_SaveState');
export const I_RestoreState = Symbol.for('I_RestoreState');
export const I_TypeCheck = Symbol.for('I_TypeCheck');

export class Code {
    private _instructions: Instruction[];

    public constructor(instructions: Instruction[]) {
        this._instructions = instructions;
    }

    public toString(): string {
        const res = [];
        for (const instruction of this._instructions) {
            res.push(instruction.toString());
        }
        return res.join('\n');
    }

    public produce(instruction: Instruction): void {
        this._instructions.push(instruction);
    }

    /* Return the instruction at the given location */
    public at(ip: number): Instruction {
        if (ip >= 0 && ip < this._instructions.length) {
            return this._instructions[ip];
        } else {
            throw Error('Code: instruction pointer out of range.');
        }
    }

    /* Return a dictionary mapping label names to their corresponding
     * instruction pointers. */
    public labelTargets(): Record<string, number> {
        const labelTargets = {};
        for (let i = 0; i < this._instructions.length; i++) {
            if (this._instructions[i].opcode === I_Label) {
                const label = (this._instructions[i] as ILabel).label;
                if (label in labelTargets) {
                    throw Error('Code: label "' + label + '" is repeated.');
                }
                labelTargets[label] = i;
            }
        }
        return labelTargets;
    }
}

function argToString(arg: any): string {
    if (arg instanceof Array) {
        const res = [];
        for (const elem of arg) {
            res.push(argToString(elem));
        }
        return '[' + res.join(', ') + ']';
    } else {
        return arg.toString();
    }
}

export class Instruction {
    public _opcode: symbol;
    public _args: any[];
    public _startPos: SourceReader;
    public _endPos: SourceReader;

    public constructor(opcode: symbol, args: any[]) {
        this._opcode = opcode;
        this._args = args;
        this._startPos = UnknownPosition;
        this._endPos = UnknownPosition;
    }

    public toString(): string {
        const opcode = Symbol.keyFor(this._opcode).substring(2);
        const sargs = [];
        for (const arg of this._args) {
            sargs.push(argToString(arg));
        }
        return '  ' + opcode + ' ' + sargs.join(', ');
    }

    public get opcode(): symbol {
        return this._opcode;
    }

    public get args(): string[] {
        return this._args;
    }

    public set startPos(position: SourceReader) {
        this._startPos = position;
    }

    public get startPos(): SourceReader {
        return this._startPos;
    }

    public set endPos(position: SourceReader) {
        this._endPos = position;
    }

    public get endPos(): SourceReader {
        return this._endPos;
    }
}

/* Push a constant on the stack. */

export class IPushInteger extends Instruction {
    public constructor(number: number) {
        super(I_PushInteger, [number]);
    }

    public get number(): number {
        return this._args[0];
    }
}

export class IPushString extends Instruction {
    public constructor(string: string) {
        super(I_PushString, [string]);
    }

    public get string(): string {
        return this._args[0];
    }
}

/* Push a local index/variable/parameter on the stack. */
export class IPushVariable extends Instruction {
    public constructor(variableName: string) {
        super(I_PushVariable, [variableName]);
    }

    public get variableName(): string {
        return this._args[0];
    }
}

/* Set a local index/variable/parameter to the value on the top of the stack. */
export class ISetVariable extends Instruction {
    public constructor(variableName: string) {
        super(I_SetVariable, [variableName]);
    }

    public get variableName(): string {
        return this._args[0];
    }
}

/* Unset a local index/variable/parameter.
 * This should be used to avoid the variable being used after the end
 * of its scope.
 *
 * E.g. "i" should have no value after the end of the foreach:
 *
 *   foreach i in [1,2,3] {
 *   }
 *   x := i
 */
export class IUnsetVariable extends Instruction {
    public constructor(variableName: string) {
        super(I_UnsetVariable, [variableName]);
    }

    public get variableName(): string {
        return this._args[0];
    }
}

/* Pseudo-instruction to mark the target of a jump. */
export class ILabel extends Instruction {
    public constructor(label: string) {
        super(I_Label, [label]);
    }

    public toString(): string {
        return this.label + ':';
    }

    public get label(): string {
        return this._args[0];
    }
}

/* Unconditional jump. */
export class IJump extends Instruction {
    public constructor(targetLabel: string) {
        super(I_Jump, [targetLabel]);
    }

    public get targetLabel(): string {
        return this._args[0];
    }
}

/* Jump if the top of the stack is False.
 * Pops the top of the stack. */
export class IJumpIfFalse extends Instruction {
    public constructor(targetLabel: string) {
        super(I_JumpIfFalse, [targetLabel]);
    }

    public get targetLabel(): string {
        return this._args[0];
    }
}

/* Jump if the top of the stack is a structure built using the given
 * constructor. Does NOT pop the top of the stack. */
export class IJumpIfStructure extends Instruction {
    public constructor(constructorName: string, targetLabel: string) {
        super(I_JumpIfStructure, [constructorName, targetLabel]);
    }

    public get constructorName(): string {
        return this._args[0];
    }

    public get targetLabel(): string {
        return this._args[1];
    }
}

/* Jump if the top of the stack is an n-tuple of the given size.
 * Does NOT pop the top of the stack. */
export class IJumpIfTuple extends Instruction {
    public constructor(size: number, targetLabel: string) {
        super(I_JumpIfTuple, [size, targetLabel]);
    }

    public get size(): number {
        return this._args[0];
    }

    public get targetLabel(): string {
        return this._args[1];
    }
}

/* Call a subroutine (procedure or function).
 * The arguments are expected to be located in the stack
 * with the last one at the top.
 *
 * The arguments are popped from the current frame and pushed
 * onto the new frame.
 */
export class ICall extends Instruction {
    public constructor(targetLabel: string, nargs: number) {
        super(I_Call, [targetLabel, nargs]);
    }

    public get targetLabel(): string {
        return this._args[0];
    }

    public get nargs(): number {
        return this._args[1];
    }
}

/* Return from a routine to the caller.
 * If returning a value (from a function or program),
 * it must be on the top of the stack. */
export class IReturn extends Instruction {
    public constructor() {
        super(I_Return, []);
    }
}

/* Make a tuple of the given size.
 * The components are expected to be located in the stack
 * with the last one at the top. */
export class IMakeTuple extends Instruction {
    public constructor(size: number) {
        super(I_MakeTuple, [size]);
    }

    public get size(): number {
        return this._args[0];
    }
}

/* Make a list of the given size.
 * The elements are expected to be located in the stack
 * with the last one at the top. */
export class IMakeList extends Instruction {
    public constructor(size: number) {
        super(I_MakeList, [size]);
    }

    public get size(): number {
        return this._args[0];
    }
}

/* Make a structure using the given constructor and the given fields.
 * The values of the fields are expected to be located in the stack
 * with the last one at the top. */
export class IMakeStructure extends Instruction {
    public constructor(typeName: string, constructorName: string, fieldNames: string[]) {
        super(I_MakeStructure, [typeName, constructorName, fieldNames]);
    }

    public get typeName(): string {
        return this._args[0];
    }

    public get constructorName(): string {
        return this._args[1];
    }

    public get fieldNames(): string[] {
        return this._args[2];
    }
}

/* Update a structure built using the given constructor with the given
 * fields.
 * The stack should have a structure built using the given constructor,
 * followed by the values of the fields that are expected.
 * The last field should be at the top. */
export class IUpdateStructure extends Instruction {
    public constructor(typeName: string, constructorName: string, fieldNames: string[]) {
        super(I_UpdateStructure, [typeName, constructorName, fieldNames]);
    }

    public get typeName(): string {
        return this._args[0];
    }

    public get constructorName(): string {
        return this._args[1];
    }

    public get fieldNames(): string[] {
        return this._args[2];
    }
}

/* Read the n-th component from the tuple at the top of the stack.
 * Does not pop the tuple. */
export class IReadTupleComponent extends Instruction {
    public constructor(index: number) {
        super(I_ReadTupleComponent, [index]);
    }

    public get index(): number {
        return this._args[0];
    }
}

/* Read the given field from the structure at the top of the stack.
 * Does not pop the structure. */
export class IReadStructureField extends Instruction {
    public constructor(fieldName: string) {
        super(I_ReadStructureField, [fieldName]);
    }

    public get fieldName(): string {
        return this._args[0];
    }
}

/* Read the given field from the structure at the top of the stack.
 * Pop the structure. */
export class IReadStructureFieldPop extends Instruction {
    public constructor(fieldName: string) {
        super(I_ReadStructureFieldPop, [fieldName]);
    }

    public get fieldName(): string {
        return this._args[0];
    }
}

/* Add the topmost elements of the stack (used mostly for testing/debugging) */
export class IAdd extends Instruction {
    public constructor() {
        super(I_Add, []);
    }
}

/* Duplicate the top of the stack (there should be at least one element) */
export class IDup extends Instruction {
    public constructor() {
        super(I_Dup, []);
    }
}

/* Pop the top of the stack (there should be at least one element) */
export class IPop extends Instruction {
    public constructor() {
        super(I_Pop, []);
    }
}

/* Call a primitive function.
 *
 * The arguments are expected to be located in the stack
 * with the last one at the top.
 *
 * Note: the compiler relies on various primitive functions.
 * For example, the operation to make a range is a primitive
 * function:
 *
 *   function _makeRange(start, end)
 *
 * So is the function that checks whether the top of the stack is a list,
 * etc. (required to compile a "foreach"), and so on.
 */
export class IPrimitiveCall extends Instruction {
    public constructor(primitiveName: string, nargs: number) {
        super(I_PrimitiveCall, [primitiveName, nargs]);
    }

    public get primitiveName(): string {
        return this._args[0];
    }

    public get nargs(): number {
        return this._args[1];
    }
}

/* Save the global state (when entering a function) */
export class ISaveState extends Instruction {
    public constructor() {
        super(I_SaveState, []);
    }
}

/* Restore the global state (when leaving a function) */
export class IRestoreState extends Instruction {
    public constructor() {
        super(I_RestoreState, []);
    }
}

/* Check that the top of the stack has the given type.
 * Does not pop the top of the stack. */
export class ITypeCheck extends Instruction {
    public constructor(type: Type) {
        super(I_TypeCheck, [type]);
    }

    public get type(): Type {
        return this._args[0];
    }
}
