import copy
import numbers
import re
from collections.abc import Iterable, Mapping
from typing import Union
from urllib.parse import urlparse

import numpy as np
import pandas as pd
import webcolors


def strip_state_properties(
    state: dict,
    strip_keys: list[str] = [],
) -> dict:
    """Remove layers from state"""
    state = copy.deepcopy(state)
    to_del = []
    for k, v in state.items():
        if k in strip_keys:
            if isinstance(v, list):
                state[k] = []
            else:
                to_del.append(k)
    for k in to_del:
        del state[k]
    return state


def strip_layers(
    state: dict,
) -> dict:
    """Remove layers from a state while preserving other properties. Intended to modify an existing state into a base state."""
    return strip_state_properties(
        state, strip_keys=["layers", "selectedLayer", "selection"]
    )


class NamedList(list):
    """A list that can be indexed by name."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._name_map = {str(item.name): item for item in self}

    def __getitem__(self, key):
        if isinstance(key, str):
            return self._name_map[key]
        return super().__getitem__(key)

    def append(self, item):
        super().append(item)
        self._name_map[str(item.name)] = item

    def extend(self, items):
        super().extend(items)
        for item in items:
            self._name_map[str(item.name)] = item


def split_point_columns(col_name: str, columns: list[str]) -> Union[str, list[str]]:
    if col_name in columns:
        return col_name
    suffixes = ["x", "y", "z"]
    resolved_columns = [f"{col_name}_{suffix}" for suffix in suffixes]

    if all(col in columns for col in resolved_columns):
        return resolved_columns
    raise ValueError(
        f"Column '{col_name}' not found, and '{col_name}_x', '{col_name}_y', '{col_name}_z' are not all present in the columns."
    )


def strip_numpy_types(obj):
    """
    Recursively convert numpy types in scalars, list-like, or dictionary values
    into pure Python types.

    Parameters
    ----------
    obj : any
        The object to process (scalar, list-like, or dictionary).

    Returns
    -------
    any
        The object with all numpy types converted to pure Python types.
    """
    if isinstance(obj, (np.generic, np.ndarray)):
        # Convert numpy scalars or arrays to Python types or lists
        if np.isscalar(obj):
            return obj.item()
        else:
            return [strip_numpy_types(x) for x in obj]
    elif isinstance(obj, pd.Series):
        # Convert pandas Series to a list of Python types
        return [strip_numpy_types(x) for x in obj.values]
    elif isinstance(obj, Mapping):
        # Recursively process dictionary values
        return {strip_numpy_types(k): strip_numpy_types(v) for k, v in obj.items()}
    elif isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)):
        # Recursively process list-like objects
        return [strip_numpy_types(x) for x in obj]
    else:
        # Return the object as-is if it's already a pure Python type
        return obj


def list_of_lists(obj):
    "Strip numpy types and return as a list of lists. Should not be used for dicts."
    if not isinstance(obj, Iterable) and pd.isnull(obj):
        return None
    obj = strip_numpy_types(obj)
    if isinstance(obj, list):
        return [obj]
    else:
        return [[obj]]


def list_of_strings(obj):
    if isinstance(obj, str):
        return [obj]
    elif isinstance(obj, Iterable):
        return [str(x) for x in obj if not pd.isna(x)]
    else:
        return []


def none_or_array(obj):
    if obj:
        return np.array(obj)
    else:
        return None


def is_list_like(obj):
    """
    Check if an object is list-like (iterable but not a mapping or string).

    Parameters
    ----------
    obj : any
        Object to check

    Returns
    -------
    bool
        True if the object is list-like, False otherwise
    """
    return (
        isinstance(obj, Iterable)
        and not isinstance(obj, Mapping)
        and not isinstance(obj, str)
    )


def is_dict_like(obj):
    """
    Check if an object is dictionary-like (mapping).

    Parameters
    ----------
    obj : any
        Object to check

    Returns
    -------
    bool
        True if the object is dictionary-like, False otherwise
    """
    return isinstance(obj, Mapping) and not isinstance(obj, str)


def omit_nones(seg_list):
    if seg_list is None or np.all(pd.isna(seg_list)):
        return []
    seg_list = np.atleast_1d(seg_list)
    seg_list = list(filter(lambda x: x is not None, seg_list))
    if len(seg_list) == 0:
        return []
    else:
        return seg_list


def parse_color(clr):
    if clr is None:
        return None

    if isinstance(clr, numbers.Number):
        clr = (clr, clr, clr)

    if isinstance(clr, str):
        hex_match = r"\#[0123456789abcdef]{6}"
        if re.match(hex_match, clr.lower()):
            return clr
        else:
            return webcolors.name_to_hex(clr)
    else:
        return webcolors.rgb_to_hex([int(255 * x) for x in clr])


def parse_graphene_header(source):
    qry = urlparse(source)
    if qry.scheme == "graphene":
        return _parse_to_mainline(qry)
    else:
        return source


def parse_graphene_image_url(source):
    qry = urlparse(source)
    if qry.scheme == "graphene":
        return _parse_to_mainline_imagery(qry)
    else:
        return source


def _parse_to_mainline(qry):
    if "https" in qry.netloc and "middleauth":
        return f"{qry.scheme}://middleauth+https:{qry.path}"
    else:
        return f"{qry.scheme}://middleauth+http:{qry.path}"


def _parse_to_mainline_imagery(qry):
    if qry.scheme == "graphene":
        if "https" in qry.netloc:
            return f"precomputed://middleauth+https:{qry.path}"
        else:
            return f"precomputed://middleauth+http:{qry.path}"
    else:
        return qry.geturl()
