import { parseArgs } from "util";

let types = {
  logical: {
    c: "char",
    js: "boolean",
    sql: "INTEGER",
    size: 1,
  },
  u8: {
    c: "unsigned char",
    js: "Uint8",
    sql: "INTEGER",
    size: 1,
  },
  i8: {
    c: "char",
    js: "Int8",
    sql: "INTEGER",
    size: 1,
  },
  u16: {
    c: "unsigned short",
    js: "Uint16",
    sql: "INTEGER",
    size: 2,
  },
  i16: {
    c: "short",
    js: "Int16",
    sql: "INTEGER",
    size: 2,
  },
  u32: {
    c: "unisgned int",
    js: "Uint32",
    sql: "INTEGER",
    size: 4,
  },
  i32: {
    c: "int",
    js: "Int32",
    sql: "INTEGER",
    size: 4,
  },
  i64: {
    c: "long",
    js: "BigInt64",
    sql: "INTEGER"
  },
  u64: {
    c: "unsigned long",
    js: "BigUint64",
    sql: "INTEGER"
  },
  f32: {
    c: "float",
    js: "Float32",
    sql: "REAL",
    size: 4,
  },
  f64: {
    c: "double",
    js: "Float64",
    sql: "REAL",
    size: 8,
  },
};

const { values } = parseArgs({
  args: Bun.argv,
  options: {
    schema: {
      type: "string",
      short: "S",
    },
    javascript_out: {
      type: "string",
      short: "j",
    },
    c_out: {
      type: "string",
      short: "c",
    },
    sqlite_out: {
      type: "string",
      short: "s"
    }
  },
  strict: true,
  allowPositionals: true,
});

function jsStructConstructor(size, containsString, type, args) {
  return `
    /** 
     * Get the struct representation of the object
     * @return {Uint8Array} u8 array of the C struct.
     */
    get bytes() {
        return new Uint8Array(this._ptr);
    }
    /**
     * Constructs a new ${type}
     * 
     * @param ${args} init The arguments to construct the object.
     * @param {ArrayBuffer} ptr The pointer to the C struct.
     */
    constructor(init = {}, ptr = undefined) {
        this._size = ${size};
        this._ptr = ptr?.buffer || new ArrayBuffer(this._size);
        this._data = new DataView(this._ptr);
        ${
          containsString
            ? `
        this._encoder = new TextEncoder(); 
        this._decoder = new TextDecoder();`
            : ""
        }          
        for (const key of Object.keys(init)) {
          this[key] = init[key];
        }
    }`;
}

const sFile = Bun.file(values.schema);
const schema = await sFile.json();
let cData = "";
let sqlData = "";

for (const type of Object.keys(schema)) {
  let containsString = false;
  let offset = 0;
  let size = 0;
  let importStatements = "";
  let jsData = "";
  let foreignKeys = "";
  const props = schema[type].members;
  let typeDef = `/**\n * @typedef {Object} ${type} ${schema[type].comment}\n`;
  let args = `{{`;
  sqlData += `CREATE TABLE ${type} (id INTEGER PRIMARY KEY AUTOINCREMENT`;
  cData += `typedef struct ${type} {`;
  jsData += `class ${type} {`;
  for (const prop of Object.keys(props)) {
    const propType = props[prop].type;
    const kind = props[prop].kind;
    const comment = props[prop].comment;
    let typeSize = parseInt(types[propType]?.size);

    switch (kind) {
      case "string":
        containsString = true;
        typeSize = props[prop].size;
        const iSize = parseInt(offset) + parseInt(typeSize);
        jsData += `
    /**
     * ${comment}
     * @return {string} gets the value of ${prop}
     */
    get ${prop}() {
        return this._decoder.decode(new Uint8Array(this._ptr.slice(${parseInt(
          offset
        )}, ${iSize})));
    }
    /**
     * ${comment}
     * @param {string} v sets the value of ${prop}
     */    
    set ${prop}(v) {
        this._data.set(this._encoder.encode(v), ${parseInt(offset)});
    }`;
        sqlData += `, ${prop} TEXT`;
        cData += `
    char ${prop}[${iSize}]; // ${comment}`;
        args += `${prop}: string, `;
        typeDef += ` * @property {string} ${prop} ${comment}\n`;
        break;
      case "scalar":
        typeSize = types[propType].size;
        jsData += `
    /**
     * ${comment}
     * @return {${types[propType].js}} gets the value of ${prop}
     */
    get ${prop}() {
        return this._data.get${types[propType].js}(${parseInt(offset)}, true);
    }
    /**
     * ${comment}
     * @param {${types[propType].js}} sets the value of ${prop}
     */
    set ${prop}(v) {
        return this._data.set${types[propType].js}(${parseInt(
          offset
        )}, v, true);
    }`;
        args += `${prop}: ${types[propType].js}, `;
        typeDef += ` * @property ${types[propType].js} ${propType} ${comment}\n`;
        sqlData += `, ${prop} ${types[propType].sql}`;
        cData += `
    ${types[propType].c} ${prop}; // ${comment}`;

        break;
      case "struct":
        const jsSize = parseInt(offset) + parseInt(types[propType].size);
        jsData += `
    /**
     * ${comment}
     * @return {${types[propType].js}} gets the value of ${prop}
     */
    get ${prop}() {
        return new ${propType}({}, new Uint8Array(this._ptr.slice(${offset}, ${jsSize})));
    }
    /**
     * ${comment}
     * @param {${types[propType].js}} sets the value of ${prop}
     */
    set ${prop}(v) {
        this._data.set(v.bytes(), ${offset});
    }`;
        const importS = `import ${propType} from "./${propType}"\n`;
        if (!importStatements.includes(importS)) {
          importStatements += importS;
        }
        const localKey = `${prop.toLowerCase()}_id`;
        args += `${prop}: ${types[propType].js}, `;
        typeDef += ` * @property ${types[propType].js} ${propType} ${comment}\n`;
        sqlData += `, ${localKey} INTEGER`;
        foreignKeys += `\n, FOREIGN KEY(${localKey}) REFERENCES ${propType}(id)`
        cData += `\n\t\t${types[propType].c} ${prop}; // ${comment}`;
        break;
      case "array":
        throw new Error("Not Implemented!");
        break;
      default:
        break;
    }

    size += parseInt(typeSize);
    offset += parseInt(typeSize);
  }

  /**
   * add the new type to the list so we can use it for structs later.
   */
  types[type] = {
    c: type,
    js: type,
    size: parseInt(size),
  };

  typeDef += " */\n"
  args += `}}`
  jsData = typeDef + jsData;
  jsData += jsStructConstructor(size, containsString, type, args);
  jsData += `\n}\n\nexport default ${type}`;
  cData += `\n} ${type};\n\n`;
  sqlData += `${foreignKeys});\n\n`;

  await Bun.write(
    Bun.file(values.javascript_out + type + ".js"),
    importStatements + jsData
  );
}
await Bun.write(Bun.file(values.c_out + "types.h"), cData);
await Bun.write(Bun.file(values.sqlite_out + "types.sql"), sqlData);
