import { Table } from '32bit-adressing-table-modrm';
import { ALL_TABLES, MOD_RM_SET, modRmTypes, operandTypes, Operation } from './constants/tables';
import { isNotANumber } from './helper/isNotANumber';
import { ALL_REGISTERS } from './constants/sets/register.set';
import '../utils/string.extensions';
import { rotate } from './helper/rotate';
import { removeTrailingZero } from './helper/removeTrailingZero';
import { removeFalsy } from '../utils/object.extensions';
import { typeTable } from './constants/tables/Types.table';
import { register } from 'ts-node';
import { JMP_TABLE } from './constants/tables/Jump.table';
import { makeHexLengthEven } from './helper/makeHexLengthEven';
import { convertToTwosComp } from './helper/twosComplement';
import { makeValueToByte } from './helper/makeValueToByte';

interface OpCode {
  instruction: string;
  codes: Code[];
}

interface Code {
  value: string;
  length?: length;
}

interface OptionsInterface {
  is16Bit?: boolean;
  isAddress?: boolean;
  isSignExtended?: boolean;
}

interface IOperand {
  operand1?: operand;
  operand2?: operand;
  pointer?: pointerType;
}

type length = 'b' | 'w' | 'd';

export class _Disassembler {
  private prefixMap = new Map<string, prefixModes>().set('66', 'registerMode').set('67', 'addressMode');
  private opCodeTable = ALL_TABLES;
  private position = 0;

  private *byteIterator(s: string): IterableIterator<{ byte: string; position: number }> {
    for (let i = 0; i < s.length; i += 2) {
      yield { byte: s[i] + s[i + 1], position: i === 0 ? 0 : i / 2 };
    }
  }

  generateInstructions(code: string): Instruction[] {
    code = code.toUpperCase().trim();
    const result: Instruction[] = [];
    const iterator = this.byteIterator(code);
    const next = () => {
      const val = iterator.next();
      this.position = val.value.position;
      return val.value.byte;
    };
    while (true) {
      const currentByte = iterator.next();
      if (currentByte.done) {
        return result;
      }
      const position = currentByte.value.position;
      let instruction = this.getInstruction(currentByte.value.byte, next);
      instruction = removeFalsy({
        ...instruction,
        operand1: removeFalsy(instruction.operand1),
        operand2: removeFalsy(instruction.operand2),
      });
      result.push({ ...instruction, position });
    }
  }

  private getInstruction(currentByte: string, next: () => string): Instruction {
    let is16Bit = false;
    let isAddress = false;
    const prefix1 = this.fetchPrefix(currentByte);
    let prefix2;
    if (prefix1) {
      // deal with prefixes
      currentByte = next();
      switch (prefix1) {
        case 'addressMode':
          isAddress = true;
          break;
        case 'registerMode':
          is16Bit = true;
      }
      prefix2 = this.fetchPrefix(currentByte);
      if (prefix2) {
        currentByte = next();
        switch (prefix2) {
          case 'addressMode':
            isAddress = true;
            break;
          case 'registerMode':
            is16Bit = true;
        }
      }
    }
    let operation: Operation | undefined;
    let tableResult: [string[], string[]] | undefined;
    if (MOD_RM_SET.has(currentByte)) {
      const nextByte = next();
      tableResult = Table.getReverseValueFromTable(nextByte, isAddress ? '16rm' : '32rm');
      if (tableResult) {
        const modRm = tableResult[1].filter((s) => !isNotANumber(s))[0] as modRmTypes;
        operation = this.opCodeTable.get({ opcode: currentByte, modRm });
      }
    } else {
      operation = this.opCodeTable.get({ opcode: currentByte });
      if (this.operationIsJump(operation?.operation)) {
        return this.processJump(operation, next);
      }
      if (operation && !operation?.op1) {
        // if instruction does not have any operand
        if (operation?.constant) {
          //eg int 03
          return { instruction: operation.operation, operand1: { value: operation.constant } };
        }
        return { instruction: operation!.operation };
      }
      let registerCode = 0;
      if (!operation) {
        // op might contain a register code
        for (let i = 1; i <= 7; i++) {
          const substracted = (parseInt(currentByte, 16) - i).toString(16).toUpperCase();
          operation = this.opCodeTable.get({ opcode: substracted });
          if (operation) {
            registerCode = i;
            break;
          }
        }
      }
      if (!operation) {
        // check for 2 bytes
        operation = this.opCodeTable.get({ opcode: currentByte + next() });
        if (this.operationIsJump(operation?.operation)) {
          return this.processJump(operation, next);
        }
      }
      if (operation?.type) {
        const type = is16Bit ? 'rw' : operation.type;
        const reg = typeTable.get(type)?.get(registerCode)!;
        switch (type) {
          case 'rb':
            tableResult = [[], [reg]];
            break;
          case 'rw':
            tableResult = [[], ['', reg]];
            break;
          case 'rd':
            tableResult = [[], ['', '', reg]];
            break;
        }
      } else {
        if (!operation?.isRegisterIncluded && operation?.op2) {
          const nextByte = next();
          tableResult = Table.getReverseValueFromTable(nextByte, isAddress ? '16rm' : '32rm');
        }
      }
    }
    if (!operation) {
      throw Error('invalid code');
    }
    const operands = this.processOperand(operation.op1!, operation.op2!, next, tableResult, {
      is16Bit,
      isAddress,
      isSignExtended: operation.isSignExtended,
    });
    return { instruction: operation.operation, ...operands };
  }

  private operationIsJump(operation: string | undefined) {
    let isJump = false;
    if (operation) {
      JMP_TABLE.forEach((_, v) => {
        if (v.operation === operation) {
          isJump = true;
        }
      });
    }
    return isJump;
  }

  private fetchPrefix(b: string) {
    return this.prefixMap.get(b);
  }

  private checkRegister(
    op1: string,
    op2: string,
    options?: OptionsInterface,
  ): [string | undefined, string | undefined] {
    const isRegister = (op: string): string | undefined => {
      if (ALL_REGISTERS.has(op)) {
        if (options?.is16Bit) {
          // convert eax to ax
          return op.replace('e', '');
        }
        return op;
      }
      return undefined;
    };
    return [isRegister(op1), isRegister(op2)];
  }

  private processOperand(
    op1: operandTypes,
    op2: operandTypes,
    next: Function,
    tableResult?: [string[], string[]],
    options?: OptionsInterface,
  ): IOperand {
    let operand1;
    let operand2;
    //check register
    [operand1, operand2] = this.checkRegister(op1, op2, options);
    if (operand1 || operand2) {
      if (operand1) {
        // e.g 04
        if (!op2) {
          return { operand1: { register: operand1 } };
        }
        if (op2 === 'imm8') {
          operand2 = next();
          return { operand1: { register: operand1 }, operand2: { value: operand2 } };
        }
        if (op2 === 'imm32') {
          operand2 = next() + next();
          if (!options?.is16Bit) {
            // if 32 bit
            operand2 = operand2 + next() + next();
          }
          operand2 = removeTrailingZero(rotate(operand2));
          return { operand1: { register: operand1 }, operand2: { value: operand2 } };
        }
        if (op2.includes('r')) {
          operand2 = tableResult![1][options?.is16Bit ? 1 : 2];
          return { operand1: { register: operand1 }, operand2: { register: operand2 } };
        }
      }
      if (operand2) {
        // e.g 04
        if (op1 === 'imm8') {
          operand1 = next();
          return { operand1: { register: operand1 }, operand2: { value: operand2 } };
        }
        if (op1 === 'imm32') {
          operand1 = next() + next();
          if (!options?.is16Bit) {
            // if 32 bit
            operand1 = operand1 + next() + next();
          }
          return { operand1: { register: operand1 }, operand2: { value: rotate(operand1) } };
        }
      }
      return { operand1: { register: operand1 }, operand2: { register: operand2 } };
    }
    if (op1 === 'm8') {
      operand1 = tableResult![0][0];
      let operandObj: operand;
      const isMemory = operand1.includes('[');
      let secondRegister;
      if (operand1.includes('disp')) {
        const disp = this.processDisp(operand1, next, options);
        operandObj = { ...disp?.operand, pointer: 'b' };
      } else {
        if (operand1.includes('[sib]')) {
          operandObj = this.processSIB(next, options)!;
          operandObj.pointer = 'b';
        } else {
          if (isMemory) {
            if (operand1.includes('+')) {
              [operand1, secondRegister] = operand1.removeBrackets().split('+');
              operandObj = { register: operand1, register2: secondRegister, pointer: 'b' };
            } else {
              operand1 = operand1.removeBrackets();
              operandObj = { register: operand1, pointer: 'b' };
            }
          }
        }
      }
      if (!op2) {
        return { operand1: operandObj! };
      }
      if (op2.includes('imm')) {
        const operand2 = this.processImm(op2, next, options)!;
        return {
          operand1: operandObj! ?? {
            register: operand1.removeBrackets(),
            pointer: isMemory ? 'b' : undefined,
          },
          operand2: { ...operand2 },
        };
      }
      if (op2 === 'r8') {
        operand2 = tableResult![1][0];
        return {
          operand1: operandObj! ?? {
            register: operand1.removeBrackets(),
            pointer: isMemory ? 'b' : undefined,
          },
          operand2: { register: operand2 },
        };
      }
    }
    if (op1 === 'm32') {
      let opObj: IOperand;
      const tableResultForOp1 = tableResult![0][0];
      if (tableResultForOp1.includes('disp')) {
        const disp = this.processDisp(tableResultForOp1, next, options);
        opObj = { operand1: { ...disp?.operand, pointer: options?.is16Bit ? 'w' : 'd' } };
      } else {
        if (tableResultForOp1.includes('[sib]')) {
          //check sib
          const sib = this.processSIB(next, options)!;
          opObj = { operand1: { ...sib, pointer: options?.is16Bit ? 'w' : 'd' } };
        } else {
          if (tableResultForOp1.includes('[')) {
            // if op1 is memory address
            if (tableResultForOp1.includes('+')) {
              const temp = tableResultForOp1.removeBrackets().split('+');
              opObj = {
                operand1: {
                  register: temp[0],
                  register2: temp[1],
                  pointer: options?.is16Bit ? 'w' : 'd',
                },
              };
            } else {
              operand1 = tableResult![0][0].removeBrackets();
              opObj = { operand1: { register: operand1, pointer: options?.is16Bit ? 'w' : 'd' } };
            }
          } else {
            operand1 = tableResult![0][options?.is16Bit ? 1 : 2];
            opObj = { operand1: { register: operand1 } };
          }
        }
      }
      if (!op2) {
        return opObj;
      }
      if (op2 === 'r32') {
        operand2 = tableResult![1][options?.is16Bit ? 1 : 2];
        return {
          operand1: opObj.operand1,
          operand2: { register: operand2 },
        };
      }
      if (op2.includes('imm')) {
        operand2 = this.processImm(op2, next, options)!;
        return {
          operand1: opObj.operand1,
          operand2,
        };
      }
    }
    if (op1.includes('r')) {
      //register and displacement
      if (op1 === 'r8') {
        operand1 = tableResult![1][0];
        if (!op2) {
          return { operand1: { register: operand1 } };
        }
        if (op2.includes('imm')) {
          return { operand1: { register: operand1 }, operand2: this.processImm(op2, next, options) };
        }
        if (op2 === 'm8') {
          const result = tableResult![0][0];
          if (options?.isAddress) {
            return this.processAddressMode(operand1, result, next, 'b');
          }
          if (result.includes('disp')) {
            const operand2 = this.processDisp(result, next, options);
            return {
              operand1: { register: operand1 },
              operand2: { ...operand2?.operand, pointer: 'b' },
            };
          }
          if (result === '[sib]') {
            const sib = this.processSIB(next, options)!;
            return {
              operand1: { register: operand1 },
              operand2: {
                register: sib.register,
                constant: sib.constant ? sib.constant : '1',
                register2: sib.register2,
                displacement: sib.displacement,
                pointer: 'b',
              },
            };
          }
        }
      }
      if (op1 === 'r32') {
        operand1 = tableResult![1][options?.is16Bit ? 1 : 2];
        if (op2.includes('imm')) {
          operand2 = this.processImm(op2, next, options)!;
          return {
            operand1: { register: operand1 },
            operand2,
          };
        }
        if (op2.includes('m')) {
          const result = tableResult![0].length > 1 ? tableResult![0][options?.is16Bit ? 1 : 2] : tableResult![0][0];
          if (options?.isAddress) {
            return this.processAddressMode(operand1, result, next, options.is16Bit ? 'w' : 'd');
          }
          if (result.includes('disp')) {
            const operand2 = this.processDisp(result, next, options);
            return {
              operand1: { register: operand1 },
              operand2: {
                ...operand2?.operand,
                pointer: op2 === 'm32' ? (options?.is16Bit ? 'w' : 'd') : 'b',
              },
            };
          }
          if (result === '[sib]') {
            const sib = this.processSIB(next, options)!;
            return {
              operand1: { register: operand1 },
              operand2: {
                register: sib.register,
                constant: sib.constant ? sib.constant : '1',
                register2: sib.register2,
                displacement: sib.displacement,
                pointer: options?.is16Bit ? 'w' : 'd',
              },
            };
          }
          operand2 = this.processM(result, op2, next, options);
          return {
            operand1: { register: operand1 },
            operand2,
          };
        }
      }
    }
    if (op1.includes('imm')) {
      if (op1 === 'imm32') {
        return { operand1: { value: this.nextByteXtimes(next, 4) } };
      }
      if (options?.is16Bit) {
        return { operand1: { value: this.nextByteXtimes(next, 2) } };
      }
      return { operand1: { value: this.nextByteXtimes(next) } };
    }
    return { operand1: { register: operand1 } };
  }

  private nextByteXtimes(next: Function, x?: number) {
    let result = '';
    const _x = x ?? 1;
    for (let i = 0; i < _x; i++) {
      result += next();
    }
    return rotate(result);
  }

  private processM(op: string, opType: string, next: Function, options?: OptionsInterface): operand {
    if (op.includes('[')) {
      return { register: op.removeBrackets(), pointer: opType === 'm32' ? (options?.is16Bit ? 'w' : 'd') : 'b' };
    }
    return { register: op };
  }

  private getPointer(s: string) {
    switch (s.length) {
      case 2:
        return 'b';
      case 4:
        return 'w';
      case 8:
        return 'd';
      default:
        throw new Error('byte pointer error');
    }
  }

  private processAddressMode(op1: string, op2: string, next: Function, type: pointerType) {
    const tableResult = op2.removeBrackets().split('+');
    if (tableResult[2]?.includes('disp')) {
      // e.g bx+si+disp
      const disp = tableResult[2];
      let displacement = next();
      if (disp.includes('16')) {
        displacement = displacement + next();
      }
      return {
        operand1: { register: op1 },
        operand2: { register: tableResult[0], register2: tableResult[1], displacement, pointer: type },
      };
    }
    if (tableResult[1]?.includes('disp')) {
      // e.g bx+disp
      const disp = tableResult[1];
      let displacement = next();
      if (disp.includes('16')) {
        displacement = displacement + next();
        if (this.isNegative(displacement, 4)) {
          displacement = '-' + convertToTwosComp(displacement);
        }
      } else {
        if (this.isNegative(displacement, 2)) {
          displacement = '-' + convertToTwosComp(displacement);
        }
      }
      return {
        operand1: { register: op1 },
        operand2: { register: tableResult[0], displacement, pointer: type },
      };
    }
    return {
      operand1: { register: op1 },
      operand2: {
        register: tableResult[0],
        register2: tableResult[1] ? tableResult[1] : undefined,
        pointer: type,
      },
    };
  }

  private processSIB(
    next: Function,
    options?: OptionsInterface,
  ): { displacement?: string; register?: string; register2?: string; constant?: string } | undefined {
    const n = next();
    const sib = Table.getReverseValueFromTable(n, '32sib');
    const [register, constant] = sib[0][0].removeBrackets('[').split('*');
    let displacement;
    let register2;
    if (sib[1][0] !== '[*]') {
      register2 = sib[1][0];
    } else {
      displacement = removeTrailingZero(rotate(next() + next() + next() + next()));
    }
    const result = {
      displacement,
      register: register2 ?? register,
      register2: register2 ? register : undefined,
      constant,
    };
    return result;
  }

  private processDisp(
    op: string,
    next: Function,
    options?: OptionsInterface,
  ): { operand: { displacement?: string; register?: string; register2?: string; constant?: string } } | undefined {
    let displacement;
    let result;

    if (op === 'disp32') {
      displacement = removeTrailingZero(rotate(next() + next() + next() + next()));
      return { operand: { displacement } };
    }
    if (op.includes('disp')) {
      if (op.includes('sib')) {
        result = this.processSIB(next, options);
      }
      if (op.includes('disp32')) {
        displacement = removeTrailingZero(rotate(next() + next() + next() + next()));
        if (this.isNegative(displacement, 8)) {
          displacement = '-' + convertToTwosComp(displacement);
        }
      } else if (op.includes('disp16')) {
        // disp16
        displacement = removeTrailingZero(rotate(next() + next()));
        if (this.isNegative(displacement, 4)) {
          displacement = '-' + convertToTwosComp(displacement);
        }
      } else {
        // disp8
        displacement = removeTrailingZero(rotate(next()));
        if (this.isNegative(displacement, 2)) {
          displacement = '-' + convertToTwosComp(displacement);
        }
      }
      if (result) {
        return { operand: { ...result, displacement } };
      }
    }
    const split = op.removeBrackets().split('+');
    if (split.length > 2) {
      //[bx+si]+disp
      const [register, register2] = split;
      return { operand: { register, register2, displacement } };
    } else {
      // eg [eax + disp32]
      const [register] = split;
      return { operand: { register, displacement } };
    }
  }

  private processImm(op2: string, next: Function, options?: OptionsInterface): operand | undefined {
    switch (op2) {
      case 'imm8':
        let nextByte = next();
        if (options?.isSignExtended && this.isNegative(nextByte)) {
          nextByte = 'FF' + nextByte;
          if (!options.is16Bit) {
            nextByte = 'FFFF' + nextByte;
          }
        }
        return { value: nextByte };
      case 'imm32':
        const operand2 = removeTrailingZero(rotate(next() + next() + (!options?.is16Bit ? next() + next() : '')));
        return { value: operand2 };
    }
  }

  private isNegative(s: string, length?: 2 | 4 | 8) {
    if (length === 2) {
      return parseInt(s, 16) > 127;
    }
    if (length === 4) {
      return parseInt(s, 16) > 32767;
    }
    if (length === 8) {
      return parseInt(s, 16) > 2147483647;
    }
    return parseInt(s, 16) > 127;
  }

  private processJump(operation: Operation | undefined, next: () => string): Instruction {
    const getDestination = (value: string, length: 2 | 4 | 8) => {
      const rotatedVal = rotate(value);
      let _value;
      if (this.isNegative(rotatedVal)) {
        _value = -parseInt(convertToTwosComp(rotatedVal), 16);
      } else {
        _value = parseInt(rotatedVal, 16);
      }
      return makeValueToByte((this.position + 1 + _value).toString(16), length);
    };

    switch (operation?.op1) {
      case 'imm8':
        const value = next();
        return { instruction: operation?.operation, operand1: { value: getDestination(value, 2) } };
      case 'imm16':
        const value2 = next() + next();
        return { instruction: operation?.operation, operand1: { value: getDestination(value2, 4) } };
      default:
        //imm32
        const value3 = next() + next() + next() + next();
        return { instruction: operation?.operation!, operand1: { value: getDestination(value3, 8) } };
    }
  }
}

// export class _Disassembler {
//   private _prefixMap = new Map<string, prefixModes>().set('66', 'registerMode').set('67', 'addressMode');
//   private modRmSet = new Set(['80', '81', '83']);
//   private _opCodeMap = new Map<string, { instruction: string; length?: length }>();
//
//   constructor() {
//     this.generateOpCodeMap([this.createOpCode('add', ['80/0', 'b', '81/0', 'd', '83/0', 'b'])]);
//   }
//
//   private *byteIterator(s: string): IterableIterator<string> {
//     for (let i = 0; i < s.length; i += 2) {
//       yield s[i] + s[i + 1];
//     }
//   }
//
//   disassemble(opCodes: string) {
//     const iterator = this.byteIterator(opCodes);
//     let currentByte = iterator.next().value;
//     const prefix = this.fetchPrefix(currentByte);
//     if (prefix) {
//       currentByte = iterator.next().value;
//     }
//     let modRmByte;
//     let possibleRegisters: string[] = [];
//     if (this.modRmSet.has(currentByte)) {
//       const nextByte = iterator.next().value;
//       const searchResult = Table.getReverseValueFromTable(nextByte);
//       modRmByte = searchResult[1].pop();
//       possibleRegisters = searchResult[0];
//     }
//     const op = this._opCodeMap.get(modRmByte ? currentByte + `/` + modRmByte : currentByte);
//     const instruction = op?.instruction;
//     const operand1 = possibleRegisters.length > 0 ? possibleRegisters[1] : undefined;
//     const operand2 = this.getValueFromLength(iterator, prefix ? 'w' : op?.length);
//     return instruction + ' ' + operand1 + ' ' + operand2;
//   }
//
//   private getValueFromLength(it: IterableIterator<string>, length?: string) {
//     if (length) {
//       switch (length) {
//         case 'b':
//           return it.next().value;
//         case 'd':
//           return it.next().value + it.next().value + it.next().value + it.next().value;
//         case 'w':
//           return it.next().value + it.next().value;
//       }
//     }
//     return '';
//   }
//
//   fetchPrefix(pref: string): prefixModes | undefined {
//     return this._prefixMap.get(pref);
//   }
//
//   private generateOpCodeMap(codes: OpCode[]) {
//     codes.forEach((c) => {
//       c.codes.forEach((c1) => {
//         this._opCodeMap.set(c1.value, { instruction: c.instruction, length: c1.length });
//       });
//     });
//   }
//
//   private createOpCode(ins: string, codeAndLength: string[]): OpCode {
//     const codes = [];
//     for (let i = 0; i < codeAndLength.length; i += 2) {
//       const code = codeAndLength[i];
//       const length = codeAndLength[i + 1];
//       codes.push({ value: code, length: length !== '' ? length : undefined });
//     }
//     return { instruction: ins, codes: (codes as unknown) as Code[] };
//   }
// }

type prefixModes = 'registerMode' | 'addressMode';
export type pointerType = 'b' | 'd' | 'w';

interface operand {
  value?: string;
  register?: string;
  register2?: string;
  displacement?: string;
  constant?: string;
  pointer?: pointerType;
}

export interface Instruction {
  instruction: string;
  operand1?: operand;
  operand2?: operand;
  position?: number;
}
