import itertools
import math
from collections.abc import Iterator
from dataclasses import dataclass
from enum import Enum

from relaxed_atomic_execution_tests import acqrel_execution_tests

# Workaround for python <3.10, escape characters can't appear in f-strings.
# Although we require 3.10 in some places, the formatter complains without this.
newline = "\n"

backslash = "\\"

FUNC_NAME = "$test-all-ops"


def indent(s):
    return "\n".join(f"  {line}" if line else "" for line in s.split("\n"))


class ValueType(Enum):
    i32 = 32
    i64 = 64


class Ordering(Enum):
    seqcst = 0
    acqrel = 1


@dataclass
class Template:
    op: str
    value_type: object
    args: int
    should_drop: bool
    bin: bytes = b""


atomic_fence_template = Template(op="atomic.fence", value_type=None, args=0, should_drop=False, bin=b"\xfe\x03")
load_store_acqrel_templates = [
    Template(op="i32.atomic.load", value_type=ValueType.i32, args=1, should_drop=True, bin=b"\xfe\x10"),
    Template(op="i64.atomic.load", value_type=ValueType.i64, args=1, should_drop=True, bin=b"\xfe\x11"),
    Template(op="i32.atomic.load8_u", value_type=ValueType.i64, args=1, should_drop=True, bin=b"\xfe\x12"),
    Template(op="i32.atomic.load16_u", value_type=ValueType.i64, args=1, should_drop=True, bin=b"\xfe\x13"),
    Template(op="i64.atomic.load8_u", value_type=ValueType.i64, args=1, should_drop=True, bin=b"\xfe\x14"),
    Template(op="i64.atomic.load16_u", value_type=ValueType.i64, args=1, should_drop=True, bin=b"\xfe\x15"),
    Template(op="i64.atomic.load32_u", value_type=ValueType.i64, args=1, should_drop=True, bin=b"\xfe\x16"),
    Template(op="i32.atomic.store", value_type=ValueType.i32, args=2, should_drop=False, bin=b"\xfe\x17"),
    Template(op="i64.atomic.store", value_type=ValueType.i64, args=2, should_drop=False, bin=b"\xfe\x18"),
    Template(op="i32.atomic.store8", value_type=ValueType.i32, args=2, should_drop=False, bin=b"\xfe\x19"),
    Template(op="i32.atomic.store16", value_type=ValueType.i32, args=2, should_drop=False, bin=b"\xfe\x1a"),
    Template(op="i64.atomic.store8", value_type=ValueType.i64, args=2, should_drop=False, bin=b"\xfe\x1b"),
    Template(op="i64.atomic.store16", value_type=ValueType.i64, args=2, should_drop=False, bin=b"\xfe\x1c"),
    Template(op="i64.atomic.store32", value_type=ValueType.i64, args=2, should_drop=False, bin=b"\xfe\x1d"),
    Template(op="i32.atomic.rmw.add", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x1e"),
    Template(op="i64.atomic.rmw.add", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x1f"),
    Template(op="i32.atomic.rmw8.add_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x20"),
    Template(op="i32.atomic.rmw16.add_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x21"),
    Template(op="i64.atomic.rmw8.add_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x22"),
    Template(op="i64.atomic.rmw16.add_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x23"),
    Template(op="i64.atomic.rmw32.add_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x24"),
    Template(op="i32.atomic.rmw.sub", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x25"),
    Template(op="i64.atomic.rmw.sub", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x26"),
    Template(op="i32.atomic.rmw8.sub_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x27"),
    Template(op="i32.atomic.rmw16.sub_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x28"),
    Template(op="i64.atomic.rmw8.sub_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x29"),
    Template(op="i64.atomic.rmw16.sub_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x2a"),
    Template(op="i64.atomic.rmw32.sub_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x2b"),
    Template(op="i32.atomic.rmw.and", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x2c"),
    Template(op="i64.atomic.rmw.and", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x2d"),
    Template(op="i32.atomic.rmw8.and_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x2e"),
    Template(op="i32.atomic.rmw16.and_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x2f"),
    Template(op="i64.atomic.rmw8.and_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x30"),
    Template(op="i64.atomic.rmw16.and_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x31"),
    Template(op="i64.atomic.rmw32.and_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x32"),
    Template(op="i32.atomic.rmw.or", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x33"),
    Template(op="i64.atomic.rmw.or", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x34"),
    Template(op="i32.atomic.rmw8.or_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x35"),
    Template(op="i32.atomic.rmw16.or_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x36"),
    Template(op="i64.atomic.rmw8.or_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x37"),
    Template(op="i64.atomic.rmw16.or_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x38"),
    Template(op="i64.atomic.rmw32.or_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x39"),
    Template(op="i32.atomic.rmw.xor", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x3a"),
    Template(op="i64.atomic.rmw.xor", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x3b"),
    Template(op="i32.atomic.rmw8.xor_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x3c"),
    Template(op="i32.atomic.rmw16.xor_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x3d"),
    Template(op="i64.atomic.rmw8.xor_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x3e"),
    Template(op="i64.atomic.rmw16.xor_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x3f"),
    Template(op="i64.atomic.rmw32.xor_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x40"),
    Template(op="i32.atomic.rmw.xchg", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x41"),
    Template(op="i64.atomic.rmw.xchg", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x42"),
    Template(op="i32.atomic.rmw8.xchg_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x43"),
    Template(op="i32.atomic.rmw16.xchg_u", value_type=ValueType.i32, args=2, should_drop=True, bin=b"\xfe\x44"),
    Template(op="i64.atomic.rmw8.xchg_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x45"),
    Template(op="i64.atomic.rmw16.xchg_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x46"),
    Template(op="i64.atomic.rmw32.xchg_u", value_type=ValueType.i64, args=2, should_drop=True, bin=b"\xfe\x47"),
    Template(op="i32.atomic.rmw.cmpxchg", value_type=ValueType.i32, args=3, should_drop=True, bin=b"\xfe\x48"),
    Template(op="i64.atomic.rmw.cmpxchg", value_type=ValueType.i64, args=3, should_drop=True, bin=b"\xfe\x49"),
    Template(op="i32.atomic.rmw8.cmpxchg_u", value_type=ValueType.i32, args=3, should_drop=True, bin=b"\xfe\x4a"),
    Template(op="i32.atomic.rmw16.cmpxchg_u", value_type=ValueType.i32, args=3, should_drop=True, bin=b"\xfe\x4b"),
    Template(op="i64.atomic.rmw8.cmpxchg_u", value_type=ValueType.i64, args=3, should_drop=True, bin=b"\xfe\x4c"),
    Template(op="i64.atomic.rmw16.cmpxchg_u", value_type=ValueType.i64, args=3, should_drop=True, bin=b"\xfe\x4d"),
    Template(op="i64.atomic.rmw32.cmpxchg_u", value_type=ValueType.i64, args=3, should_drop=True, bin=b"\xfe\x4e"),
]


def all_combinations() -> Iterator[(Template, (int, ValueType), Ordering)]:
    """Yield tuples covering all possible combinations of atomic memory operations.

    (template, (idx, memory_ptr_type), ordering)
    where idx is a memory index or None representing an implicit 0 index
      and memory_ptr_type is i32 or i64 based on the memory being indexed
      and ordering is an `Ordering` enum or None representing an implicit seqcst ordering.
    """
    # See the memory section defined in `binary_test`
    memories = [(None, ValueType.i32), (0, ValueType.i32), (1, ValueType.i64)]

    yield from itertools.product(load_store_acqrel_templates, memories, [None, Ordering.acqrel, Ordering.seqcst])

    for ordering in None, Ordering.acqrel, Ordering.seqcst:
        yield atomic_fence_template, (None, None), ordering


def statement(template, mem_idx: int | None, mem_ptr_type: ValueType, ordering: Ordering | None):
    """Return a statement exercising the op in `template` e.g. (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42))."""
    if template.op == "atomic.fence":
        assert mem_idx is None
        assert mem_ptr_type is None

        return f"(atomic.fence{' ' + ordering.name if ordering is not None else ''})"

    memargs = []
    if mem_idx is not None:
        memargs.append(str(mem_idx))
    if ordering is not None:
        memargs.append(ordering.name)

    memarg_str = " ".join(memargs) + " " if memargs else ""

    # The first argument (the memory location) must match the memory that we're indexing. Other arguments match the op (e.g. i32 for i32.atomic.load).
    args = [f"({mem_ptr_type.name}.const 0)"] + [f"({template.value_type.name}.const 42)" for _ in range(template.args - 1)]

    op_str = "(" + "".join([template.op, " ", memarg_str, " ".join(args)]) + ")"
    if not template.should_drop:
        return op_str
    return f"(drop {op_str})"


def func():
    """Return a func exercising all atomic ops.

    e.g.
    (func $test-all-ops
      (drop (i32.atomic.load (i32.const 42)))
      (drop (i32.atomic.load acqrel (i32.const 42)))
      ...
    )
    """
    return f''';; Memory index must come before memory ordering if present.
;; Both immediates are optional; an omitted memory ordering will be treated as seqcst.
(func $test-all-ops
{indent(newline.join(statement(template, mem_idx, mem_ptr_type, ordering) for template, (mem_idx, mem_ptr_type), ordering in all_combinations()))}
)'''


def text_test():
    """Return a (module ...) that exercises all atomic ops."""
    return f'''(module
  (memory i32 1 1)
  (memory i64 1 1)

{indent(func())}
)'''


def invalid_text_test():
    return '''(assert_invalid (module
  (memory 1 1 shared)

  (func $i32load (drop (i32.load acqrel (i32.const 51))))
) "Can't set memory ordering for non-atomic i32.load")'''


def bin_to_str(bin: bytes) -> str:
    r"""Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00."""
    return ''.join(f'{backslash}{byte:02x}' for byte in bin)


def i64_const(num):
    return b"\x42" + int.to_bytes(num)


def i32_const(num):
    return b"\x41" + int.to_bytes(num)


def bin_statement_lines(template: Template, mem_idx: int, mem_ptr_type: ValueType, ordering: Ordering) -> Iterator[(bytes, str)]:
    r"""Yield (b, comment) where `b` is a part of the statement using `template`, and `comment` explains that part.

    e.g.
        (b"\xfe\x11", "i64.atomic.load")
    The entire iterator represents a complete expression using the `template`. e.g.
        (drop (i32.atomic.load (i32.const 42)))
    """
    if template.op == "atomic.fence":
        assert mem_idx is None
        assert mem_ptr_type is None

        assert ordering is not None

        yield template.bin, template.op
        yield int.to_bytes(ordering.value), f"{ordering.name} memory ordering"
        return

    arg_one_bin = i64_const(0) if mem_ptr_type == ValueType.i64 else i32_const(0)
    yield arg_one_bin, f"({mem_ptr_type.name}.const 0)"
    for _ in range(template.args - 1):
        const = i64_const(51) if template.value_type == ValueType.i64 else i32_const(51)
        yield const, f"({template.value_type.name}.const 51)"

    yield template.bin, template.op

    has_ordering = ordering is not None
    has_mem_idx = mem_idx is not None
    raw_alignment = int(math.log2(mem_ptr_type.value // 8))
    alignment = raw_alignment | (has_ordering << 4) | (has_mem_idx << 6)
    comment = f"Alignment of {raw_alignment}" \
              f'{" with bit 4 set indicating that an ordering immediate follows" if has_ordering else ""}' \
              f'{" and" if has_ordering and has_mem_idx else ""}' \
              f'{" with bit 6 set indicating that a memory index immediate follows" if has_mem_idx else ""}'

    yield int.to_bytes(alignment), comment

    if has_mem_idx:
        yield int.to_bytes(mem_idx), "memory index"

    if has_ordering:
        ordering_num = ordering.value
        if "rmw" in template.op:
            ordering_num |= ordering_num << 4

        yield int.to_bytes(ordering_num), f"{ordering.name} memory ordering"

    yield b"\x00", "offset"

    if template.should_drop:
        yield b"\x1a", "drop"


def bin_statement(template: Template, mem_idx: int, mem_ptr_type: ValueType, ordering: Ordering) -> (bytes, str):
    r"""Return (b, s) where `b` is the binary exercising an instruction.

    e.g:
     (drop (i32.atomic.load (i32.const 42)))
    and `s` is a str containing the binary along with comments explaining it, e.g.
     "\41\33" ;; (i32.const 51)
     "\fe\10" ;; i32.atomic.load
     "\42" ;; Alignment of 2 with bit 6 set indicating that a memory index immediate follows
     "\00" ;; memory index
     "\00" ;; offset
     "\1a" ;; drop
    """
    bins = []
    strs = []

    for bin, comment in bin_statement_lines(template, mem_idx, mem_ptr_type, ordering):
        bins.append(bin)
        strs.append(f'"{bin_to_str(bin)}" ;; {comment}')

    return b"".join(bins), "\n".join(strs)


def to_unsigned_leb(num: int) -> bytearray:
    ret = bytearray()

    if num == 0:
        ret.append(0)
        return ret

    rem = num
    while (num := rem) > 0:
        rem = num >> 7
        ret.append((num & 0x7F) | (bool(rem) << 7))

    return ret


def binary_func_body():
    statement_strs = []
    statement_bins = []

    for template, (mem_idx, mem_ptr_type), ordering in all_combinations():
        # In the binary encoding for atomic.fence, there's no 'default' without
        # a memory ordering. Skip this case.
        if template.op == "atomic.fence" and ordering is None:
            continue

        bin, s = bin_statement(template, mem_idx, mem_ptr_type, ordering)
        statement_bins.append(bin)
        statement_strs.append(s)

    # plus two with \0b function end byte and local decl count (0)
    func_body_size = sum(map(len, statement_bins)) + 2
    section_size = (
        func_body_size +  # function body bytes
        len(to_unsigned_leb(func_body_size)) +  # LEB encoding of function body size
        1)  # Function count byte (0)
    code_section_strs = \
        [rf'"\0a{bin_to_str(to_unsigned_leb(section_size))}\01" ;; Code section' "\n"
         rf'"{bin_to_str(to_unsigned_leb(func_body_size))}\00" ;; func {FUNC_NAME}'] + \
        list(map(indent, statement_strs)) + \
        [r'  "\0b" ;; end']

    return "\n\n".join(code_section_strs) + "\n"


def module_binary(str: str):
    return f'''(module binary
{indent(str)})'''


def binary_test():
    header = fr'''"\00asm\01\00\00\00" ;; Wasm header
"\01\04\01" ;; Type section
  "\60\00\00" ;; {FUNC_NAME} type
"\03\02\01\00" ;; Function section
"\05\07\02" ;; Memory section
  "\01\01\01" ;; (memory i32 1 1)
  "\05\01\01" ;; (memory i64 1 1)'''
    return module_binary(header + "\n" + binary_func_body())


def main():
    print(";; Generated by scripts/test/generate_atomic_spec_test.py. Do not edit manually.")
    print()
    print(text_test())
    print()
    print(invalid_text_test())
    print()
    print(binary_test())
    print()
    print(acqrel_execution_tests)


if __name__ == "__main__":
    main()
