import {UInt64} from './util';


export const enum SIZE {
    BYTE = 8,
    WORD = 16,
    DOUBLE = 32,
    QUAD = 64,
}


// # General operand used in our assembly "language".

export abstract class Operand {

    // Convenience method to get `Register` associated with `Register` or `Memory`.
    reg(): Register {
        return null;
    }

    isRegister(): boolean {
        return this instanceof Register;
    }

    isMemory(): boolean {
        return this instanceof Memory;
    }

    toString(): string {
        return '[operand]';
    }
}


// ## Constant
//
// Constants are everything where we directly type in a `number` value.

export type number64 = [number, number];

export class Constant extends Operand {

    static sizeClass(value) {
        if((value <= 0x7f) && (value >= -0x80))             return SIZE.BYTE;
        if((value <= 0x7fff) && (value >= -0x8000))         return SIZE.WORD;
        if((value <= 0x7fffffff) && (value >= -0x80000000)) return SIZE.DOUBLE;
        return SIZE.QUAD;
    }

    // Size in bits.
    size: number = 32;

    value: number|number64 = 0;

    // Each byte as a `number` in reverse order.
    octets: number[] = [];

    constructor(value: number|number64) {
        super();
        this.Value = value;
    }

    set Value(value: number|number64) {
        if(value instanceof Array) {
            if(value.length !== 2) throw TypeError('number64 must be a 2-tuple, given: ' + value);
            this.setValue64(value as number64);
        } else if(typeof value === 'number') {
            /* JS integers are 53-bit, so split here `number`s over 32 bits into [number, number]. */
            if(Constant.sizeClass(value) === SIZE.QUAD) this.setValue64([UInt64.lo(value), UInt64.hi(value)]);
            else                                        this.setValue32(value);
        } else
            throw TypeError('Constant value must be of type number|number64.');
    }

    protected setValue32(value: number) {
        var size = Constant.sizeClass(value);
        this.size = size;
        this.value = value;
        this.octets = [];
        this.octets[0] = value & 0xFF;
        if(size > SIZE.BYTE) this.octets[1] = (value >> 8) & 0xFF;
        if(size > SIZE.WORD) {
            this.octets[2] = (value >> 16) & 0xFF;
            this.octets[3] = (value >> 24) & 0xFF;
        }
    }

    protected setValue64(value: number64) {
        this.size = 64;
        this.value = value;
        this.octets = [];
        var [lo, hi] = value;
        this.octets[0] = (lo) & 0xFF;
        this.octets[1] = (lo >> 8) & 0xFF;
        this.octets[2] = (lo >> 16) & 0xFF;
        this.octets[3] = (lo >> 24) & 0xFF;
        this.octets[4] = (hi) & 0xFF;
        this.octets[5] = (hi >> 8) & 0xFF;
        this.octets[6] = (hi >> 16) & 0xFF;
        this.octets[7] = (hi >> 24) & 0xFF;
    }

    zeroExtend(size) {
        if(this.size > size) throw Error(`Already larger than ${size} bits, cannot zero-extend.`);
        var missing_bytes = (size - this.size) / 8;
        this.size = size;
        for(var i = 0; i < missing_bytes; i++) this.octets.push(0);
    }

    toString() {
        return `const[${this.size}]: ${this.value}`;
    }
}

export class ImmediateValue extends Constant {}

export class DisplacementValue extends Constant {
    static SIZE = {
        DISP8:  SIZE.BYTE,
        DISP32: SIZE.DOUBLE,
    };

    size = DisplacementValue.SIZE.DISP8;

    constructor(value: number) {
        super(value);
    }

    protected setValue32(value: number) {
        super.setValue32(value);
        /* Make sure `Displacement` is 1 or 4 bytes, not 2. */
        if(this.size > DisplacementValue.SIZE.DISP8) this.zeroExtend(DisplacementValue.SIZE.DISP32);
    }

    protected setValue64() {
        throw TypeError(`Displacement can be only of these sizes: ${DisplacementValue.SIZE.DISP8} and ${DisplacementValue.SIZE.DISP32}.`);
    }
}


// # Scale
//
// `Scale` used in SIB byte in two bit `SCALE` field.
export class Scale extends Operand {
    static VALUES = [1, 2, 4, 8];

    value: number;

    constructor(scale: number = 1) {
        super();
        if(Scale.VALUES.indexOf(scale) < 0)
            throw TypeError(`Scale must be one of [1, 2, 4, 8].`);
        this.value = scale;
    }

    toString() {
        return '' + this.value;
    }
}


// ## Registers
//
// `Register` represents one of `%rax`, `%rbx`, etc. registers.
export enum R64 {
    RAX = 0,
    RCX,
    RDX,
    RBX,
    RSP,
    RBP,
    RSI,
    RDI,
    R8,
    R9,
    R10,
    R11,
    R12,
    R13,
    R14,
    R15,
}

export enum R32 {
    EAX = 0,
    ECX,
    EDX,
    EBX,
    ESP,
    EBP,
    ESI,
    EDI,
    R8D,
    R9D,
    R10D,
    R11D,
    R12D,
    R13D,
    R14D,
    R15D,
}

export enum R8 {
    AL = 0,
    CL,
    DL,
    BL,
    SPL,
    BPL,
    SIL,
    DIL,
    R8B,
    R9B,
    R10B,
    R11B,
    R12B,
    R13B,
    R14B,
    R15B,
}

export class Register extends Operand {
    id: number = 0;                 // Number value of register.
    size: SIZE = SIZE.QUAD;         // Size in bits

    constructor(id: number, size: SIZE) {
        super();
        this.id = id;
        this.size = size;
    }

    reg(): Register {
        return this;
    }

    ref(): Memory {
        return (new Memory).ref(this);
    }

    disp(value: number): Memory {
        return (new Memory).ref(this).disp(value);
    }

    // Whether the register is one of `%r8`, `%r9`, etc. extended registers.
    isExtended() {
        return this.id > 0b111;
    }

    get3bitId() {
        return this.id & 0b111;
    }

    getName() {
        switch(this.size) {
            case SIZE.QUAD:     return R64[this.id].toLowerCase();
            case SIZE.DOUBLE:   return R32[this.id].toLowerCase();
            case SIZE.BYTE:     return R8[this.id].toLowerCase();
            default:            return 'unknown';
        }
    }

    toString() {
        return '%' + this.getName();
    }
}

export class Register64 extends Register {
    constructor(id: number) {
        super(id, SIZE.QUAD);
    }
}

export class Register32 extends Register {
    constructor(id: number) {
        super(id, SIZE.DOUBLE);
    }
}

export class Register16 extends Register {
    constructor(id: number) {
        super(id, SIZE.WORD);
    }
}

export class Register8 extends Register {
    constructor(id: number) {
        super(id, SIZE.BYTE);
    }
}

export var rax  = new Register64(R64.RAX);
export var rbx  = new Register64(R64.RBX);
export var rcx  = new Register64(R64.RCX);
export var rdx  = new Register64(R64.RDX);
export var rsi  = new Register64(R64.RSI);
export var rdi  = new Register64(R64.RDI);
export var rbp  = new Register64(R64.RBP);
export var rsp  = new Register64(R64.RSP);
export var r8   = new Register64(R64.R8);
export var r9   = new Register64(R64.R9);
export var r10  = new Register64(R64.R10);
export var r11  = new Register64(R64.R11);
export var r12  = new Register64(R64.R12);
export var r13  = new Register64(R64.R13);
export var r14  = new Register64(R64.R14);
export var r15  = new Register64(R64.R15);


export var eax  = new Register32(R32.EAX);
export var ebx  = new Register32(R32.EBX);
export var ecx  = new Register32(R32.ECX);
export var edx  = new Register32(R32.EDX);
export var esi  = new Register32(R32.ESI);
export var edi  = new Register32(R32.EDI);
export var ebp  = new Register32(R32.EBP);
export var esp  = new Register32(R32.ESP);
export var r8d  = new Register32(R32.R8D);
export var r9d  = new Register32(R32.R9D);
export var r10d = new Register32(R32.R10D);
export var r11d = new Register32(R32.R11D);
export var r12d = new Register32(R32.R12D);
export var r13d = new Register32(R32.R13D);
export var r14d = new Register32(R32.R14D);
export var r15d = new Register32(R32.R15D);


export var al   = new Register8(R8.AL);
export var bl   = new Register8(R8.BL);
export var cl   = new Register8(R8.CL);
export var dl   = new Register8(R8.DL);
export var sil  = new Register8(R8.SIL);
export var dil  = new Register8(R8.DIL);
export var bpl  = new Register8(R8.BPL);
export var spl  = new Register8(R8.SPL);
export var r8b  = new Register8(R8.R8B);
export var r9b  = new Register8(R8.R9B);
export var r10b = new Register8(R8.R10B);
export var r11b = new Register8(R8.R11B);
export var r12b = new Register8(R8.R12B);
export var r13b = new Register8(R8.R13B);
export var r14b = new Register8(R8.R14B);
export var r15b = new Register8(R8.R15B);


// ## Memory
//
// `Memory` is RAM addresses which `Register`s can *dereference*.
export class Memory extends Operand {
    base: Register = null;
    index: Register = null;
    scale: Scale = null;
    displacement: DisplacementValue = null;

    reg(): Register {
        if(this.base) return this.base;
        if(this.index) return this.index;
        // throw Error('No backing register.');
        return null;
    }

    needsSib() {
        return !!this.index || !!this.scale;
    }

    ref(base: Register): this {
        this.base = base;
        return this;
    }

    disp(value: number): this {
        this.displacement = new DisplacementValue(value);
        return this;
    }

    toString() {
        var base = this.base ? this.base.toString() : '';
        var index = this.index ? this.index.toString() : '';
        var scale = this.scale ? this.scale.toString() : '';
        var disp = this.disp ? this.disp.toString() : '';
        return `[%${base} + %{index} * ${scale} + ${disp}]`;
    }
}
