#!/usr/bin/env python
"""Creates the pin file for the MIMXRT10xx."""

from __future__ import print_function

import argparse
import sys
import csv
import re

SUPPORTED_AFS = {"GPIO", "USDHC", "FLEXPWM", "TMR"}
MAX_AF = 10  # AF0 .. AF9
ADC_COL = 11


regexes = [
    r"IOMUXC_(?P<pin>GPIO_SD_B\d_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_AD_B\d_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_EMC_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_B\d_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_AD_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_SD_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_EMC_B\d_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_DISP_B\d_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
    r"IOMUXC_(?P<pin>GPIO_LPSR_\d\d)_(?P<function>\w+) (?P<muxRegister>\w+), (?P<muxMode>\w+), (?P<inputRegister>\w+), (?P<inputDaisy>\w+), (?P<configRegister>\w+)",
]


def parse_pad(pad_str):
    """Parses a string and returns a (port, gpio_bit) tuple."""
    if len(pad_str) < 4:
        raise ValueError("Expecting pad name to be at least 4 characters")
    if pad_str[:4] != "GPIO":
        raise ValueError("Expecting pad name to start with GPIO")
    return pad_str


def af_supported(af_str):
    for supported_af in SUPPORTED_AFS:
        if af_str.startswith(supported_af):
            return True
    else:
        return False


class Pin(object):
    """Holds the information associated with a pin."""

    def __init__(self, pad, gpio, pin, idx=0):
        self.idx = idx
        self.name = pad
        self.pad = pad
        self.gpio = gpio
        self.pin = pin
        self.alt_fn = []
        self.adc_fns = []
        self.board_pin = False

    def set_is_board_pin(self):
        self.board_pin = True

    def is_board_pin(self):
        return self.board_pin

    def parse_adc(self, adc_str):
        adc_regex = r"ADC(?P<instance>\d*)_IN(?P<channel>\d*)"
        lpadc_regex = r"ADC(?P<instance>\d*)_CH(?P<channel>\d*)"  # LPADC for MIMXRT11xx chips

        matches = re.finditer(adc_regex, adc_str, re.MULTILINE)
        for match in matches:
            self.adc_fns.append(
                AdcFunction(instance=match.group("instance"), channel=match.group("channel"))
            )

        matches = re.finditer(lpadc_regex, adc_str, re.MULTILINE)
        for match in matches:
            self.adc_fns.append(
                AdcFunction(
                    peripheral="LPADC",
                    instance=match.group("instance"),
                    channel=match.group("channel"),
                )
            )

    def parse_af(self, af_idx, af_strs_in):
        pass

    def add_af(self, af):
        self.alt_fn.append(af)

    def print_pin_af(self):
        if self.alt_fn:
            print(
                "static const machine_pin_af_obj_t pin_{0}_af[{1}] = {{".format(
                    self.name, len(self.alt_fn)
                )
            )
            for af in self.alt_fn:
                af.print()
            print("};")
        else:
            raise ValueError("Pin '{}' has no alternative functions".format(self.name))

    def print_pin_adc(self):
        if self.adc_fns:
            print(
                "static const machine_pin_adc_obj_t pin_{0}_adc[{1}] = {{".format(
                    self.name, len(self.adc_fns)
                )
            )
            for adc_fn in self.adc_fns:
                adc_fn.print()
            print("};")

    def print(self):
        if self.alt_fn:
            self.print_pin_af()
            self.print_pin_adc()

            print(
                "const machine_pin_obj_t pin_{0} = {1}({0}, {2}, {3}, pin_{0}_af, {4}, {5});\n".format(
                    self.name,
                    "PIN_LPSR" if "LPSR" in self.name else "PIN",
                    self.gpio,
                    int(self.pin),
                    len(self.adc_fns),
                    "pin_{}_adc".format(self.name) if self.adc_fns else "NULL",
                )
            )
        else:
            raise ValueError("Pin '{}' has no alternative functions".format(self.name))

    def print_header(self, hdr_file):
        pass


class AdcFunction(object):
    """Holds the information associated with a pins ADC function."""

    def __init__(self, instance, channel, peripheral="ADC"):
        self.peripheral = peripheral
        self.instance = instance
        self.channel = channel

    def print(self):
        """Prints the C representation of this AF."""
        print(f"    PIN_ADC({self.peripheral}{self.instance}, {self.channel}),")


class AlternateFunction(object):
    """Holds the information associated with a pins alternate function."""

    def __init__(self, idx, input_reg, input_daisy, af_str):
        self.idx = idx
        self.af_str = af_str
        self.input_reg = input_reg
        self.input_daisy = input_daisy
        self.instance = self.af_str.split("_")[0]

    def print(self):
        """Prints the C representation of this AF."""
        print(
            "    PIN_AF({0}, PIN_AF_MODE_ALT{1}, {2}, {3}, {4}, {5}),".format(
                self.af_str, self.idx, self.input_daisy, self.instance, self.input_reg, "0x10B0U"
            )
        )


class NamedPin(object):
    def __init__(self, name, pad, idx):
        self.name = name
        self.pad = pad
        self.idx = idx


class Pins(object):
    def __init__(self):
        self.cpu_pins = []
        self.board_pins = []

    def find_pin_by_num(self, pin_num):
        for pin in self.cpu_pins:
            if pin.pin_num == pin_num:
                return pin

    def find_pin_by_name(self, pad):
        for pin in self.cpu_pins:
            if pin.pad == pad:
                return pin

    def parse_board_file(self, filename):
        with open(filename, "r") as csvfile:
            rows = csv.reader(csvfile)
            for row in rows:
                if len(row) == 0 or row[0].startswith("#"):
                    # Skip empty lines, and lines starting with "#"
                    continue
                if len(row) != 2:
                    raise ValueError("Expecting two entries in a row")

                pin = self.find_pin_by_name(row[1])
                if pin and row[0]:  # Only add board pins that have a name
                    self.board_pins.append(NamedPin(row[0], pin.pad, pin.idx))

    def parse_af_file(self, filename, iomux_filename):
        iomux_pin_config = dict()
        with open(iomux_filename, "r") as ipt:
            input_str = ipt.read()
            for regex in regexes:
                matches = re.finditer(regex, input_str, re.MULTILINE)

                for match in matches:
                    if match.group("pin") not in iomux_pin_config:
                        iomux_pin_config[match.group("pin")] = {
                            int((match.groupdict()["muxMode"].strip("U")), 16): match.groupdict()
                        }
                    else:
                        iomux_pin_config[match.group("pin")][
                            int((match.groupdict()["muxMode"].strip("U")), 16)
                        ] = match.groupdict()

        with open(filename, "r") as csvfile:
            rows = csv.reader(csvfile)
            header = next(rows)
            # Extract indexes from header row
            pad_col = header.index("Pad")
            adc_col = header.index("ADC")
            acmp_col = header.index("ACMP")
            #
            for idx, row in enumerate(rows):
                pad = row[pad_col]
                gpio, pin = row[6].split("_")
                pin_number = pin.lstrip("IO")
                pin = Pin(pad, gpio, pin_number, idx=idx)

                if any(s in pad for s in ("SNVS", "WAKEUP")):
                    continue

                # Parse alternate functions
                af_idx = 0
                for af_idx, af in enumerate(row[(pad_col + 1) : adc_col]):
                    if af and af_supported(af):
                        pin.add_af(
                            AlternateFunction(
                                af_idx,
                                iomux_pin_config[pin.name][af_idx]["inputRegister"].strip("U"),
                                int(
                                    iomux_pin_config[pin.name][af_idx]["inputDaisy"].strip("U"), 16
                                ),
                                af,
                            )
                        )

                pin.parse_adc(row[adc_col])

                self.cpu_pins.append(pin)

    @staticmethod
    def print_named(label, pins):
        print("")
        print(
            "STATIC const mp_rom_map_elem_t pin_{:s}_pins_locals_dict_table[] = {{".format(label)
        )
        for pin in pins:
            print(
                "    {{ MP_ROM_QSTR(MP_QSTR_{}), MP_ROM_PTR(&pin_{}) }},".format(pin.name, pin.pad)
            )
        print("};")
        print(
            "MP_DEFINE_CONST_DICT(machine_pin_{:s}_pins_locals_dict, pin_{:s}_pins_locals_dict_table);".format(
                label, label
            )
        )

    def print(self):
        # Print Pin Object declarations
        for pin in self.cpu_pins:
            pin.print()

        print("")
        print("const machine_pin_obj_t* machine_pin_board_pins [] = {")
        for pin in self.board_pins:
            print("    &pin_{},".format(pin.pad))
        print("};")
        print("const uint32_t num_board_pins = {:d};".format(len(self.board_pins)))
        # Print Pin mapping dictionaries
        self.print_named("cpu", self.cpu_pins)
        self.print_named("board", self.board_pins)
        print("")

    def print_header(self, hdr_filename):
        with open(hdr_filename, "w") as hdr_file:
            for pin in self.cpu_pins:
                hdr_file.write("extern const machine_pin_obj_t pin_{};\n".format(pin.name))
            hdr_file.write("extern const machine_pin_obj_t* machine_pin_board_pins[];\n")
            hdr_file.write("extern const uint32_t num_board_pins;\n")
            hdr_file.write("extern const mp_obj_dict_t machine_pin_cpu_pins_locals_dict;\n")
            hdr_file.write("extern const mp_obj_dict_t machine_pin_board_pins_locals_dict;\n")

            hdr_file.write("\n// Defines\n")
            module_instance_factory(self.cpu_pins, hdr_file, "USDHC")
            module_instance_factory(self.cpu_pins, hdr_file, "FLEXPWM")
            module_instance_factory(self.cpu_pins, hdr_file, "TMR")


def module_instance_factory(pins, output_file, name):
    module_pin = filter(lambda p: any([af for af in p.alt_fn if name in af.af_str]), pins)

    module_instances = dict()
    for pin in module_pin:
        for idx, alt_fn in enumerate(pin.alt_fn):
            if name in alt_fn.instance:
                format_string = "#define {0}_{1} &pin_{0}, {2}"
                if alt_fn.instance not in module_instances:
                    module_instances[alt_fn.instance] = [
                        format_string.format(pin.name, alt_fn.af_str, idx)
                    ]
                else:
                    module_instances[alt_fn.instance].append(
                        format_string.format(pin.name, alt_fn.af_str, idx)
                    )

    for k, v in module_instances.items():
        output_file.write(f"// {k}\n")
        output_file.write(f"#define {k}_AVAIL (1)\n")
        if name == "FLEXPWM":
            output_file.write(f"#define {k} {k[-4:]}\n")
        for i in v:
            output_file.write(i + "\n")


def main():
    parser = argparse.ArgumentParser(
        prog="make-pins.py",
        usage="%(prog)s [options] [command]",
        description="Generate board specific pin file",
    )
    parser.add_argument(
        "-a",
        "--af",
        dest="af_filename",
        help="Specifies the alternate function file for the chip",
        default="mimxrt1021_af.csv",
    )
    parser.add_argument(
        "-i",
        "--iomux",
        dest="iomux_filename",
        help="Specifies the fsl_iomuxc.h file for the chip",
        default="fsl_iomuxc.h",
    )
    parser.add_argument(
        "-b",
        "--board",
        dest="board_filename",
        help="Specifies the board file",
        default="MIMXRT1020_EVK/pins.csv",
    )
    parser.add_argument(
        "-p",
        "--prefix",
        dest="prefix_filename",
        help="Specifies beginning portion of generated pins file",
        default="mimxrt_prefix.c",
    )
    parser.add_argument(
        "-r",
        "--hdr",
        dest="hdr_filename",
        help="Specifies name of generated pin header file",
        default="build/pins.h",
    )

    pins = Pins()

    # test code
    args = parser.parse_args()
    #

    if args.af_filename:
        print("// --af {:s}".format(args.af_filename))
        pins.parse_af_file(args.af_filename, args.iomux_filename)

    if args.board_filename:
        print("// --board {:s}".format(args.board_filename))
        pins.parse_board_file(args.board_filename)

    if args.hdr_filename:
        print("// --hdr {:s}".format(args.hdr_filename))

    if args.prefix_filename:
        print("// --prefix {:s}".format(args.prefix_filename))
        with open(args.prefix_filename, "r") as prefix_file:
            print(prefix_file.read())

    pins.print()
    pins.print_header(args.hdr_filename)


if __name__ == "__main__":
    main()
