"""
Utility to autogenerate pinctrl definitions.

Usage::
    python3 gd32pinctrl.py [-i /path/to/configs] [-o /path/to/include]

Copyright (c) 2021 Teslabs Engineering S.L.
SPDX-License-Identifier: Apache 2.0
"""

import argparse
from collections import OrderedDict
from pathlib import Path
import re

import yaml


REPO_ROOT = Path(__file__).absolute().parents[1]
"""Repository root."""

AFIO_REMAP_SUFFIXES = {
    0: ("NORMP",),
    2: ("NORMP", "RMP"),
    3: ("NORMP", "PRMP", "FRMP"),
    4: ("NORMP", "PRMP1", "PRMP2", "FRMP"),
}
"""AFIO remap suffixes (# remap options <> suffix)."""

AFIO_MODE_NAMES = {
    "analog": "ANALOG",
    "inp": "GPIO_IN",
    "out": "ALTERNATE",
}
"""AFIO mode names."""

HEADER = """/*
 * Autogenerated file
 *
 * SPDX-License-Identifier: Apache 2.0
 */
"""
"""Header for the generated files."""


def get_header_fname(series, variant):
    """Get header file name.

    Args:
        series: Series.
        variant: Variant information.

    Returns:
        Header file name.
    """

    pincode = variant["pincode"].lower()
    memories = f"({'-'.join((str(m).lower() for m in variant['memories']))})"
    return f"{series}{pincode}{memories}xx-pinctrl.h"


def get_port_pin(pin_name):
    """Obtain port and pin number from a pin name

    Args:
        pin_name: Pin name, e.g. PA0

    Returns:
        Port and pin, e.g. A, 0.
    """

    m = re.match(r"P([A-Z])(\d+)", pin_name)
    if not m:
        raise ValueError(f"Unexpected pin name: {pin_name}")

    return m.group(1), m.group(2)


def generate_afio_header(outdir, variant, series, pin_cfgs):
    """Generate AFIO header with pin configurations.

    Args:
        outdir: Output base directory.
        variant: Variant information.
        series: Series.
        pin_cfgs: Pin configurations.
    """

    pin_cfgs = OrderedDict(sorted(pin_cfgs.items(), key=lambda kv: kv[0]))

    with open(outdir / get_header_fname(series, variant), "w") as f:
        f.write(HEADER)
        f.write(f"\n#include \"{series}xx-afio.h\"\n")
        for signal, cfg in pin_cfgs.items():
            f.write(f"\n/* {signal} */\n")
            for port, pin, mode, name_suffix, remap in cfg:
                define = f"#define {signal}_P{port}{pin}{name_suffix}"
                define_val = f"GD32_PINMUX_AFIO('{port}', {pin}, {mode}, {remap})"
                f.write(f"{define} \\\n\t{define_val}\n")


def generate_af_header(outdir, variant, series, pin_cfgs):
    """Generate AF header with pin configurations.

    Args:
        outdir: Output base directory.
        variant: Variant information.
        series: Series.
        pin_cfgs: Pin configurations.
    """

    pin_cfgs = OrderedDict(sorted(pin_cfgs.items(), key=lambda kv: kv[0]))

    with open(outdir / get_header_fname(series, variant), "w") as f:
        f.write(HEADER)
        f.write("\n#include \"gd32-af.h\"\n")
        for signal, cfg in pin_cfgs.items():
            f.write(f"\n/* {signal} */\n")
            for port, pin, mode in cfg:
                define = f"#define {signal}_P{port}{pin}"
                define_val = f"GD32_PINMUX_AF('{port}', {pin}, {mode})"
                f.write(f"{define} \\\n\t{define_val}\n")


def build_afio_pin_cfgs(variant, signal_configs, pins, remaps):
    """Build AFIO pin configurations.

    Args:
        variant: Variant information.
        signal_configs: Signal configurations.
        pins: Pins description.
        remaps: Remaps description.

    Returns:
        Dictionary with pins configuration.
    """

    pin_cfgs = {"ANALOG": []}

    pincode = variant["pincode"]
    memories = variant["memories"]

    for signal, signal_cfg in signal_configs.items():
        # check if signal is excluded from current pincode
        if pincode in signal_cfg.get("exclude-pincodes", []):
            continue

        # check if signal is excluded from current list of memories
        if set(memories).intersection(signal_cfg.get("exclude-memories", [])):
            continue

        signal_pins = {}

        # collect all afs
        for pin, pin_cfg in pins.items():
            if pincode not in pin_cfg["pincodes"]:
                continue
            if signal in pin_cfg["afs"]:
                signal_pins[pin] = [0]

        # collect all remaps
        remap_options = 0
        signal_remaps = remaps.get(signal)
        if signal_remaps:
            for pin in signal_remaps["pins"]:
                if not pin:
                    continue

                if pincode in pins[pin]["pincodes"]:
                    if pin not in signal_pins:
                        signal_pins[pin] = []

                    if remap_options not in signal_pins[pin]:
                        signal_pins[pin].append(remap_options)

                remap_options += 1

        for pin, remap_values in signal_pins.items():
            for remap_value in remap_values:
                for mode in signal_cfg["modes"]:
                    port, pin_number = get_port_pin(pin)
                    remap = AFIO_REMAP_SUFFIXES[remap_options][remap_value]

                    name_suffix = ""
                    if len(signal_cfg["modes"]) > 1:
                        name_suffix = f"_{mode.upper()}"

                    if remap_options > 0:
                        name_suffix += f"_{remap}"
                        remap = signal.split("_")[0] + f"_{remap}"

                    if signal not in pin_cfgs:
                        pin_cfgs[signal] = []

                    pin_cfgs[signal].append(
                        (
                            port,
                            pin_number,
                            AFIO_MODE_NAMES[mode],
                            name_suffix,
                            remap,
                        )
                    )

    # add analog entries (used for low power mode)
    for pin, pin_cfg in pins.items():
        if pincode not in pin_cfg["pincodes"]:
            continue

        port, pin_number = get_port_pin(pin)
        pin_cfgs["ANALOG"].append((port, pin_number, "ANALOG", "", "NORMP"))

    return pin_cfgs


def build_af_pin_cfgs(variant, signal_configs, pins):
    """Build AF pin configurations.

    Args:
        variant: Variant information.
        signal_configs: Signals description.
        pins: Pins description.

    Returns:
        Dictionary with pins configuration.
    """

    pin_cfgs = {"ANALOG": []}

    pincode = variant["pincode"]
    memories = variant["memories"]

    for pin, pin_cfg in pins.items():
        if pincode not in pin_cfg["pincodes"]:
            continue

        port, pin_number = get_port_pin(pin)

        # add analog entry (used for low power mode)
        pin_cfgs["ANALOG"].append((port, pin_number, "ANALOG"))

        for signal, mode in pin_cfg["afs"].items():
            signal_config = signal_configs.get(signal)
            # check if signal is excluded from current pincode.
            if signal_config and pincode in signal_config.get(
                "exclude-pincodes", []
            ):
                continue
            # check if signal is excluded from current list of memories
            if signal_config and set(memories).intersection(
                signal_config.get("exclude-memories", [])
            ):
                continue

            if signal not in pin_cfgs:
                pin_cfgs[signal] = []

            if mode != "ANALOG":
                mode = f"AF{mode}"

            pin_cfgs[signal].append((port, pin_number, mode))

    return pin_cfgs


def main(indir, outdir) -> None:
    """Entry point.

    Args:
        indir: Directory with pin configuration files.
        outdir: Output directory
    """

    if outdir.exists():
        for entry in outdir.glob("gd32*-pinctrl.h"):
            entry.unlink()
    else:
        outdir.mkdir()

    for entry in indir.iterdir():
        if not entry.is_file() or entry.suffix not in (".yml", ".yaml"):
            continue

        config = yaml.load(open(entry), Loader=yaml.Loader)

        model = config["model"]
        series = config["series"]
        variants = config["variants"]
        signal_configs = config.get("signal-configs", {})
        pins = config["pins"]

        if model == "afio":
            remaps = config["remaps"]
            for variant in variants:
                pin_cfgs = build_afio_pin_cfgs(variant, signal_configs, pins, remaps)
                generate_afio_header(outdir, variant, series, pin_cfgs)
        elif model == "af":
            for variant in variants:
                pin_cfgs = build_af_pin_cfgs(variant, signal_configs, pins)
                generate_af_header(outdir, variant, series, pin_cfgs)
        else:
            raise ValueError(f"Unexpected model: {model}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-i",
        "--indir",
        type=Path,
        default=REPO_ROOT / "pinconfigs",
        help="Directory with pin configuration files",
    )
    parser.add_argument(
        "-o",
        "--outdir",
        type=Path,
        default=REPO_ROOT / "include" / "dt-bindings" / "pinctrl",
        help="Output directory",
    )
    args = parser.parse_args()

    main(args.indir, args.outdir)
