"""Additional utility functions for Veles"""

import itertools
import inspect
import re
from pathlib import Path
from base64 import b64encode
from warnings import warn


def flatten(args: tuple) -> list:
    """Flatten a list of lists or touples"""
    args = [*args]
    args = [[arg] if not isinstance(arg, (list, tuple)) else arg for arg in args]
    return list(itertools.chain.from_iterable(args))


def dict_without_defaults(self) -> dict:
    "Return a dictionary of the changed object's attributes"
    # Values that are set to their default values differently from the SurveyJS default values
    # "attribute": "default value in SurveyJS"
    custom_defaults = {}

    return {
        k: v
        for k, v in vars(self).items()
        if (custom_defaults.get(k) is not None and v != custom_defaults.get(k))
        or (
            k not in ["questions", "pages", "validators", "addCode", "columns", "rows"]
            and v != self.model_fields[k].default
        )
        or (k == "type")
    }


def get_class_attributes(cls):
    attributes = []

    # Loop over the model fields, including inherited ones
    for name, field in cls.model_fields.items():
        # Get the type annotation from `field.annotation`
        attr_type = field.annotation

        # Handle None or optional types (Pydantic uses `NoneType` for fields with `None`)
        if hasattr(attr_type, "__name__"):
            attr_type_str = attr_type.__name__
        else:
            attr_type_str = str(attr_type).replace("typing.", "")

        # Handle the default value
        if field.default is not None:
            default = field.default
        elif field.default_factory is not None:
            default = field.default_factory()
        else:
            default = None

        # Append the attribute in the desired format
        if name not in [
            "type",
            "name",
            "title",
            "choices",
            "columns",
            "pages",
            "questions",
        ]:
            attributes.append(f"{name}: {attr_type_str} = {default!r}")

    # Join attributes as comma-separated values
    return ",".join(attributes) + ","


def get_class_attributes_assignments(cls):
    # Extract all field names from the model
    attribute_assignments = []
    for name in cls.model_fields:
        if name not in [
            "type",
            "name",
            "title",
            "choices",
            "columns",
            "pages",
            "questions",
        ]:
            attribute_assignments.append(f'"{name}": {name}')

    # Join them as a comma-separated string
    return "args = {" + ", ".join(attribute_assignments) + "}"


def create_docs(func_name: callable):
    signature = str(inspect.signature(func_name))
    signature = re.sub(", ", ",\n    ", signature)
    signature = re.sub(r"\(", "(\n    ", signature)
    signature = re.sub(r"\)", "\n)", signature)

    docstring = inspect.getdoc(func_name).split("\n")
    description = re.sub(r"(?<!\.)$", ".", docstring[0])
    args = docstring[4:]

    args = [re.sub("^ +", "", arg) for arg in args]
    args = [re.sub(r"^(\w+)", r"**`\1`**", arg) for arg in args]
    args = [re.sub(r"\((.+)\): ", r": _\1_<br>\n", arg) for arg in args]

    string = f"""# `{func_name.__name__}()`

{description}

![The {func_name.__name__} question example](./figs/question_types/{func_name.__name__}.png)

## Signature

```{{python}}
#| eval: false
{func_name.__name__}{signature}
```

## Arguments

{"\n\n".join(args)}"""

    string = re.sub(r"velesresearch\.models\.", "", string)

    if func_name.__name__ in ["survey", "page", "panel"]:
        string = re.sub(".+./figs/question_types.+\n\n", "", string)

    return string


def convertImage(*files: Path | str | list[Path | str]) -> str | list[str]:
    """Convert an images to base64; used for the `image` question type

    Args:
        files (Path | str): The path to the image file.
    """
    files = [
        Path(file) if not isinstance(file, Path) else file
        for file in flatten(list(files))
    ]
    base64 = []
    for file in files:
        if file.stat().st_size > 1048576:  # 1 MB
            warn("Your image is larger than 1 MB, consider compressing it.")
        extension = file.suffix[1:]
        with open(file, "rb") as image:
            string = str(b64encode(image.read()), encoding="UTF-8")
            string = f"data:image/{extension};charset=utf-8;base64,{string}"
            base64.append(string)
    if len(base64) == 1:
        return base64[0]
    return base64


def getJS(*files: Path | str | list[Path | str]) -> str:
    """Get the contents of a .js file and return it as a string"""
    files = [
        Path(file) if not isinstance(file, Path) else file
        for file in flatten(list(files))
    ]
    contents = []
    for file in files:
        with open(file, "r", encoding="UTF-8") as file:
            data = file.read()
            contents.append(data)

    return "\n\n".join(contents)
