import re
from dataclasses import dataclass, field, replace
from enum import Enum
from os import path

import numpy as np

from sasdata.ascii_reader_metadata import (
    AsciiMetadataCategory,
    AsciiReaderMetadata,
    bidirectional_pairings,
    pairings,
)
from sasdata.data import SasData
from sasdata.dataset_types import DatasetType, one_dim, unit_kinds
from sasdata.default_units import get_default_unit
from sasdata.guess import (
    guess_column_count,
    guess_columns,
    guess_dataset_type,
    guess_starting_position,
)
from sasdata.metadata import Metadata, MetaNode
from sasdata.quantities.quantity import Quantity
from sasdata.quantities.units import NamedUnit


class AsciiSeparator(Enum):
    Comma = (0,)
    Whitespace = (1,)
    Tab = 2


# TODO: Turn them all of for now so the caller can turn one of them on. But is this the desired behaviour?
def initialise_separator_dict(initial_value: bool = False) -> dict[str, bool]:
    return {"Whitespace": initial_value, "Comma": initial_value, "Tab": initial_value}


@dataclass
class AsciiReaderParams:
    """This object contains the parameters that are used to load a series of
    ASCII files. These parameters can be generated by the ASCII Reader Dialog
    when using SasView."""

    # These will be the FULL file path. Will need to convert to basenames for some functions.
    filenames: list[str]
    # The unit object for the column should only be None if the column is <ignore>!
    columns: list[tuple[str, NamedUnit | None]]
    metadata: AsciiReaderMetadata = field(default_factory=AsciiReaderMetadata)
    starting_line: int = 0
    excluded_lines: set[int] = field(default_factory=set)
    separator_dict: dict[str, bool] = field(default_factory=initialise_separator_dict)
    # Take a copy in case its mutated (which it shouldn't be)
    dataset_type: DatasetType = field(default_factory=lambda: replace(one_dim))

    def __post_init__(self):
        self.initialise_metadata()

    def initialise_metadata(self):
        for filename in self.filenames:
            basename = path.basename(filename)
            if basename not in self.metadata.filename_separator:
                self.metadata.filename_separator[basename] = "_"
                self.metadata.filename_specific_metadata[basename] = {}

    @property
    def columns_included(self) -> list[tuple[str, NamedUnit]]:
        return [
            column
            for column in self.columns
            if column[0] != "<ignore>" and isinstance(column[1], NamedUnit)
        ]


# TODO: Should I make this work on a list of filenames as well?
def guess_params_from_filename(
    filename: str, dataset_type: DatasetType
) -> AsciiReaderParams:
    # Lets just assume we want all of the seaprators on. This seems to work for most files.
    separator_dict = initialise_separator_dict(True)
    with open(filename) as file:
        lines = file.readlines()
        lines_split = [split_line(separator_dict, line) for line in lines]
        startpos = guess_starting_position(lines_split)
        colcount = guess_column_count(lines_split, startpos)
        columns = [
            (x, get_default_unit(x, unit_kinds[x]))
            for x in guess_columns(colcount, dataset_type)
            if x in unit_kinds
        ]
        params = AsciiReaderParams(
            [filename],
            columns,
            starting_line=startpos,
            separator_dict=separator_dict,
            dataset_type=guess_dataset_type(filename),
        )
        return params


def split_line(separator_dict: dict[str, bool], line: str) -> list[str]:
    """Split a line in a CSV file based on which seperators the user has
    selected on the widget.

    """
    expr = ""
    for seperator, isenabled in separator_dict.items():
        if isenabled:
            if expr != r"":
                expr += r"|"
            match seperator:
                case "Comma":
                    seperator_text = r","
                case "Whitespace":
                    seperator_text = r"\s+"
                case "Tab":
                    seperator_text = r"\t"
            expr += seperator_text

    return re.split(expr, line.strip())


# TODO: Implement error handling.
def load_quantities(params: AsciiReaderParams, filename: str) -> dict[str, Quantity]:
    """Load a list of quantities from the filename based on the params."""
    with open(filename) as ascii_file:
        lines = ascii_file.readlines()
        arrays: list[np.ndarray] = []
        for _ in params.columns_included:
            arrays.append(np.zeros(len(lines) - params.starting_line))
        for i, current_line in enumerate(lines):
            if i < params.starting_line or current_line in params.excluded_lines:
                continue
            line_split = split_line(params.separator_dict, current_line)
            try:
                for j, token in enumerate(line_split):
                    # Sometimes in the split, there might be an extra column that doesn't need to be there (e.g. an empty
                    # string.) This won't convert to a float so we need to ignore it.
                    if j >= len(params.columns_included):
                        continue
                    # TODO: Data might not be floats. Maybe don't hard code this.
                    arrays[j][i - params.starting_line] = float(token)
            except ValueError:
                # If any of the lines contain non-numerical data, then this line can't be read in as a quantity so it
                # should be ignored entirely.
                print(f"Line {i + 1} skipped.")
                continue
    file_quantities = {
        name: Quantity(arrays[i], unit)
        for i, (name, unit) in enumerate(params.columns_included)
    }
    return file_quantities


def import_metadata(metadata: dict[str, AsciiMetadataCategory[str]]) -> MetaNode:
    root_contents = []
    for top_level_key, top_level_item in metadata.items():
        children = []
        for metadatum_name, metadatum in top_level_item.values.items():
            children.append(MetaNode(name=metadatum_name, attrs={}, contents=metadatum))
        if top_level_key == "other":
            root_contents.extend(children)
        else:
            group = MetaNode(name=top_level_key, attrs={}, contents=children)
            root_contents.append(group)
    return MetaNode(name="root", attrs={}, contents=root_contents)


def merge_uncertainties(quantities: dict[str, Quantity]) -> dict[str, Quantity]:
    """Data in the ASCII files will have the uncertainties in a separate column.
    This function will merge columns of data with the columns containing their
    uncertainties so that both are in one Quantity object."""
    new_quantities: dict[str, Quantity] = {}
    error_quantity_names = pairings.values()
    for name, quantity in quantities.items():
        if name in error_quantity_names:
            continue
        pairing = bidirectional_pairings.get(name, "")
        error_quantity = None
        for other_name, other_quantity in quantities.items():
            if other_name == pairing:
                error_quantity = other_quantity
        if error_quantity is not None:
            to_add = quantity.with_standard_error(error_quantity)
        else:
            to_add = quantity
        new_quantities[name] = to_add
    return new_quantities


def load_data(params: AsciiReaderParams) -> list[SasData]:
    """This loads a series of SasData objects based on the params. The amount of
    SasData objects loaded will depend on how many filenames are present in the
    list contained in the params."""
    loaded_data: list[SasData] = []
    for filename in params.filenames:
        quantities = load_quantities(params, filename)
        raw_metadata = import_metadata(
            params.metadata.all_file_metadata(path.basename(filename))
        )
        metadata = Metadata(
            title=None,
            run=[],
            definition=None,
            sample=None,
            instrument=None,
            process=None,
            raw=raw_metadata,
        )
        data = SasData(
            path.basename(filename),
            merge_uncertainties(quantities),
            params.dataset_type,
            metadata,
        )
        loaded_data.append(data)
    return loaded_data


def load_data_default_params(filename: str) -> list[SasData]:
    params = guess_params_from_filename(filename, guess_dataset_type(filename))
    return load_data(params)
