#!/usr/bin/env python3 -B
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import re

quoted_pat = re.compile(r"([^A-Za-z0-9]|^)'([^']+)'")
js_pat = re.compile(r"([^A-Za-z0-9]|^)(JS[A-Z0-9_\*]+)")


def codify(text):
    text = re.sub(quoted_pat, "\\1<code>\\2</code>", text)
    text = re.sub(js_pat, "\\1<code>\\2</code>", text)

    return text


space_star_space_pat = re.compile(r"^\s*\* ?", re.M)


def get_comment_body(comment):
    return re.sub(space_star_space_pat, "", comment).split("\n")


quote_pat = re.compile('"([^"]+)"')
str_pat = re.compile("js_([^_]+)_str")


def parse_name(s):
    m = quote_pat.search(s)
    if m:
        return m.group(1)
    m = str_pat.search(s)
    if m:
        return m.group(1)
    return s


csv_pat = re.compile(", *")


def parse_csv(s):
    a = csv_pat.split(s)
    if len(a) == 1 and a[0] == "":
        return []
    return a


def get_stack_count(stack):
    if stack == "":
        return 0
    if "..." in stack:
        return -1
    return len(stack.split(","))


def parse_index(comment):
    index = []
    current_types = None
    category_name = ""
    category_pat = re.compile(r"\[([^\]]+)\]")
    for line in get_comment_body(comment):
        m = category_pat.search(line)
        if m:
            category_name = m.group(1)
            if category_name == "Index":
                continue
            current_types = []
            index.append((category_name, current_types))
        else:
            type_name = line.strip()
            if type_name and current_types is not None:
                current_types.append((type_name, []))

    return index


# Holds the information stored in the comment with the following format:
#   /*
#    * {desc}
#    *   Category: {category_name}
#    *   Type: {type_name}
#    *   Operands: {operands}
#    *   Stack: {stack_uses} => {stack_defs}
#    */


class CommentInfo:
    def __init__(self):
        self.desc = ""
        self.category_name = ""
        self.type_name = ""
        self.operands = ""
        self.stack_uses = ""
        self.stack_defs = ""


# Holds the information stored in the macro with the following format:
#   MACRO({op}, {op_snake}, {token}, {length}, {nuses}, {ndefs}, {format})
# and the information from CommentInfo.


class OpcodeInfo:
    def __init__(self, value, comment_info):
        self.op = ""
        self.op_snake = ""
        self.value = value
        self.token = ""
        self.length = ""
        self.nuses = ""
        self.ndefs = ""
        self.format_ = ""

        self.operands_array = []
        self.stack_uses_array = []
        self.stack_defs_array = []

        self.desc = comment_info.desc
        self.category_name = comment_info.category_name
        self.type_name = comment_info.type_name
        self.operands = comment_info.operands
        self.operands_array = comment_info.operands_array
        self.stack_uses = comment_info.stack_uses
        self.stack_uses_array = comment_info.stack_uses_array
        self.stack_defs = comment_info.stack_defs
        self.stack_defs_array = comment_info.stack_defs_array

        # List of OpcodeInfo that corresponds to macros after this.
        #   /*
        #    * comment
        #    */
        #   MACRO(Sub, ...)
        #   MACRO(Mul, ...)
        #   MACRO(Div, ...)
        self.group = []

        self.sort_key = ""


def find_by_name(list, name):
    for n, body in list:
        if n == name:
            return body

    return None


def add_to_index(index, opcode):
    types = find_by_name(index, opcode.category_name)
    if types is None:
        raise Exception("Category is not listed in index: " f"{opcode.category_name}")
    opcodes = find_by_name(types, opcode.type_name)
    if opcodes is None:
        if opcode.type_name:
            raise Exception(
                f"Type is not listed in {opcode.category_name}: " f"{opcode.type_name}"
            )
        types.append((opcode.type_name, [opcode]))
        return

    opcodes.append(opcode)


tag_pat = re.compile(r"^\s*[A-Za-z]+:\s*|\s*$")


def get_tag_value(line):
    return re.sub(tag_pat, "", line)


RUST_OR_CPP_KEYWORDS = {
    "and",
    "case",
    "default",
    "double",
    "false",
    "goto",
    "in",
    "new",
    "not",
    "or",
    "return",
    "throw",
    "true",
    "try",
    "typeof",
    "void",
}


def get_opcodes(dir):
    iter_pat = re.compile(
        r"/\*(.*?)\*/"  # either a documentation comment...
        r"|"
        r"MACRO\("  # or a MACRO(...) call
        r"(?P<op>[^,]+),\s*"
        r"(?P<op_snake>[^,]+),\s*"
        r"(?P<token>[^,]+,)\s*"
        r"(?P<length>[0-9\-]+),\s*"
        r"(?P<nuses>[0-9\-]+),\s*"
        r"(?P<ndefs>[0-9\-]+),\s*"
        r"(?P<format>[^\)]+)"
        r"\)",
        re.S,
    )
    stack_pat = re.compile(r"^(?P<uses>.*?)" r"\s*=>\s*" r"(?P<defs>.*?)$")

    opcodes = dict()
    index = []

    with open(f"{dir}/js/src/vm/Opcodes.h", encoding="utf-8") as f:
        data = f.read()

    comment_info = None
    opcode = None

    # The first opcode after the comment.
    group_head = None
    next_opcode_value = 0

    for m in re.finditer(iter_pat, data):
        comment = m.group(1)
        op = m.group("op")

        if comment:
            if "[Index]" in comment:
                index = parse_index(comment)
                continue

            if "Operands:" not in comment:
                continue

            group_head = None

            comment_info = CommentInfo()

            state = "desc"
            stack = ""
            desc = ""

            for line in get_comment_body(comment):
                if line.startswith("  Category:"):
                    state = "category"
                    comment_info.category_name = get_tag_value(line)
                elif line.startswith("  Type:"):
                    state = "type"
                    comment_info.type_name = get_tag_value(line)
                elif line.startswith("  Operands:"):
                    state = "operands"
                    comment_info.operands = get_tag_value(line)
                elif line.startswith("  Stack:"):
                    state = "stack"
                    stack = get_tag_value(line)
                elif state == "desc":
                    desc += line + "\n"
                elif line.startswith("   "):
                    if line.isspace():
                        pass
                    elif state == "operands":
                        comment_info.operands += " " + line.strip()
                    elif state == "stack":
                        stack += " " + line.strip()
                else:
                    raise ValueError(
                        f"unrecognized line in comment: {line!r}\n\nfull comment was:\n{comment}"
                    )

            comment_info.desc = desc

            comment_info.operands_array = parse_csv(comment_info.operands)
            comment_info.stack_uses_array = parse_csv(comment_info.stack_uses)
            comment_info.stack_defs_array = parse_csv(comment_info.stack_defs)

            m2 = stack_pat.search(stack)
            if m2:
                comment_info.stack_uses = m2.group("uses")
                comment_info.stack_defs = m2.group("defs")
        else:
            assert op is not None
            opcode = OpcodeInfo(next_opcode_value, comment_info)
            next_opcode_value += 1

            opcode.op = op
            opcode.op_snake = m.group("op_snake")
            opcode.token = parse_name(m.group("token"))
            opcode.length = m.group("length")
            opcode.nuses = m.group("nuses")
            opcode.ndefs = m.group("ndefs")
            opcode.format_ = m.group("format").split("|")

            expected_snake = re.sub(r"(?<!^)(?=[A-Z])", "_", opcode.op).lower()
            if expected_snake in RUST_OR_CPP_KEYWORDS:
                expected_snake += "_"
            if opcode.op_snake != expected_snake:
                raise ValueError(
                    f"Unexpected snake-case name for {opcode.op}: expected {expected_snake!r}, got {opcode.op_snake!r}"
                )

            if not group_head:
                group_head = opcode

                opcode.sort_key = opcode.op
                if opcode.category_name == "":
                    raise Exception("Category is not specified for " f"{opcode.op}")
                add_to_index(index, opcode)
            else:
                if group_head.length != opcode.length:
                    raise Exception(
                        "length should be same for opcodes of the"
                        " same group: "
                        f"{group_head.length}({group_head.op}) != "
                        f"{opcode.length}({opcode.op})"
                    )
                if group_head.nuses != opcode.nuses:
                    raise Exception(
                        "nuses should be same for opcodes of the"
                        " same group: "
                        f"{group_head.nuses}({group_head.op}) != "
                        f"{opcode.nuses}({opcode.op})"
                    )
                if group_head.ndefs != opcode.ndefs:
                    raise Exception(
                        "ndefs should be same for opcodes of the"
                        " same group: "
                        f"{group_head.ndefs}({group_head.op}) != "
                        f"{opcode.ndefs}({opcode.op})"
                    )

                group_head.group.append(opcode)

                if opcode.op < group_head.op:
                    group_head.sort_key = opcode.op

            opcodes[op] = opcode

            # Verify stack notation.
            nuses = int(opcode.nuses)
            ndefs = int(opcode.ndefs)

            stack_nuses = get_stack_count(opcode.stack_uses)
            stack_ndefs = get_stack_count(opcode.stack_defs)

            if nuses != -1 and stack_nuses != -1 and nuses != stack_nuses:
                raise Exception(
                    f"nuses should match stack notation: {op}: "
                    f"{nuses} != {stack_nuses} "
                    "(stack_nuses)"
                )
            if ndefs != -1 and stack_ndefs != -1 and ndefs != stack_ndefs:
                raise Exception(
                    f"ndefs should match stack notation: {op}: "
                    f"{ndefs} != {stack_ndefs} "
                    "(stack_ndefs)"
                )

    return index, opcodes
