import re
import shutil
import pickle
import pandas as pd
from pathlib import Path
from .utils import load_yaml_file


class Configuration:
    _CONFIG_PATH = Path('../conf/config.yaml')

    def __init__(self, other=None):
        if other is None:
            _my_path = Path(__file__).parent
            self._config_file = _my_path / Configuration._CONFIG_PATH
        else:
            other_file = Path(other)
            if other_file.suffix != '.yaml':
                raise IOError(f"Incorrect file extension {repr(other_file.suffix)}. It must be a 'yaml' file.")
            self._config_file = other_file

        if not self._config_file.is_file():
            raise FileNotFoundError(f"Configuration file {repr(self._config_file.name)} "
                                    f"not found in {repr(str(self._config_file.parent))}.")

    def __enter__(self):
        self.content = load_yaml_file(self._config_file)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.content.clear()

    def get(self, key):
        if isinstance(key, str):
            return self.content.get(key)
        elif isinstance(key, list):
            return {k: self.content.get(k) for k in key}

    def get_full(self):
        return self.content.copy()

    def set(self, key, value):
        self.content[key] = value


class DotDict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__


class Workspace:
    """
    Manages the output files generated by the IH/IS analysis, loading them properly and decoupling this task from the
    other modules.
    """

    FILES = {
        'data': 'data.csv',
        'metadata': 'metadata.csv',
        'ih_values': 'ih.csv',
        'is_coordinates': 'coordinates.csv',
        'footprint_performance': 'footprint_performance.csv'
    }
    ISA_COLUMNS = {
        'index': 'Row',
        'dim1': 'z_1',
        'dim2': 'z_2'
    }
    INDEX = 'instances'

    def __init__(self, dir_path, data_path=None):
        path = Path(dir_path)
        my_path = Path(__file__).parent
        if not path.is_dir():
            raise NotADirectoryError(f"Invalid path '{str(path)}', it is not a directory.")
        elif not path.is_absolute():
            self.path = my_path / path
        else:
            self.path = path

        if data_path is None:
            self.data_path = self.path / self.FILES['data']
        else:
            path = Path(data_path)
            if not path.is_file():
                raise FileNotFoundError(f"Invalid file path '{str(path)}'.")
            elif not path.is_absolute():
                self.data_path = my_path / path
            else:
                self.data_path = path

        self.files = DotDict(self.FILES)
        self.isa_cols = DotDict(self.ISA_COLUMNS)

        self.data = self.metadata = self.metadata_ext = self.ih_values = None
        self.is_coordinates = self.footprints = self.footprint_performance = None

    def __enter__(self):
        self.load()

    def __exit__(self, exc_type, exc_val, exc_tb):
        del self.data, self.metadata, self.metadata_ext, self.ih_values
        del self.is_coordinates, self.footprints, self.footprint_performance

    def __getstate__(self):
        return self.__dict__

    def __setstate__(self, d):
        self.__dict__.update(d)

    def _call_method(self, name, **kwargs):
        return getattr(self, name)(**kwargs)

    def load(self):
        """
        Loads result files into memory. The following files are expected to be located into `dir_path`:

        - `data.csv`: original dataset
        - `metadata.csv`: generated metadata
        - `ih.csv`: instance hardness values
        - `coordinates.csv`: instance space coordinates
        - `footprint_algo_(good | bad)` like files
        - `footprint_performance.csv`: footprint performance
        """
        self.data = pd.read_csv(self.data_path)
        self.metadata = pd.read_csv(self.path / self.files.metadata, index_col=self.INDEX)
        self.ih_values = pd.read_csv(self.path / self.files.ih_values, index_col=self.INDEX)
        df_metadata_ext = self.ih_values.join(self.metadata, how='right')

        df_is = pd.read_csv(self.path / self.files.is_coordinates, index_col=self.isa_cols.index)
        df_foot_perf = pd.read_csv(self.path / self.files.footprint_performance, index_col=self.isa_cols.index)
        df_foot_perf.index.name = 'Algorithm'

        pattern = re.compile('(^footprint)_(.+)_(good|bad|best)', re.IGNORECASE)
        footprint_files = [u.name for u in self.path.glob('*.csv')
                           if u.is_file() and bool(pattern.search(u.name))]
        fp_dict = dict()
        for file in footprint_files:
            g = pattern.match(file).groups()
            try:
                fp_dict[(g[1], g[2])] = pd.read_csv(self.path / file,
                                                    usecols=[self.isa_cols.index,
                                                             self.isa_cols.dim1,
                                                             self.isa_cols.dim2],
                                                    index_col=self.isa_cols.index)
            except ValueError:
                continue
        df_footprint = pd.concat(fp_dict)
        df_footprint.reset_index(level=self.isa_cols.index, drop=True, inplace=True)
        df_footprint.index.names = ['algo', 'type']
        df_footprint.sort_index(inplace=True)

        self.data.index = self.metadata.index
        df_is.index.name = df_metadata_ext.index.name

        self.metadata_ext = df_metadata_ext
        self.is_coordinates = df_is
        self.footprints = df_footprint
        self.footprint_performance = df_foot_perf

    def get_data(self):
        if self.data is not None:
            return self.data.copy()
        else:
            raise RuntimeError("Workspace not yet loaded.")

    def get_metadata(self):
        if self.metadata is not None:
            return self.metadata.copy()
        else:
            raise RuntimeError("Workspace not yet loaded.")

    def get_ih_values(self):
        if self.ih_values is not None:
            return self.ih_values.copy()
        else:
            raise RuntimeError("Workspace not yet loaded.")

    def get_is_coordinates(self):
        if self.is_coordinates is not None:
            return self.is_coordinates.copy()
        else:
            raise RuntimeError("Workspace not yet loaded.")

    def get_footprints(self):
        if self.footprints is not None:
            return self.footprints.copy()
        else:
            raise RuntimeError("Workspace not yet loaded.")

    def get_footprint_performance(self):
        if self.footprint_performance is not None:
            return self.footprint_performance.copy()
        else:
            raise RuntimeError("Workspace not yet loaded.")

    def get_all_files(self, extended_metadata=True):
        output = {key: self._call_method(f"get_{key}") for key in self.FILES.keys()}

        if extended_metadata:
            _ = output.pop('metadata')
            output['metadata_ext'] = self.metadata_ext.copy()

        return output

    def compress(self, file_name, dest):
        """
        Compresses workspace associated files and saves in zip format.

        :param str file_name: name of output zip file
        :param str dest: destination folder. Must be different from workspace directory
        """
        path_dest = Path(dest)
        if self.path == path_dest:
            raise RuntimeError("Workspace dir and dest dir must be different.")
        if not path_dest.is_dir():
            raise NotADirectoryError(f"Destination folder {repr(str(path_dest))} is not a directory.")

        shutil.make_archive(path_dest / file_name, 'zip', self.path)

    def serialize(self, file_name=None):
        """
        Serializes workspace data and saves to file.

        :param str file_name: name of output file. Automatically assigns '.pickle' extension
        """
        if file_name is None:
            file_name = 'result'
        with open(self.path / file_name + '.pickle', 'wb') as file:
            pickle.dump(self.get_all_files(), file)
