import os.path
import pandas as pd

from .dotnet import from_dotnet_datetime, to_numpy, to_dotnet_datetime
from .query import QueryData, QueryDataCatchment, QueryDataNode, QueryDataReach
from .various import mike1d_quantities, NAME_DELIMITER

from System import DateTime
from DHI.Mike1D.ResultDataAccess import ResultData, ResultDataQuery, Filter, DataItemFilterName
from DHI.Mike1D.Generic import Connection, Diagnostics


class Res1D:

    def __init__(self,
                 file_path=None,
                 lazy_load=False,
                 put_chainage_in_col_name=True,
                 reaches=None,
                 nodes=None,
                 catchments=None,
                 header_load=False):

        self.file_path = file_path
        self._put_chainage_in_col_name = put_chainage_in_col_name
        self._lazy_load = lazy_load

        self._reaches = reaches if reaches else []
        self._nodes = nodes if nodes else []
        self._catchments = catchments if catchments else []

        self._use_filter = (reaches is not None or
                            nodes is not None or
                            catchments is not None)

        self._time_index = None
        self._start_time = None
        self._end_time = None

        self._load_header()
        if not header_load:
            self._load_file()

    def __repr__(self):
        out = ["<mikeio1d.Res1D>"]

        if self.file_path:
            out.append(f"Start time: {str(self.start_time)}")
            out.append(f"End time: {str(self.end_time)}")
            out.append(f"# Timesteps: {str(self.data.NumberOfTimeSteps)}")
            out.append(f"# Catchments: {self.data.Catchments.get_Count()}")
            out.append(f"# Nodes: {self.data.Nodes.get_Count()}")
            out.append(f"# Reaches: {self.data.Reaches.get_Count()}")

            out.append(f"# Globals: {self.data.GlobalData.DataItems.Count}")
            for i, quantity in enumerate(self.data.Quantities):
                out.append(f"{i} - {quantity.Id} <{quantity.EumQuantity.UnitAbbreviation}>")

        return str.join("\n", out)

    #region File loading

    def _load_header(self):
        if not os.path.exists(self.file_path):
            raise FileExistsError(f"File {self.file_path} does not exist.")

        self._data = ResultData()
        self._data.Connection = Connection.Create(self.file_path)
        self._diagnostics = Diagnostics("Loading header")

        if self._lazy_load:
            self._data.Connection.BridgeName = "res1dlazy"

        if self._use_filter:
            self._data.LoadHeader(True, self._diagnostics)
        else:
            self._data.LoadHeader(self._diagnostics)

    def _load_file(self):

        if self._use_filter:
            self._setup_filter()

            for reach in self._reaches:
                self._add_reach(reach)
            for node in self._nodes:
                self._add_node(node)
            for catchment in self._catchments:
                self._add_catchment(catchment)

            self._data.LoadData(self._diagnostics)
        else:
            self._data.Load(self._diagnostics)

        self._query = ResultDataQuery(self._data)

    def _setup_filter(self):
        """
        Setup the filter for result data object.
        """
        if not self._use_filter:
            return

        self._data_filter = Filter()
        self._data_subfilter = DataItemFilterName(self._data)
        self._data_filter.AddDataItemFilter(self._data_subfilter)

        self._data.Parameters.Filter = self._data_filter

    def _add_reach(self, reach_id):
        self._data_subfilter.Reaches.Add(reach_id)

    def _add_node(self, node_id):
        self._data_subfilter.Nodes.Add(node_id)

    def _add_catchment(self, catchment_id):
        self._data_subfilter.Catchments.Add(catchment_id)

    #endregion File loading

    def read(self, queries=None):
        """
        Read loaded .res1d file data.

        Parameters
        ----------
        queries: A single query or a list of queries.
        Default is None = reads all data.
        """

        if queries is None:
            return self.read_all()

        queries = queries if isinstance(queries, list) else [queries]

        dfs = []
        for query in queries:
            df = pd.DataFrame(index=self.time_index)
            df[str(query)] = query.get_values(self)
            dfs.append(df)

        return pd.concat(dfs, axis=1)

    def read_all(self):
        """ Read all data from res1d file to dataframe. """

        dfs = []
        for data_set in self.data.DataSets:

            # Skip filtered data sets
            name = Res1D.get_data_set_name(data_set)
            if self._use_filter and name not in self._catchments + self._reaches + self._nodes:
                continue

            for data_item in data_set.DataItems:
                for values, col_name in Res1D.get_values(
                    data_set, data_item, NAME_DELIMITER, self._put_chainage_in_col_name
                ):
                    df = pd.DataFrame(index=self.time_index)
                    df[col_name] = values
                    dfs.append(df)

        return pd.concat(dfs, axis=1)

    @staticmethod
    def get_values(
        data_set, data_item, col_name_delimiter=":", put_chainage_in_col_name=True
    ):
        """ Get all time series values in given data_item. """
        name = Res1D.get_data_set_name(data_set)
        if data_item.IndexList is None:
            name = "" if name is None else name
            col_name = col_name_delimiter.join([data_item.Quantity.Id, name])
            yield data_item.CreateTimeSeriesData(0), col_name
        else:
            chainages = data_set.GetChainages(data_item)
            for i in range(0, data_item.NumberOfElements):
                if put_chainage_in_col_name:
                    postfix = f"{chainages[i]:g}"
                else:
                    postfix = str(i)

                col_name_i = col_name_delimiter.join(
                    [data_item.Quantity.Id, name, postfix]
                )

                yield data_item.CreateTimeSeriesData(i), col_name_i

    @property
    def time_index(self):
        """ pandas.DatetimeIndex of the time index. """
        if self._time_index is not None:
            return self._time_index

        time_stamps = [from_dotnet_datetime(t) for t in self.data.TimesList]
        self._time_index = pd.DatetimeIndex(time_stamps)
        return self._time_index

    @property
    def start_time(self):
        if self._start_time is not None:
            return self._start_time

        return from_dotnet_datetime(self.data.StartTime)

    @property
    def end_time(self):
        if self._end_time is not None:
            return self._end_time

        return from_dotnet_datetime(self.data.EndTime)

    @property
    def quantities(self):
        """ Quantities in res1d file. """
        return [quantity.Id for quantity in self._data.Quantities]

    @property
    def query(self):
        """
        Object to use for querying the loaded res1d data.
        Returns a C# ResultDataQuery object that has the following methods
        //
        // Summary:
        //     Find element in dataItem that is closest to chainage
        public int FindClosestElement(IRes1DReach reach, double chainage, IDataItem dataItem);
        //
        // Summary:
        //     Find data item in dataSet which quantity matches the given quantityId.
        //     Returns null if none found
        public IDataItem FindDataItem(IRes1DDataSet dataSet, string quantityId);
        //
        // Summary:
        //     Get time series values for quantityId of catchment with id catchmentId. If catchment
        //     or quantity could not be found, null is returned.
        public float[] GetCatchmentValues(string catchmentId, string quantityId);
        //
        // Summary:
        //     Get result file datetimes.
        public DateTime[] GetDateTimes();
        //
        // Summary:
        //     Get result file datetimes as strings.
        public string[] GetDateTimesAsStrings(string format = "u");
        //
        // Summary:
        //     Get time series values for the node with the id nodeId and quantity with id quantityId.
        //     If node or quantity could not be found, null is returned.
        public float[] GetNodeValues(string nodeId, string quantityId);
        //
        // Summary:
        //     Get time series values at the end of of the reach with name reachName and quantity
        //     with id quantityId. If reach or quantity could not be found, null is returned.
        public float[] GetReachEndValues(string reachName, string quantityId);
        //
        // Summary:
        //     Get time series values at the start of of the reach with name reachName and quantity
        //     with id quantityId. If reach or quantity could not be found, null is returned.
        public float[] GetReachStartValues(string reachName, string quantityId);
        //
        // Summary:
        //     Get time series values summing up quantity for all grid points in reach. This
        //     is useful for quantities like water volumes.
        public float[] GetReachSumValues(string reachName, string quantityId);
        //
        // Summary:
        //     Get value at the element that is closest to chainage in reach with name reachName
        //     and quantity with id quantityId, at time time, interpolated if required. If reach
        //     or quantity could not be found, null is returned.
        public float GetReachValue(string reachName, double chainage, string quantityId, DateTime time);
        //
        // Summary:
        //     Get time series values at the element that is closest to chainage in reach with
        //     name reachName and quantity with id quantityId. If reach or quantity could not
        //     be found, null is returned.
        public float[] GetReachValues(string reachName, double chainage, string quantityId);
        """
        return self._query

    @property
    def data(self):
        """
        Object with the loaded res1d data.
        Returns a C# ResultData object that has the following methods:
        // Summary:
        //     Data coverage start
        public DateTime StartTime { get; set; }
        //
        // Summary:
        //     Data coverage end
        public DateTime EndTime { get; set; }
        //
        // Summary:
        //     Number of time steps
        public int NumberOfTimeSteps { get; }
        //
        // Summary:
        //     Time axis for the data.
        public IListDateTimes TimesList { get; set; }
        public ResultTypes ResultType { get; set; }
        //
        // Summary:
        //     List of the contained quantities. Note: This is a derived property
        public IQuantities Quantities { get; }
        //
        // Summary:
        //     List of the contained quantities. Note: This is a derived property
        public IListstrings StructureTypes { get; }
        //
        // Summary:
        //     Get an iterator that iterates over all data items
        public IEnumerable<IDataItem> DataItems { get; }
        //
        // Summary:
        //     Get an iterator that iterates over all data sets
        public IEnumerable<IRes1DDataSet> DataSets { get; }
        //
        // Summary:
        //     List of nodes
        public IRes1DNodes Nodes { get; set; }
        //
        // Summary:
        //     Unit system of the simulation that produced the result data object.
        //     When creating a result data object and storing: Properties of ResultData objects
        //     (coordinates, bottom levels etc.) must always be set in SI units.
        //     When loading a result data object from storage: The DHI.Mike1D.ResultDataAccess.IResultDataParameters.UnitSystem
        //     and DHI.Mike1D.ResultDataAccess.IResultDataParameters.ConvertGeometry can be
        //     used to change units of data and properties in the Result Data object. This property
        //     will maintain the original value and will not be changed by updating the DHI.Mike1D.ResultDataAccess.IResultDataParameters
        public UnitSystem UnitSystem { get; set; }
        //
        // Summary:
        //     List of branches
        public IRes1DReaches Reaches { get; set; }
        //
        // Summary:
        //     Global data. Valid for entire network
        public IRes1DGlobalData GlobalData { get; set; }
        //
        // Summary:
        //     Static data on the network.
        //     Used for user defined markers from MIKE 11 and Critical Levels in MU.
        public IList<INetworkDataDouble> NetworkDatas { get; }
        public float DeleteValue { get; set; }
        public double SecondsBetweenFileFlush { get; set; }
        //
        // Summary:
        //     Result specification
        public ResultSpecification ResultSpecs { get; set; }
        public LoadStatus LoadStatus { get; }
        //
        // Summary:
        //     List of catchments
        public IRes1DCatchments Catchments { get; set; }
        //
        // Summary:
        //     A WKT string for a spatial reference system.
        public string ProjectionString { get; set; }
        """
        return self._data

    @property
    def catchments(self):
        """ Catchments in res1d file. """
        return {Res1D.get_data_set_name(catchment): catchment for catchment in self._data.Catchments}

    @property
    def reaches(self):
        """ Reaches in res1d file. """
        return {Res1D.get_data_set_name(reach): reach for reach in self._data.Reaches}

    @property
    def nodes(self):
        """ Nodes in res1d file. """
        return {Res1D.get_data_set_name(node): node for node in self._data.Nodes}

    @property
    def global_data(self):
        """ Global data items in res1d file. """
        return {Res1D.get_data_set_name(gdat): gdat for gdat in self._data.GlobalData.DataItems}

    def get_catchment_values(self, catchment_id, quantity):
        return to_numpy(self.query.GetCatchmentValues(catchment_id, quantity))

    def get_node_values(self, node_id, quantity):
        return to_numpy(self.query.GetNodeValues(node_id, quantity))

    def get_reach_values(self, reach_name, chainage, quantity):
        return to_numpy(self.query.GetReachValues(reach_name, chainage, quantity))

    def get_reach_value(self, reach_name, chainage, quantity, time):
        time_dotnet = time if isinstance(time, DateTime) else to_dotnet_datetime(time)
        return self.query.GetReachValue(reach_name, chainage, quantity, time_dotnet)

    def get_reach_start_values(self, reach_name, quantity):
        return to_numpy(self.query.GetReachStartValues(reach_name, quantity))

    def get_reach_end_values(self, reach_name, quantity):
        return to_numpy(self.query.GetReachEndValues(reach_name, quantity))

    def get_reach_sum_values(self, reach_name, quantity):
        return to_numpy(self.query.GetReachSumValues(reach_name, quantity))

    @staticmethod
    def get_data_set_name(data_set):
        return data_set.Name if hasattr(data_set, "Name") else data_set.Id
