# SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries LLC
#
# SPDX-License-Identifier: MIT
"""
`adafruit_pioasm`
================================================================================

Simple assembler to convert pioasm to bytes


* Author(s): Scott Shawcroft
"""

import array
import re

splitter = re.compile(r",\s*|\s+(?:,\s*)?").split

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PIOASM.git"

CONDITIONS = ["", "!x", "x--", "!y", "y--", "x!=y", "pin", "!osre"]
IN_SOURCES = ["pins", "x", "y", "null", None, None, "isr", "osr"]
OUT_DESTINATIONS = ["pins", "x", "y", "null", "pindirs", "pc", "isr", "exec"]
WAIT_SOURCES = ["gpio", "pin", "irq", None]
MOV_DESTINATIONS = ["pins", "x", "y", None, "exec", "pc", "isr", "osr"]
MOV_SOURCES = ["pins", "x", "y", "null", None, "status", "isr", "osr"]
MOV_OPS = [None, "~", "::", None]
SET_DESTINATIONS = ["pins", "x", "y", None, "pindirs", None, None, None]


def assemble(text_program):
    """Converts pioasm text to encoded instruction bytes"""
    # pylint: disable=too-many-branches,too-many-statements,too-many-locals
    assembled = []
    program_name = None
    labels = {}
    instructions = []
    sideset_count = 0
    for line in text_program.split("\n"):
        line = line.strip()
        if not line:
            continue
        if ";" in line:
            line = line.split(";")[0].strip()
        if line.startswith(".program"):
            if program_name:
                raise RuntimeError("Multiple programs not supported")
            program_name = line.split()[1]
        elif line.startswith(".wrap_target"):
            if len(instructions) > 0:
                raise RuntimeError("wrap_target not supported")
        elif line.startswith(".wrap"):
            pass
        elif line.startswith(".side_set"):
            sideset_count = int(line.split()[1])
        elif line.endswith(":"):
            label = line[:-1]
            if label in labels:
                raise SyntaxError(f"Duplicate label {repr(label)}")
            labels[label] = len(instructions)
        elif line:
            # Only add as an instruction if the line isn't empty
            instructions.append(line)

    max_delay = 2 ** (5 - sideset_count) - 1
    assembled = []
    for instruction in instructions:
        # print(instruction)
        instruction = splitter(instruction.strip())
        delay = 0
        if instruction[-1].endswith("]"):  # Delay
            delay = int(instruction[-1].strip("[]"))
            if delay > max_delay:
                raise RuntimeError("Delay too long:", delay)
            instruction.pop()
        if len(instruction) > 1 and instruction[-2] == "side":
            sideset_value = int(instruction[-1])
            if sideset_value > 2 ** sideset_count:
                raise RuntimeError("Sideset value too large")
            delay |= sideset_value << (5 - sideset_count)
            instruction.pop()
            instruction.pop()

        if instruction[0] == "nop":
            #                  mov delay   y op   y
            assembled.append(0b101_00000_010_00_010)
        elif instruction[0] == "jmp":
            #                instr delay cnd addr
            assembled.append(0b000_00000_000_00000)
            target = instruction[-1]
            if target[:1] in "0123456789":
                assembled[-1] |= int(target)
            elif instruction[-1] in labels:
                assembled[-1] |= labels[target]
            else:
                raise SyntaxError(f"Invalid jmp target {repr(target)}")

            if len(instruction) > 2:
                assembled[-1] |= CONDITIONS.index(instruction[1]) << 5

        elif instruction[0] == "wait":
            #                instr delay p sr index
            assembled.append(0b001_00000_0_00_00000)
            polarity = int(instruction[1])
            if not 0 <= polarity <= 1:
                raise RuntimeError("Invalid polarity")
            assembled[-1] |= polarity << 7
            assembled[-1] |= WAIT_SOURCES.index(instruction[2]) << 5
            num = int(instruction[3])
            if not 0 <= num <= 31:
                raise RuntimeError("Wait num out of range")
            assembled[-1] |= num
            if instruction[-1] == "rel":
                assembled[-1] |= 0x10  # Set the high bit of the irq value
        elif instruction[0] == "in":
            #                instr delay src count
            assembled.append(0b010_00000_000_00000)
            assembled[-1] |= IN_SOURCES.index(instruction[1]) << 5
            count = int(instruction[-1])
            if not 1 <= count <= 32:
                raise RuntimeError("Count out of range")
            assembled[-1] |= count & 0x1F  # 32 is 00000 so we mask the top
        elif instruction[0] == "out":
            #                instr delay dst count
            assembled.append(0b011_00000_000_00000)
            assembled[-1] |= OUT_DESTINATIONS.index(instruction[1]) << 5
            count = int(instruction[-1])
            if not 1 <= count <= 32:
                raise RuntimeError("Count out of range")
            assembled[-1] |= count & 0x1F  # 32 is 00000 so we mask the top
        elif instruction[0] == "push" or instruction[0] == "pull":
            #                instr delay d i b zero
            assembled.append(0b100_00000_0_0_0_00000)
            if instruction[0] == "pull":
                assembled[-1] |= 0x80
            if instruction[-1] == "block" or not instruction[-1].endswith("block"):
                assembled[-1] |= 0x20
            if len(instruction) > 1 and instruction[1] in ("ifempty", "iffull"):
                assembled[-1] |= 0x40
        elif instruction[0] == "mov":
            #                instr delay dst op src
            assembled.append(0b101_00000_000_00_000)
            assembled[-1] |= MOV_DESTINATIONS.index(instruction[1]) << 5
            assembled[-1] |= MOV_SOURCES.index(instruction[-1])
            if len(instruction) > 3:
                assembled[-1] |= MOV_OPS.index(instruction[-2]) << 3
        elif instruction[0] == "irq":
            #                instr delay z c w index
            assembled.append(0b110_00000_0_0_0_00000)
            if instruction[-1] == "rel":
                assembled[-1] |= 0x10  # Set the high bit of the irq value
                instruction.pop()
            num = int(instruction[-1])
            if not 0 <= num <= 7:
                raise RuntimeError("Interrupt index out of range")
            assembled[-1] |= num
            if len(instruction) == 3:  # after rel has been removed
                if instruction[1] == "wait":
                    assembled[-1] |= 0x20
                elif instruction[1] == "clear":
                    assembled[-1] |= 0x40
                # All other values are the default of set without waiting
        elif instruction[0] == "set":
            #                instr delay dst data
            assembled.append(0b111_00000_000_00000)
            assembled[-1] |= SET_DESTINATIONS.index(instruction[1]) << 5
            value = int(instruction[-1])
            if not 0 <= value <= 31:
                raise RuntimeError("Set value out of range")
            assembled[-1] |= value
        else:
            raise RuntimeError("Unknown instruction:" + instruction)
        assembled[-1] |= delay << 8
    # print(hex(assembled[-1]))

    return array.array("H", assembled)
