import fs from "fs";
import parse from "../../src/parse";
import { EffectiveAddressNode } from "../../src/parse/nodes";
import {
  Mnemonics,
  AddressingModes,
  Qualifiers,
  Directives,
} from "../../src/syntax";

describe("parse()", () => {
  test("parse file", () => {
    const file = fs
      .readFileSync(__dirname + "/../examples/example.s")
      .toString();
    const result = parse(file);
    expect(result).toHaveLength(843);
  });

  describe("instruction parsing", () => {
    test("two operands", () => {
      const [result] = parse("  move.w d0,(a0) ; foo");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        ((result.statement
          .operands[0] as EffectiveAddressNode) as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        ((result.statement
          .operands[1] as EffectiveAddressNode) as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("single operand", () => {
      const [result] = parse("  ext.w d0");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.EXT);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        ((result.statement
          .operands[0] as EffectiveAddressNode) as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("unary", () => {
      const [result] = parse("  rts");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.RTS);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("complex expression", () => {
      const [result] = parse(
        "foo:   move.w      #(CopperE-Copper)/4-1,d0;foo bar baz"
      );
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Imm);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("with comment", () => {
      const [result] = parse("   move.w     d0,(a0);foo bar baz");
      expect(result.statement.comment.text).toEqual(";foo bar baz");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("with comment - alt", () => {
      const [result] = parse("   move.w     d0,(a0) *foo bar baz");
      expect(result.statement.comment.text).toEqual("*foo bar baz");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("with label", () => {
      const [result] = parse("foo:   move.w     d0,(a0)");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("with label - local", () => {
      const [result] = parse(".foo:   move.w     d0,(a0)");
      expect(result.statement.label.text).toEqual(".foo");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("with label - no colon", () => {
      const [result] = parse("foo   move.w     d0,(a0)");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("default qualifier", () => {
      const [result] = parse("  MOVE D0,(A0)");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier).toBeFalsy();
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("case insensitive", () => {
      const [result] = parse("  MOVE.W D0,(A0)");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });

    test("space in parentheses", () => {
      const [result] = parse("  move.w d0,(  a0 )");
      expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
      expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.Dn);
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).mode
      ).toEqual(AddressingModes.AnIndir);
      expect(result.timing).toBeTruthy();
      expect(result.bytes).toBeTruthy();
    });
  });

  test("label no space", () => {
    const [result] = parse("a:MOVE.W D0,(A0)");
    expect(result.statement.label.text).toEqual("a");
    expect(result.statement.opcode.op.name).toEqual(Mnemonics.MOVE);
    expect(result.statement.opcode.qualifier.name).toEqual(Qualifiers.W);
    expect((result.statement.operands[0] as EffectiveAddressNode).mode).toEqual(
      AddressingModes.Dn
    );
    expect((result.statement.operands[1] as EffectiveAddressNode).mode).toEqual(
      AddressingModes.AnIndir
    );
    expect(result.timing).toBeTruthy();
    expect(result.bytes).toBeTruthy();
  });

  describe("directives", () => {
    test("assignment EQU", () => {
      const [result] = parse("foo EQU 1");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode.op.name).toEqual(Directives.EQU);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).text
      ).toEqual("1");
    });

    test("assignment =", () => {
      const [result] = parse("foo = 1");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode.op.name).toEqual(Directives["="]);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).text
      ).toEqual("1");
    });

    test("assignment =", () => {
      const [result] = parse("foo=1");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode.op.name).toEqual(Directives["="]);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).text
      ).toEqual("1");
    });

    test("memory", () => {
      const [result] = parse("a: dc.w 1,2,3");
      expect(result.statement.label.text).toEqual("a");
      expect(result.statement.opcode.op.name).toEqual(Directives.DC);
      expect(
        (result.statement.operands[0] as EffectiveAddressNode).text
      ).toEqual("1");
      expect(
        (result.statement.operands[1] as EffectiveAddressNode).text
      ).toEqual("2");
      expect(result.statement.operands[2].text).toEqual("3");
    });

    test("macro definition", () => {
      const [result] = parse("a: macro");
      expect(result.statement.label.text).toEqual("a");
      expect(result.statement.opcode.op.name).toEqual(Directives.MACRO);
    });

    test("macro invocation: label", () => {
      const lines = parse(`
a:    macro
      move.w \\1,\\2
      endm
      a d0,(a0)`);
      expect(lines[4].macroLines).toHaveLength(1);
      expect(lines[4].bytes).toEqual(2);
      expect(lines[4].timing?.values).toEqual([[8, 1, 1]]);
    });

    test("macro invocation: operand", () => {
      const lines = parse(`
      macro a
      move.w \\1,\\2
      endm
      a d0,(a0)`);
      expect(lines[4].macroLines).toHaveLength(1);
      expect(lines[4].bytes).toEqual(2);
      expect(lines[4].timing?.values).toEqual([[8, 1, 1]]);
    });

    test("rept", () => {
      const lines = parse(`
      rept 4
      move.w d0,(a0)
      endr`);
      expect(lines[3].macroLines).toHaveLength(4);
      expect(lines[3].bytes).toEqual(2 * 4);
      expect(lines[3].timing.values).toEqual([[8 * 4, 4, 4]]);
    });

    test("bss section", () => {
      const lines = parse(`
a: ds.w 1
      BSS
b: ds.w 1
      DATA
c: ds.w 1
      SECTION "foo",BSS
d: ds.w 1

`);
      expect(lines[1].bss).toBe(false);
      expect(lines[3].bss).toBe(true);
      expect(lines[5].bss).toBe(false);
      expect(lines[7].bss).toBe(true);
    });
  });

  describe("parse assignments", () => {
    test("dynamic shift", () => {
      const code = `
foo=4
      lsl.w #foo,d0
      `;
      const lines = parse(code);
      const n = 4;
      expect(lines[2].timing.values).toEqual([[6 + 2 * n, 1, 0]]);
    });

    test("forward ref", () => {
      const code = `
      lsl.w #foo,d0
foo=4
      `;
      const lines = parse(code);
      const n = 4;
      expect(lines[1].timing.values).toEqual([[6 + 2 * n, 1, 0]]);
    });
  });

  describe("no instruction", () => {
    test("empty line", () => {
      const [result] = parse("");
      expect(result.statement.comment).toBeFalsy();
      expect(result.statement.label).toBeFalsy();
      expect(result.statement.opcode).toBeFalsy();
    });

    test("only whitespace", () => {
      const [result] = parse("       ");
      expect(result.statement.comment).toBeFalsy();
      expect(result.statement.label).toBeFalsy();
      expect(result.statement.opcode).toBeFalsy();
    });

    test("only comment", () => {
      const [result] = parse("; foo bar baz");
      expect(result.statement.comment.text).toEqual("; foo bar baz");
      expect(result.statement.opcode).toBeFalsy();
    });

    test("only comment - alt", () => {
      const [result] = parse("* foo bar baz");
      expect(result.statement.comment.text).toEqual("* foo bar baz");
      expect(result.statement.opcode).toBeFalsy();
    });

    test("only label", () => {
      const [result] = parse("foo:");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode).toBeFalsy();
    });

    test("only label - local", () => {
      const [result] = parse(".foo:");
      expect(result.statement.label.text).toEqual(".foo");
      expect(result.statement.opcode).toBeFalsy();
    });

    test("only label - no colon", () => {
      const [result] = parse("foo");
      expect(result.statement.label.text).toEqual("foo");
      expect(result.statement.opcode).toBeFalsy();
    });
  });
});
