import gzip
import os
import sys
from datetime import datetime
from os.path import basename
from pytz import timezone
from simses.commons.config.simulation.general import GeneralSimulationConfig
from simses.commons.profile.power.power_file_generator import PowerFileGenerator
from simses.commons.error import EndOfFileError
from simses.commons.log import Logger
from simses.commons.timeseries.average.average import Average
from simses.commons.timeseries.average.mean_average import MeanAverage
from simses.commons.timeseries.interpolation.interpolation import Interpolation
from simses.commons.timeseries.interpolation.linear_interpolation import LinearInterpolation
from simses.commons.timeseries.timevalue import TimeValue
from simses.commons.utils.utilities import download_file


class FileProfile:
    """
    FileProfile is able to read time series from a file. It supports several unit and date formats, takes timezones into
    consideration (default: Berlin) and interpolates respectively averages values of the given time series. If no time series
    is given, a time generator generates the timestep according to the given sampling rate or simulation timestep.

    A header could inherit the given timezone, value unit, time unit and sampling rate:\n
    # Unit: W\n
    # Timezone: Berlin\n
    # Time: s\n
    # Sampling in s: 3600s\n
    """

    class Header:
        TIMEZONE: str = 'Timezone'
        UNIT: str = 'Unit'
        TIME: str = 'Time'
        SAMPLING: str = 'Sampling in s'
        LATITUDE: str = 'Latitude'
        LONGITUDE: str = 'Longitude'

    __UNITS: dict = {'W': 1, 'kW': 1e3, 'MW': 1e6, 'GW': 1e9, 's': 1, 'ms': 0.001, 'EUR': 1, 'TEUR': 1e3, 'Hz': 1, '%': 0.01, 'W/m2': 1, 'C': 1}
    """List of recognized unit formats of FileProfile"""

    __TIME_IDX: int = 0

    __EPOCH_FORMAT: str = 'epoch'
    __DATE_FORMATS: [str] = ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', '%d.%m.%Y %H:%M:%S', '%d.%m.%Y %H:%M',
                             __EPOCH_FORMAT]
    """List of supported date formats"""

    __UTC: timezone = timezone('UTC')
    __BERLIN: timezone = timezone('Europe/Berlin')

    __EXT_GZIP: str = '.gz'
    __EXT_CSV: str = '.csv'
    __SUPPORTED_EXTENSIONS: [str] = list()
    __SUPPORTED_EXTENSIONS.append(__EXT_CSV + __EXT_GZIP)
    __SUPPORTED_EXTENSIONS.append(__EXT_GZIP)
    __SUPPORTED_EXTENSIONS.append(__EXT_CSV)

    __ERROR = None

    __URL: str = 'https://syncandshare.lrz.de/dl/fiACpACzuTyoENQE2qwqrG/profiles/'
    """URL to folder of profiles to download"""

    def __init__(self, config: GeneralSimulationConfig, profile_filename: str, profile_mat_filename: str = None,
                 delimiter: str = ',', scaling_factor: float = 1, value_index: int = 1,
                 interpolation: Interpolation = LinearInterpolation(), average: Average = MeanAverage()):
        """
        Constructor of FileProfile

        Parameters
        ----------
        config :
            simulation configuration
        filename :
            path to file
        delimiter :
            delimiter of values in file, default: ,
        scaling_factor :
            linear scaling of values, default: 1
        value_index :
            column index for values, default: 1
        interpolation:
            provides an Interpolation object, default: LinearInterpolation
        average:
            provides an Average object, default: MeanAverage
        """
        self.__log: Logger = Logger(type(self).__name__)
        self.__log.info('Preparing ' + profile_filename)
        self.__filename: str = profile_filename
        self.__mat_filename: str = profile_mat_filename
        self.__file_generator: PowerFileGenerator = PowerFileGenerator(self.__filename, self.__mat_filename)
        self.generate_file()
        self.__delimiter: str = delimiter
        self.__scaling_factor: float = scaling_factor
        self.__VALUE_IDX: int = value_index
        self.__timestep: float = config.timestep
        self.__start: float = config.start
        self.__end: float = config.end
        self.__time_offset: int = 0
        self.__file = None
        self.__date_format = None
        self.__last_data: [TimeValue] = list()
        self.__last_time: float = self.__start
        self.__interpolation: Interpolation = interpolation
        self.__average: Average = average
        header: dict = self.get_header_from(self.__filename)
        self.__unit_factor: float = self.__get_value_unit_from(header)
        self.__time_factor: float = self.__get_time_unit_from(header)
        self.__timezone: timezone = self.__get_timezone_from(header)
        self.__sampling_time: float = self.__get_sampling_time_from(header)
        self.__time_generator = self.__get_sampling_time_generator()
        self.__latitude: float = self.__get_latitude_from(header)
        self.__longitude: float = self.__get_longitude_from(header)
        self.initialize_file()

    def next(self, time: float) -> float:
        """
        Retrieves the next value for given time from file time series

        Parameters
        ----------
        time :
            time as epoch timestamp

        Returns
        -------
        float :
            (interpolated/averaged) value for given time
        """
        try:
            data: [TimeValue] = self.__get_data_until(time)
            values: [TimeValue] = self.__filter_current_values(time, data)
            if not values and len(data) < 2:
                value: float = self.__average.average(data)
            elif not values:
                value: float = self.__interpolation.interpolate(time, data[-1], data[-2])
            else:
                value: float = self.__average.average(values)
            self.__last_data = self.__filter_last_values(data)
            self.__last_time = time
            return value * self.__scaling_factor
        except EndOfFileError as err:
            self.__log.warn(str(err))
            return 0.0

    def generate_file(self):
        if self.__mat_filename is not None:
            file_out = self.__file_generator.generate_path_for_file(self.__filename, is_file_in=False)
            if file_out is not None and not os.path.exists(file_out):
                self.__file_generator.generate_csv_file()

    def initialize_file(self) -> None:
        """
        Allows to re-initialize the profile to start reading values from the beginning again
        """
        if self.__file is not None:
            if not self.__file.closed:
                self.__file.close()
        self.__file = self.open_file(self.__filename)
        self.__last_data = self.__get_initial_data()

    @classmethod
    def open_file(cls, filename: str):
        """
        Opens file with filename depending on the file type. Currently .gz and .csv are supported.
        The file type extension is added if necessary.

        Parameters
        ----------
        filename :
            path to file

        Returns
        -------
            file object (needs to be closed manually)
        """
        filename: str = cls.__add_extension_to(filename)
        try:
            if filename.endswith(cls.__EXT_GZIP):
                return gzip.open(filename, 'rt')
            elif filename.endswith(cls.__EXT_CSV):
                return open(filename, 'r', newline='')
            else:
                raise FileNotFoundError('Format of ' + filename + ' is currently not supported. Please check your '
                                            'file type. Following file types are supported: ' + str(cls.__SUPPORTED_EXTENSIONS))
        except FileNotFoundError as err:
            if cls.__URL is cls.__ERROR:
                raise err
            print(filename + ' is not available. Searching for file on server...')
            if cls.__profile_downloaded(filename):
                return cls.open_file(filename)
            else:
                raise FileNotFoundError(basename(filename) + ' is currently not available for download.', err)

    @classmethod
    def __profile_downloaded(cls, filename: str) -> bool:
        found: bool = False
        for file in cls.__possible_file_names(filename):
            try:
                download_file(cls.__URL + basename(file), file)
                found = True
                break
            except FileNotFoundError:
                print(basename(file) + ' could not be found on server')
        return found

    @classmethod
    def __possible_file_names(cls, filename: str) -> [str]:
        possible_file_names: [str] = list()
        if cls.__has_extension(filename):
            possible_file_names.append(filename)
        else:
            for extension in cls.__SUPPORTED_EXTENSIONS:
                possible_file_names.append(filename + extension)
        return possible_file_names

    @classmethod
    def __has_extension(cls, filename: str) -> bool:
        has_extension: bool = False
        for extension in cls.__SUPPORTED_EXTENSIONS:
            has_extension |= filename.endswith(extension)
        return has_extension

    @classmethod
    def __add_extension_to(cls, filename: str) -> str:
        has_extension: bool = cls.__has_extension(filename)
        if not has_extension:
            for extension in cls.__SUPPORTED_EXTENSIONS:
                if os.path.isfile(filename + extension):
                    filename += extension
                    break
        return filename

    def __filter_current_values(self, time: float, data: [TimeValue]) -> [TimeValue]:
        values: [TimeValue] = list()
        data_iterator = iter(data)
        try:
            while True:
                value = next(data_iterator)
                if self.__last_time < value.time <= time:
                    values.append(value)
        except StopIteration:
            pass
        return values

    def __filter_last_values(self, data: [TimeValue]) -> [TimeValue]:
        return data[-2:]
        # values: [TimeValue] = list()
        # data_iterator = iter(data)
        # try:
        #     while True:
        #         value = next(data_iterator)
        #         if self.__last_time < value.time:
        #             values.append(value)
        # except StopIteration:
        #     pass
        # return values

    def __get_data_until(self, time: float) -> [TimeValue]:
        values: list = list()
        data = self.__last_data[-1]
        while data.time < time:
            data = self.__get_next_data()
            values.append(data)
        values.extend(self.__last_data)
        TimeValue.sort_by_time(values)
        return values

    def __get_next_data(self) -> TimeValue:
        while True:
            line: str = ''
            try:
                line = self.__file.readline()
                if line.startswith('#') or line.startswith('"') or line in ['\r\n', '\n']:# or self.__delimiter not in line:
                    continue
                if line == '':  # end of file_name
                    raise EndOfFileError('End of Profile ' + self.__filename + ' reached.')
                data = line.split(self.__delimiter)
                if len(data) < 2:
                    time: float = next(self.__time_generator) + self.__time_offset
                    value: float = float(data[self.__VALUE_IDX - 1]) * self.__unit_factor
                else:
                    time: float = self.__format_time(data[self.__TIME_IDX]) * self.__time_factor + self.__time_offset
                    value: float = float(data[self.__VALUE_IDX]) * self.__unit_factor
                return TimeValue(time, value)
            except ValueError:
                self.__log.error('No value found for ' + line)

    def __get_sampling_time_generator(self):
        return self.__get_time_generator(self.__sampling_time)

    def __get_timestep_time_generator(self):
        return self.__get_time_generator(self.__timestep)

    def __get_time_generator(self, time_increment: float):
        time: float = self.__start
        while True:
            yield time
            time += time_increment

    def __format_time(self, time: str) -> float:
        if self.__date_format is None:
            self.__date_format = self.__find_date_format_for(time)
            self.__log.info('Found format: ' + str(self.__date_format))
        if self.__date_format == self.__EPOCH_FORMAT:
            return float(time)
        else:
            return self.__extract_timestamp_from(time, self.__date_format)

    def __find_date_format_for(self, time: str) -> str:
        for date_format in self.__DATE_FORMATS:
            try:
                if date_format == self.__EPOCH_FORMAT:
                    float(time)
                    return date_format
                else:
                    self.__extract_timestamp_from(time, date_format)
                    return date_format
            except ValueError:
                pass
        raise Exception('Unknown date format for ' + time)

    def __extract_timestamp_from(self, time: str, date_format: str) -> float:
        date: datetime = datetime.strptime(time, date_format)
        date = self.__get_local_datetime_from(date=date)
        return date.timestamp()

    def __get_local_datetime_from(self, date: datetime = None, tstmp: float = None) -> datetime:
        if date is None:
            if tstmp is None:
                tstmp = datetime.now()
            date = datetime.fromtimestamp(tstmp)
        return self.__timezone.localize(date, is_dst=None)

    def __get_initial_data(self) -> [TimeValue]:
        timestamp: float = self.__start
        data = self.__get_next_data()
        self.__set_time_offset(data.time, timestamp)
        data.time += self.__time_offset
        while data.time < timestamp:
            data = self.__get_next_data()
        return [data]

    def __set_time_offset(self, file_tstmp: float, simulation_tstmp: float) -> None:
        #Set profile year to simulation year
        file_date = self.__get_local_datetime_from(tstmp=file_tstmp)
        simulation_date = self.__get_local_datetime_from(tstmp=simulation_tstmp)
        if file_date.year == simulation_date.year:
            return
        adapted_file_tstmp = file_date.replace(year=simulation_date.year).timestamp()
        self.__time_offset = adapted_file_tstmp - file_tstmp
        if not self.__time_offset == 0:
            self.__log.warn('Time offset is ' + str(self.__time_offset) + ' s. \n'
                            'File time: ' + str(self.__get_local_datetime_from(tstmp=file_tstmp)) + ', \n'
                            'Simulation time: ' + str(self.__get_local_datetime_from(tstmp=simulation_tstmp)))

    def __get_timezone_from(self, header: dict) -> timezone:
        try:
            tz: str = header[self.Header.TIMEZONE]
            return timezone(tz)
        except KeyError:
            self.__log.warn('No timezone for profile is given, UTC timzone is assumed.')
            return self.__UTC

    def __get_latitude_from(self, header: dict) -> float:
        try:
            lat: str = header[self.Header.LATITUDE]
            return float(lat)
        except KeyError:
            return self.__ERROR

    def __get_longitude_from(self, header: dict) -> float:
        try:
            lon: str = header[self.Header.LONGITUDE]
            return float(lon)
        except KeyError:
            return self.__ERROR

    @classmethod
    def __get_unit_from(cls, header: dict, identifier: str) -> float:
        try:
            unit: str = header[identifier]
            return cls.__UNITS[unit]
        except KeyError:
            # print('Unit is unknown. Valid types are ' + str(cls.__UNITS.keys()) + '.')
            return 1.0

    @classmethod
    def __get_value_unit_from(cls, header: dict) -> float:
        return cls.__get_unit_from(header, cls.Header.UNIT)

    @classmethod
    def __get_time_unit_from(cls, header: dict) -> float:
        return cls.__get_unit_from(header, cls.Header.TIME)

    def __get_sampling_time_from(self, header: dict) -> float:
        try:
            sampling_time: str = header[self.Header.SAMPLING]
            return float(sampling_time)
        except KeyError:
            return self.__timestep

    def profile_data_to_list(self, sign_factor=1) -> ([float], [float]):
        """
        Extracts the whole time series as a list and resets the pointer of the (internal) file afterwards

        Parameters
        ----------
        sign_factor :

        Returns
        -------
        list:
            profile values as a list

        """
        profile_data: [float] = list()
        time_data: [float] = list()
        time_generator = self.__get_timestep_time_generator()
        time: float = next(time_generator)
        while time <= self.__end:
            time_data.append(time)
            profile_data.append(self.next(time) * sign_factor)
            time = next(time_generator)
        self.initialize_file()
        return time_data, profile_data

    def get_latitude(self) -> float:
        """

        Returns
        -------
        float:
            Latitude value of profile location, raises an exception if not available
        """
        if self.__latitude is self.__ERROR:
            raise Exception('No latitude value given in profile. Please add the header for latitude.')
        return self.__latitude

    def get_longitude(self) -> float:
        """

        Returns
        -------
        float:
            Longitude value of profile location, raises an exception if not available
        """
        if self.__longitude is self.__ERROR:
            raise Exception('No longitude value given in profile. Please add the header for longitude.')
        return self.__longitude

    def get_timezone(self) -> timezone:
        """

        Returns
        -------
        float:
            timezone for profile location, raises an exception if not available
        """
        if self.__timezone is self.__ERROR:
            raise Exception('No timezone value given in profile. Please add the header for timezone.')
        return self.__timezone

    @classmethod
    def get_header_from(cls, filename: str) -> dict:
        """
        Extracts header from given file

        Attention: Only searches in the first ten lines for a header!

        Parameters
        ----------
        filename :

        Returns
        -------
        dict:
            header with key/value pairs

        """
        header: dict = dict()
        file = cls.open_file(filename)
        # with open(filename, 'r', newline='') as file:
        line = file.readline()
        line_count: int = 0
        while True:
            if '#' in line:
                try:
                    key_raw, entry_raw = line.split(sep=':', maxsplit=1)
                    key = key_raw.strip('# ')
                    entry = entry_raw.strip()
                    header[key] = entry
                except ValueError:
                    sys.stderr.write('WARNING: Could not interpret ' + line)
                    sys.stderr.flush()
            line = file.readline()
            if line_count > 20:
                break
            else:
                line_count += 1
        file.close()
        return header

    @classmethod
    def get_max_value_of(cls, filename: str, delimiter: str = ',', value_idx: int = 1, start: float = 0.0) -> float:
        """
        Searching for maximum value within given file

        Attention: This method could be very time consuming depending on the profile to read.

        Parameters
        ----------
        filename :
            filename of profile which is read in
        delimiter :
            delimiter for values in one line, default: ','
        value_idx :
            column number of line for value which should be considered, default: 1
        start :
            start value for search

        Returns
        -------
        float:
            maximum value of given profile
        """
        # ts_before = time.time()
        header: dict = cls.get_header_from(filename)
        unit_factor: float = cls.__get_value_unit_from(header)
        file = cls.open_file(filename)
        max_value: float = start
        for line in file:
            if line.startswith('#') or line.startswith('"') or line in ['\r\n', '\n']:
                continue
            data: [str] = line.split(delimiter)
            if len(data) < 2:
                value: float = float(data[0])
            else:
                value: float = float(data[value_idx])
            max_value = max(value * unit_factor, max_value)
        file.close()
        # print(max_value, unit_factor)
        # print(str(time.time() - ts_before))
        return max_value

    def close(self):
        self.__log.close()
        self.__file.close()
