# SPDX-FileCopyrightText: 2014-2025 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later

import errno
import os
import struct
import time

from .logger import log
from .util import FatalError, PrintOnce

# Used for resetting into bootloader on Unix-like systems
if os.name != "nt":
    import fcntl
    import termios

    # Constants used for terminal status lines reading/setting.
    # Taken from pySerial's backend for IO:
    # https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
    TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
    TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
    TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
    TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)

DEFAULT_RESET_DELAY = 0.05  # default time to wait before releasing boot pin after reset


class ResetStrategy:
    print_once = PrintOnce(log.warning)

    def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
        self.port = port
        self.reset_delay = reset_delay

    def __call__(self):
        """
        On targets with USB modes, the reset process can cause the port to
        disconnect / reconnect during reset.
        This will retry reconnections on ports that
        drop out during the reset sequence.
        """
        for retry in reversed(range(3)):
            try:
                if not self.port.isOpen():
                    self.port.open()
                self.reset()
                break
            except OSError as e:
                # ENOTTY for TIOCMSET; EINVAL for TIOCMGET
                if e.errno in [errno.ENOTTY, errno.EINVAL]:
                    self.print_once(
                        "Chip was NOT reset. Setting RTS/DTR lines is not "
                        f"supported for port '{self.port.name}'. Set --before and "
                        "--after arguments to 'no-reset' and switch to bootloader "
                        "manually to avoid this warning."
                    )
                    break
                elif not retry:
                    raise
                self.port.close()
                time.sleep(0.5)

    def reset(self):
        pass

    def _setDTR(self, state):
        self.port.setDTR(state)

    def _setRTS(self, state):
        self.port.setRTS(state)
        # Work-around for adapters on Windows using the usbser.sys driver:
        # generate a dummy change to DTR so that the set-control-line-state
        # request is sent with the updated RTS state and the same DTR state
        self.port.setDTR(self.port.dtr)

    def _setDTRandRTS(self, dtr=False, rts=False):
        status = struct.unpack(
            "I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
        )[0]
        if dtr:
            status |= TIOCM_DTR
        else:
            status &= ~TIOCM_DTR
        if rts:
            status |= TIOCM_RTS
        else:
            status &= ~TIOCM_RTS
        fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))


class ClassicReset(ResetStrategy):
    """
    Classic reset sequence, sets DTR and RTS lines sequentially.
    """

    def reset(self):
        self._setDTR(False)  # IO0=HIGH
        self._setRTS(True)  # EN=LOW, chip in reset
        time.sleep(0.1)
        self._setDTR(True)  # IO0=LOW
        self._setRTS(False)  # EN=HIGH, chip out of reset
        time.sleep(self.reset_delay)
        self._setDTR(False)  # IO0=HIGH, done


class UnixTightReset(ResetStrategy):
    """
    UNIX-only reset sequence with custom implementation,
    which allows setting DTR and RTS lines at the same time.
    """

    def reset(self):
        self._setDTRandRTS(False, False)
        self._setDTRandRTS(True, True)
        self._setDTRandRTS(False, True)  # IO0=HIGH & EN=LOW, chip in reset
        time.sleep(0.1)
        self._setDTRandRTS(True, False)  # IO0=LOW & EN=HIGH, chip out of reset
        time.sleep(self.reset_delay)
        self._setDTRandRTS(False, False)  # IO0=HIGH, done
        self._setDTR(False)  # Needed in some environments to ensure IO0=HIGH


class USBJTAGSerialReset(ResetStrategy):
    """
    Custom reset sequence, which is required when the device
    is connecting via its USB-JTAG-Serial peripheral.
    """

    def reset(self):
        self._setRTS(False)
        self._setDTR(False)  # Idle
        time.sleep(0.1)
        self._setDTR(True)  # Set IO0
        self._setRTS(False)
        time.sleep(0.1)
        self._setRTS(True)  # Reset. Calls inverted to go through (1,1) instead of (0,0)
        self._setDTR(False)
        self._setRTS(True)  # RTS set as Windows only propagates DTR on RTS setting
        time.sleep(0.1)
        self._setDTR(False)
        self._setRTS(False)  # Chip out of reset


class HardReset(ResetStrategy):
    """
    Reset sequence for hard resetting the chip.
    Can be used to reset out of the bootloader or to restart a running app.
    """

    def __init__(self, port, uses_usb=False):
        super().__init__(port)
        self.uses_usb = uses_usb

    def reset(self):
        self._setRTS(True)  # EN->LOW
        if self.uses_usb:
            # Give the chip some time to come out of reset,
            # to be able to handle further DTR/RTS transitions
            time.sleep(0.2)
            self._setRTS(False)
            time.sleep(0.2)
        else:
            time.sleep(0.1)
            self._setRTS(False)


class CustomReset(ResetStrategy):
    """
    Custom reset strategy defined with a string.

    CustomReset object is created as "rst = CustomReset(port, seq_str)"
    and can be later executed simply with "rst()"

    The seq_str input string consists of individual commands divided by "|".
    Commands (e.g. R0) are defined by a code (R) and an argument (0).

    The commands are:
    D: setDTR - 1=True / 0=False
    R: setRTS - 1=True / 0=False
    U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
    W: Wait (time delay) - positive float number

    e.g.
    "D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
    "U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
    """

    format_dict = {
        "D": "self.port.setDTR({})",
        "R": "self.port.setRTS({})",
        "W": "time.sleep({})",
        "U": "self._setDTRandRTS({})",
    }

    def reset(self):
        exec(self.constructed_strategy)

    def __init__(self, port, seq_str):
        super().__init__(port)
        self.constructed_strategy = self._parse_string_to_seq(seq_str)

    def _parse_string_to_seq(self, seq_str):
        try:
            cmds = seq_str.split("|")
            fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
        except Exception as e:
            raise FatalError(f"Invalid custom reset sequence option format: {e}")
        return "\n".join(fn_calls_list)
