"""
Utility functions that don't fit the other modules.
"""
from __future__ import annotations
from logging import Logger
import re
import subprocess
import os
import inspect
import types
from configparser import ConfigParser
import itertools
from typing import TypeVar, Generic, Iterable
import threading

from pexpect import fdpexpect

def get_env_default(var, default: str, logger: Logger):
    """Obtains an environment variable. If its not configured, it instead returns
    the default value and logs a warning message."""
    value = os.environ.get(var)

    if not value:
        value = default
        logger.warning(f"{var} not configured, defaulting to {default}")

    return value

def config_else_env(option: str, section: str, parser: ConfigParser, error=True, default=None):
    """Tries to find option from section of a .ini file. If it fails,
    it instead looks at the environment variable option. If this also
    fails, raises exception.."""
    if parser:
        if section in parser.sections():
            value = parser[section].get(option)
            if value:
                return value

    value = os.environ.get(option)
    if error:
        if not value:
            if default:
                return default
            raise Exception(f"Configuration error. Set {section}.{option} in the configuration or specify {option} as an environment variable.")

    return value

def check_default(devpath) -> bool:
    """Checks for whether a device is running the default firmware."""
    # Sometimes closing the fd takes a long time (> 10s) on some firmwares,
    # this might create issues. I'm not really sure what the cause is, I added
    # a read from stdio to the default firmware and it seems to fix the issue.
    # The same behavior happens from opening and closing the file in C.
    try:
        with open(devpath, "r") as f:
            p = fdpexpect.fdspawn(f, timeout=2)
            p.expect("default firmware", timeout=2)

    except Exception:
        return False

    return True

def get_ip() -> str:
    """Obtains local network ip from hostname -I."""
    res = subprocess.run(["hostname", "-I"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True).stdout
    group = re.search("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", str(res))
    if group:
        return group.group(0)

def typecheck(fn, args) -> bool:
    """Checks whether args are valid types for fn. Only works on classes
    and non nested list generics. For dict, only checks if arg is a dict."""
    params = inspect.signature(fn).parameters.values()

    if len(params) != len(args):
        return False

    for arg, param in zip(args, params):
        annotation = param.annotation

        if annotation is inspect._empty:
            continue

        if inspect.isclass(annotation):
            if not isinstance(arg, annotation):
                return False

            continue

        if not isinstance(annotation, types.GenericAlias):
            return False

        if annotation.__origin__ is dict:
            continue

        if annotation.__origin__ is not list or not isinstance(arg, list):
            return False

        if len(annotation.__args__) != 1:
            return False

        type_ = annotation.__args__[0]

        for value in arg:
            if not isinstance(value, type_):
                return False

    return True

def json_to_args(json, parameters):
    values = list(map(json.get, parameters))
    if any(map(lambda x : x is None, values)):
        return False

    return values

def generate_circuit(hz, build_dir, build_script="src/icefarm/utils/build.sh", pcf_path="src/icefarm/utils/pico_ice.pcf", clk=48000000):
    """Builds a circuit of approximately hz."""
    if not os.path.isdir(build_dir):
        os.mkdir(build_dir)

    incr = int((clk/hz) - 1)
    veri = f"""
    module top (
        input CLK,
        output ICE_27,
        output LED_R,
        output LED_B
    );
    reg [22:0] counter;
    reg out;
    always @(posedge CLK) begin
        counter <= counter + 1;

        if (counter >= {incr}) begin
            out <= 1'b1;
            counter <= 23'b00000000000000000000000;
        end else begin
            out <= 1'b0;
        end
    end

    assign ICE_27 = out;
    assign LED_R = counter[22];
    endmodule
    """

    with open(os.path.join(build_dir, "top.v"), "w") as f:
        f.write(veri)

    subprocess.run(["bash", build_script, build_dir, pcf_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)

    return os.path.join(build_dir, "top.bin"), clk / incr / 1000

# Ideally would inherit dict but that doesn't behave nicely
# TODO should be able to replace this with dict.setdefault
class MappedQueues:
    """
    Manages a dict[Any, list[Any]] so that new lists are
    created if a key does not already exist. Empty list values
    mean that the corresponding key is treated as through it does
    not exist.
    """
    def __init__(self):
        self.state = {}

    def __getitem__(self, key) -> list:
        if key not in self.state:
            self.state[key] = []

        return self.state[key]

    def append(self, key, value):
        self[key].append(value)

    def pop(self, key, amount) -> list:
        """Pops up to amount items from queue key. If less
        than amount items are in queue key, all items will
        be popped.."""
        out = []

        for _ in range(amount):
            if key not in self:
                return out

            out.append(self[key].pop())

        return out

    def __bool__(self):
        return any(self.state.values())

    def __contains__(self, value):
        return bool(self.state.get(value))

    def __iter__(self):
        return itertools.takewhile(self.__getitem__, self.state)

    def keys(self):
        return list(iter(self))

    def values(self):
        return [self[key] for key in self]

def batch(l, amount):
    batches = [[] for _ in range(amount)]
    for i, item in enumerate(l):
        batches[i % amount].append(item)

    return batches


E = TypeVar("E")
class QueueShutDown(Exception): ...

# TODO remove once using python 3.13+
class Queue(Generic[E]):
    """Replacement for queue.Queue with shutdown capabilities."""
    def __init__(self, contents: Iterable[E]=None):
        super().__init__()
        self.contents = list(contents) if contents else []
        self.shutting_down = False
        self.cv = threading.Condition()

    def put(self, item: E | Iterable[E]):
        """Adds an item to queue. Raises QueueShutDown if the queue has shutdown."""
        with self.cv:
            if self.shutting_down:
                raise QueueShutDown

            if isinstance(item, Iterable):
                self.contents.extend(item)
            else:
                self.contents.append(item)

            self.cv.notify_all()

    def pop(self) -> E:
        """Pops an item from the queue, waiting for one to be available if necessary. Raises QueueShutDown
        if the queue has shutdown or does so while waiting."""
        with self.cv:
            self.cv.wait_for(lambda: self.contents or self.shutting_down)

            if self.shutting_down:
                raise QueueShutDown

            return self.contents.pop(0)

    def shutdown(self) -> list[E]:
        """Enters shutdown mode, returns all current contents and empties queue."""
        with self.cv:
            contents = self.contents
            self.contents = []
            self.shutting_down = True

            self.cv.notify_all()
            return contents

    def __len__(self):
        with self.cv:
            if self.shutting_down:
                raise QueueShutDown

            return len(self.contents)

    def __bool__(self):
        with self.cv:
            if self.shutting_down:
                raise QueueShutDown

            return bool(self.contents)
