const { COMMANDS } = require("./constants");
const Command = require("./command");
const MAGIC = "FG";
const ENCODING = "ascii";

class Parser {
  _readMagic() {
    const magic = this._readStringOfLength(MAGIC.length);
    if (magic !== MAGIC) {
      throw new Error("Magic number does not mach. Is this a maki file?");
    }
    return magic;
  }

  _readVersion() {
    // No idea what we're actually expecting here.
    this._i += 2;
  }

  _readTypes() {
    let count = this._readUInt32LE();
    const types = [];
    while (count--) {
      let identifier = "";
      let chunks = 4;
      while (chunks--) {
        identifier += this._readUInt32LE().toString(16);
      }
      types.push(identifier.padStart(32, "0"));
    }
    return types;
  }

  _readFunctionsNames() {
    let count = this._readUInt32LE();
    const functionNames = [];
    while (count--) {
      const func = {};
      func.classCode = this._readUInt16LE();
      // Offset into our parsed types
      func.classType = func.classCode & 0xff;
      func.dummy2 = this._readUInt16LE();
      func.name = this._readString();
      functionNames.push(func);
    }
    return functionNames;
  }

  _readVariables() {
    let count = this._readUInt32LE();
    const variables = [];
    while (count--) {
      const type = this._readUInt8();
      const object = this._readUInt8();
      const subClass = this._readUInt16LE();
      const uinit1 = this._readUInt16LE();
      const uinit2 = this._readUInt16LE();
      const uinit3 = this._readUInt16LE();
      const uinit4 = this._readUInt16LE();
      const global = this._readUInt8();
      const system = this._readUInt8();
      const variable = {
        type,
        object,
        subClass,
        uinit1,
        uinit2,
        uinit3,
        uinit4,
        global,
        system
      };
      variables.push(variable);
    }
    return variables;
  }

  _readConstants() {
    let count = this._readUInt32LE();
    const constants = [];
    while (count--) {
      const varNum = this._readUInt32LE();
      const value = this._readString();
      constants.push({ varNum, value });
    }
    return constants;
  }

  _readFunctions() {
    let count = this._readUInt32LE();
    const functions = [];
    while (count--) {
      const varNum = this._readUInt32LE();
      const funcNum = this._readUInt32LE();
      const offset = this._readUInt32LE();
      functions.push({ varNum, offset, funcNum });
    }
    return functions;
  }

  _readCommands(code) {
    let i = 0;
    while (i < code.length) {
      const opCode = code.charCodeAt(i);
      const command = COMMANDS[opCode];
      if (command == null) {
        // console.warn(`Missing command opCode: ${opCode}`);
        i++;
        continue;
      }
      if (command.arg == null) {
        i++;
        continue;
      }
      switch (command.arg) {
        case "var":
          // This is the index into this._debug.variables
          // this._readUInt32LE();
          i += 5;
          break;
        default:
          i += 5;
      }
      // console.log({ command });
    }
  }

  _readFunctionsCode() {
    const code = this._readStringOfLength(this._readUInt32LE());
    return code;
  }

  _readUInt32LE() {
    const int = this._buffer.readUInt32LE(this._i);
    this._i += 4;
    return int;
  }

  _readUInt16LE() {
    const int = this._buffer.readUInt16LE(this._i);
    this._i += 2;
    return int;
  }

  _readUInt8() {
    const int = this._buffer.readUInt8(this._i);
    this._i++;
    return int;
  }

  _readStringOfLength(length) {
    const str = this._buffer.toString(ENCODING, this._i, this._i + length);
    this._i += length;
    return str;
  }

  _readString() {
    return this._readStringOfLength(this._readUInt16LE());
  }

  _decodeCode({ functionsCode, types, variables, functionNames, functions }) {
    let pos = 0;
    const localFunctions = {};
    const results = [];
    const poss = [];
    while (pos < functionsCode.length) {
      poss.push(pos);
      const command = new Command({
        functionsCode,
        pos,
        types,
        variables,
        functionNames,
        localFunctions
      });
      pos += command.size;
      results.push(command);
    }
    // TODO: Don't mutate
    Object.values(localFunctions).forEach(localFunction => {
      functions.push(localFunction);
    });

    functions.sort((a, b) => {
      // TODO: Confirm that I have this the right way round
      return a.offset - b.offset;
    });

    return results;
  }

  parse(buffer) {
    this._buffer = buffer;
    this._i = 0;

    const magic = this._readMagic();
    this._readVersion();
    this._readUInt32LE(); // Not sure what we are skipping over here. Just some UInt 32.
    const types = this._readTypes();
    const functionNames = this._readFunctionsNames();
    const variables = this._readVariables();
    const constants = this._readConstants();
    const functions = this._readFunctions();
    const functionsCode = this._readFunctionsCode();
    const decoding = this._decodeCode({
      functionsCode,
      types,
      variables,
      functionNames,
      functions
    });
    this._readCommands(functionsCode);
    return {
      magic,
      types,
      functionNames,
      variables,
      constants,
      functionsCode,
      functions,
      decoding
    };
  }
}

function parse(buffer) {
  const parser = new Parser();
  return parser.parse(buffer);
}

module.exports = { parse };
