const fs = require('fs');
const memoryjs = require('./build/Release/memoryjs');
const Debugger = require('./src/debugger');
const constants = require('./src/consts');
const { STRUCTRON_TYPE_STRING } = require('./src/utils');

function openProcess(processIdentifier, callback) {
  if (arguments.length === 1) {
    return memoryjs.openProcess(processIdentifier);
  }

  return memoryjs.openProcess(processIdentifier, callback);
}

function closeProcess(handle) {
  return memoryjs.closeProcess(handle);
}

function getProcesses(callback) {
  if (arguments.length === 0) {
    return memoryjs.getProcesses();
  }

  return memoryjs.getProcesses(callback);
}

function findModule(moduleName, processId, callback) {
  if (arguments.length === 2) {
    return memoryjs.findModule(moduleName, processId);
  }

  return memoryjs.findModule(moduleName, processId, callback);
}

function getModules(processId, callback) {
  if (arguments.length === 1) {
    return memoryjs.getModules(processId);
  }

  return memoryjs.getModules(processId, callback);
}

function readMemory(handle, address, dataType, callback) {
  if (dataType.toLowerCase().endsWith('_be')) {
    return readMemoryBE(handle, address, dataType, callback);
  }

  if (arguments.length === 3) {
    return memoryjs.readMemory(handle, address, dataType.toLowerCase());
  }

  return memoryjs.readMemory(handle, address, dataType.toLowerCase(), callback);
}

function readMemoryBE(handle, address, dataType, callback) {
  let value = null;

  switch (dataType) {
    case constants.INT64_BE:
      value = readBuffer(handle, address, 8).readBigInt64BE();
      break;

    case constants.UINT64_BE:
      value = readBuffer(handle, address, 8).readBigUInt64BE();
      break;

    case constants.INT32_BE:
    case constants.INT_BE:
    case constants.LONG_BE:
      value = readBuffer(handle, address, 4).readInt32BE();
      break;

    case constants.UINT32_BE:
    case constants.UINT_BE:
    case constants.ULONG_BE:
      value = readBuffer(handle, address, 4).readUInt32BE();
      break;

    case constants.INT16_BE:
    case constants.SHORT_BE:
      value = readBuffer(handle, address, 2).readInt16BE();
      break;

    case constants.UINT16_BE:
    case constants.USHORT_BE:
      value = readBuffer(handle, address, 2).readUInt16BE();
      break;

    case constants.FLOAT_BE:
      value = readBuffer(handle, address, 4).readFloatBE();
      break;

    case constants.DOUBLE_BE:
      value = readBuffer(handle, address, 8).readDoubleBE();
      break;
  }

  if (typeof callback !== 'function') {
    if (value === null) {
      throw new Error('Invalid data type argument!');
    }

    return value;
  }

  callback(value === null ? 'Invalid data type argument!' : '', value);
}

function readBuffer(handle, address, size, callback) {
  if (arguments.length === 3) {
    return memoryjs.readBuffer(handle, address, size);
  }

  return memoryjs.readBuffer(handle, address, size, callback);
}

function writeMemory(handle, address, value, dataType) {
  let dataValue = value;
  if (dataType === constants.STR || dataType === constants.STRING) {
    dataValue += '\0'; // add terminator
  }

  const bigintTypes = [constants.INT64, constants.INT64_BE, constants.UINT64, constants.UINT64_BE];
  if (bigintTypes.indexOf(dataType) != -1 && typeof value !== 'bigint') {
    throw new Error(`${dataType.toUpperCase()} expects type BigInt`);
  }

  if (dataType.endsWith('_be')) {
    return writeMemoryBE(handle, address, dataValue, dataType);
  }

  return memoryjs.writeMemory(handle, address, dataValue, dataType.toLowerCase());
}

function writeMemoryBE(handle, address, value, dataType) {
  let buffer = null;

  switch (dataType) {
    case constants.INT64_BE:
      if (typeof value !== 'bigint') {
        throw new Error('INT64_BE expects type BigInt');
      }
      buffer = Buffer.alloc(8);
      buffer.writeBigInt64BE(value);
      break;

    case constants.UINT64_BE:
      if (typeof value !== 'bigint') {
        throw new Error('UINT64_BE expects type BigInt');
      }
      buffer = Buffer.alloc(8);
      buffer.writeBigUInt64BE(value);
      break;

    case constants.INT32_BE:
    case constants.INT_BE:
    case constants.LONG_BE:
      buffer = Buffer.alloc(4);
      buffer.writeInt32BE(value);
      break;

    case constants.UINT32_BE:
    case constants.UINT_BE:
    case constants.ULONG_BE:
      buffer = Buffer.alloc(4);
      buffer.writeUInt32BE(value);
      break;

    case constants.INT16_BE:
    case constants.SHORT_BE:
      buffer = Buffer.alloc(2);
      buffer.writeInt16BE(value);
      break;

    case constants.UINT16_BE:
    case constants.USHORT_BE:
      buffer = Buffer.alloc(2);
      buffer.writeUInt16BE(value);
      break;

    case constants.FLOAT_BE:
      buffer = Buffer.alloc(4);
      buffer.writeFloatBE(value);
      break;

    case constants.DOUBLE_BE:
      buffer = Buffer.alloc(8);
      buffer.writeDoubleBE(value);
      break;
  }

  if (buffer == null) {
    throw new Error('Invalid data type argument!');
  }

  writeBuffer(handle, address, buffer);
}

function writeBuffer(handle, address, buffer) {
  return memoryjs.writeBuffer(handle, address, buffer);
}

function findPattern() {
  const findPattern           = ['number', 'string', 'number', 'number'].toString();
  const findPatternByModule   = ['number', 'string', 'string', 'number', 'number'].toString();
  const findPatternByAddress  = ['number', 'number', 'string', 'number', 'number'].toString();

  const args = Array.from(arguments).map(arg => typeof arg);

  if (args.slice(0, 4).toString() === findPattern) {
    if (args.length === 4 || (args.length === 5 && args[4] === 'function')) {
      return memoryjs.findPattern(...arguments);
    }
  }

  if (args.slice(0, 5).toString() === findPatternByModule) {
    if (args.length === 5 || (args.length === 6 && args[5] === 'function')) {
      return memoryjs.findPatternByModule(...arguments);
    }
  }

  if (args.slice(0, 5).toString() === findPatternByAddress) {
    if (args.length === 5 || (args.length === 6 && args[5] === 'function')) {
      return memoryjs.findPatternByAddress(...arguments);
    }
  }

  throw new Error('invalid arguments!');
}

function callFunction(handle, args, returnType, address, callback) {
  if (arguments.length === 4) {
    return memoryjs.callFunction(handle, args, returnType, address);
  }

  return memoryjs.callFunction(handle, args, returnType, address, callback);
}

function virtualAllocEx(handle, address, size, allocationType, protection, callback) {
  if (arguments.length === 5) {
    return memoryjs.virtualAllocEx(handle, address, size, allocationType, protection);
  }

  return memoryjs.virtualAllocEx(handle, address, size, allocationType, protection, callback);
}

function virtualProtectEx(handle, address, size, protection, callback) {
  if (arguments.length === 4) {
    return memoryjs.virtualProtectEx(handle, address, size, protection);
  }

  return memoryjs.virtualProtectEx(handle, address, size, protection, callback);
}

function getRegions(handle, getOffsets, callback) {
  if (arguments.length === 1) {
    return memoryjs.getRegions(handle);
  }

  return memoryjs.getRegions(handle, callback);
}

function virtualQueryEx(handle, address, callback) {
  if (arguments.length === 2) {
    return memoryjs.virtualQueryEx(handle, address);
  }

  return memoryjs.virtualQueryEx(handle, address, callback);
}

function injectDll(handle, dllPath, callback) {
  if (!dllPath.endsWith('.dll')) {
    throw new Error("Given path is invalid: file is not of type 'dll'.");
  }

  if (!fs.existsSync(dllPath)) {
    throw new Error('Given path is invaild: file does not exist.');
  }

  if (arguments.length === 2) {
    return memoryjs.injectDll(handle, dllPath);
  }

  return memoryjs.injectDll(handle, dllPath, callback);
}

function unloadDll(handle, module, callback) {
  if (arguments.length === 2) {
    return memoryjs.unloadDll(handle, module);
  }

  return memoryjs.unloadDll(handle, module, callback);
}

const library = {
  openProcess,
  closeProcess,
  getProcesses,
  findModule,
  getModules,
  readMemory,
  readBuffer,
  writeMemory,
  writeBuffer,
  findPattern,
  callFunction,
  virtualAllocEx,
  virtualProtectEx,
  getRegions,
  virtualQueryEx,
  injectDll,
  unloadDll,
  attachDebugger: memoryjs.attachDebugger,
  detatchDebugger: memoryjs.detatchDebugger,
  awaitDebugEvent: memoryjs.awaitDebugEvent,
  handleDebugEvent: memoryjs.handleDebugEvent,
  setHardwareBreakpoint: memoryjs.setHardwareBreakpoint,
  removeHardwareBreakpoint: memoryjs.removeHardwareBreakpoint,
  Debugger: new Debugger(memoryjs),
};

module.exports = {
  ...constants,
  ...library,
  STRUCTRON_TYPE_STRING: STRUCTRON_TYPE_STRING(library),
};
