# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.

from audio_dsp.tuning.transport import (
    CommandPayload,
    TuningTransport,
    MultiValType,
    DeviceConnectionError,
)
from pathlib import Path
import platform
import subprocess

PathLike = str | Path

INT_LIKE_TYPES = [
    "uint8_t",
    "int8_t",
    "int16_t",
    "int",
    "int32_t",
    "uint32_t",
    "int32_t*",
]


class InvalidHostAppError(Exception):
    """Raised for issues with the host application itself e.g. the path cannot be found"""

    pass


class USBTransport(TuningTransport):
    def __init__(self, host_app_path: PathLike = Path("dsp_host")) -> None:
        self.connected = False

        if not (isinstance(host_app_path, str) or isinstance(host_app_path, Path)):
            raise TypeError

        self.host_app_path = Path(host_app_path)

        if (
            platform.system().lower() == "windows"
            and self.host_app_path.suffix != ".exe"
        ):
            self.host_app_path = self.host_app_path.with_suffix(".exe")

    def _construct_cmd_list(self, payload: CommandPayload) -> list[str]:
        cmd_list = f"{self.host_app_path} --instance-id {payload.stage.index} {payload.stage.name}_{payload.command}".split()
        if payload.values is not None:
            if isinstance(payload.values, list) or isinstance(payload.values, tuple):
                cmd_list += [str(v) for v in payload.values]
            else:
                cmd_list.append(str(payload.values))
        return cmd_list

    def _from_strings(self, payload: CommandPayload, out: list[str]) -> MultiValType:
        if payload.cmd_type in INT_LIKE_TYPES:
            if len(out) == 1:
                return int(out[0])
            else:
                return [int(x) for x in out]
        else:
            if len(out) == 1:
                return float(out[0])
            else:
                return [float(x) for x in out]

    def connect(self) -> TuningTransport:
        if not self.host_app_path.exists():
            raise InvalidHostAppError(f"Host app not found at {self.host_app_path}")
        if not self.connected:
            self.connected = True
        return self

    def write(self, payload: CommandPayload, verbose=False) -> int:
        if not self.connected:
            raise DeviceConnectionError

        cmd = self._construct_cmd_list(payload)
        ret = subprocess.run(cmd, stdout=subprocess.PIPE)

        if verbose:
            print(f"Write: {' '.join(cmd)}")

        if ret.returncode:
            raise DeviceConnectionError(
                f"Unable to connect to device using {self.host_app_path}."
            )

        return ret.returncode

    def read(self, payload: CommandPayload, verbose=False) -> MultiValType:
        if not self.connected:
            raise DeviceConnectionError

        cmd = self._construct_cmd_list(payload)
        ret = subprocess.run(cmd, stdout=subprocess.PIPE)

        if verbose:
            print(f"Read: {' '.join(cmd)}")

        if ret.returncode:
            raise DeviceConnectionError(
                f"Unable to connect to device using {self.host_app_path}."
            )

        return self._from_strings(
            payload, [x.strip() for x in ret.stdout.decode().splitlines()]
        )

    def disconnect(self):
        self.connected = False
