import logging
import datetime
import traceback
import pandas as pd
import numpy as np
import tango

import ArchiveExtractor as ae
import ArchiveExtractor.Amenities as aea
import ArchiveExtractor.Core as aec

##########################################################################
###                 Install logger for the module                      ###
##########################################################################
logger = logging.getLogger("ArchiveExtractor")

if not logger.hasHandlers():
    # No handlers, create one
    sh = logging.StreamHandler()
    sh.setLevel(logger.level)
    sh.setFormatter(logging.Formatter("{name}-{levelname:8}: {message}", style='{'))
    logger.addHandler(sh)

##########################################################################
###                Module initialisation functions                     ###
##########################################################################

def init(
        HdbExtractorPath="archiving/hdbextractor/2",
        TdbExtractorPath="archiving/tdbextractor/2",
        loglevel="info",
            ):
    """
    Initialize the module.
    Instanciate tango.DeviceProxy for extractors (TDB and HDB)

    Parameters:
    -----------
    HdbExtractorPath, TdbExtractorPath: string
        Tango path to the extractors.

    loglevel: string
        loglevel to pass to logging.Logger
    """

    ae._Extractors = (None, None)
    ae._AttrTables = (None, None)
    _Extr = []
    _Attr = []

    try:
        logger.setLevel(getattr(logging, loglevel.upper()))
    except AttributeError:
        logger.error("Wrong log level specified: {}".format(loglevel.upper()))
        logger.error("Init module failed. You can try to run ArchiveExtractor.init() again with another configuration")
        return

    logger.debug("Instanciating extractors device proxy...")

    for p in [HdbExtractorPath, TdbExtractorPath]:
        try:
            _Extr.append(tango.DeviceProxy(p))
        except tango.DevFailed:
            logger.error("Could not find extractor {}".format(p))
            logger.error("Init module failed. You can try to run ArchiveExtractor.init() again with another configuration")
            return

    logger.debug("{} and {} instanciated.".format(*_Extr))

    logger.debug("Configuring extractors device proxy...")
    for e in _Extr:
        # set timeout to 3 sec
        e.set_timeout_millis(3000)

    logger.debug("Filling attributes lookup tables (use GetAttNameAll)...")
    for e in _Extr:
        try:
            _Attr.append(e.getattnameall())
        except (tango.DevFailed, AttributeError):
            logger.error("Could not fetch attributes for extractor {}".format(e))
            logger.error("Init module failed. You can try to run ArchiveExtractor.init() again with another configuration")
            return

    logger.debug("HDB: {} TDB: {} attributes counted".format(len(_Attr[0]), len(_Attr[1])))

    ae._Extractors = tuple(_Extr)
    ae._AttrTables = tuple(_Attr)

##########################################################################
###                    Module access functions                         ###
##########################################################################

def extract(
        attr,
        date1, date2=None,
        method="nearest",
        db='H',
        ):
    """
    Access function to perform extraction between date1 and date2.
    Can extract one or several attributes.
    date1 and date2 can be both exact date, or one of two can be a time interval that will be taken relative to the other.


    Parameters:
    -----------
    attr: string, list, dict
        Attribute(s) to extract.
        If string, extract the given attribute, returning a pandas.Series.
        If list, extract attributes and return a list of pandas.Series.
        If a dict, extract attributes and return a dict of pandas.Series with same keys.

    date1, date2: string, datetime.datetime, datetime.timedelta, None
        Exact date, or duration relative to date2.
        If string, it will be parsed.
        A start date can be given with string format '%Y-%m-%d-%H:%M:%S' or less precise (ie '2021-02', '2022-11-03' '2022-05-10-21:00'.i..).
        A duration can be given with string format 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
        A datetime.datetime object or datetime.timedelta object will be used as is.
        date2 can be None. In that case it is replaced by the current time.

    method: str
        Method of extraction
            'nearest': Retrieve nearest value of date1, date2 is ignored.
            'between': Retrive data between date1 and date2.

    db: str
        Which database to look in, 'H' or 'T'.

    """

    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
    #    Perform a few sanity checks
    if not aea._check_initialized():
        # Stop here, the function has produced a message if necessary
        return

    if not db in ("H", "T"):
        raise ValueError("Attribute 'db' should be 'H' or 'T'")


    allowedmethods=("nearest", "between", "minmaxmean")
    if not method in allowedmethods:
        raise ValueError("Attribute 'method' should be in {}".format(str(allowedmethods)))

    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
    #     Work with dates
    if not type(date1) in (datetime.datetime, datetime.timedelta):
        date1 = aea._dateparse(date1)
    if date2 is None:
        date2 = datetime.datetime.now()
    else:
        if not type(date2) in (datetime.datetime, datetime.timedelta):
            date2 = aea._dateparse(date2)

    if not datetime.datetime in (type(date1), type(date2)):
        logger.error("One of date1 date2 should be an exact date.\nGot {} {}".format(date1, date2))
        raise ValueError("date1 and date2 not valid")

    # Use timedelta relative to the other date. date1 is always before date2
    if type(date1) is datetime.timedelta:
        date1 = date2-date1
    if type(date2) is datetime.timedelta:
        date2 = date1+date2

    if  date1 > date2:
        logger.error("date1 must precede date2.\nGot {} {}".format(date1, date2))
        raise ValueError("date1 and date2 not valid")

    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
    #      Perform extraction and return

    if type(attr) is dict:
        d=dict()
        for k,v in attr.items():
            try:
                d.update({k:aec._extract_attribute(v, method, date1, date2, db)})
            except Exception as e:
                logger.debug("Exception in _extract_attribute(): "+str(e))
                logger.debug(traceback.print_tb(e.__traceback__))
                logger.error("Could not extract {}.".format(v))

        return d

    if type(attr) in (list,tuple):
        d=[]
        for v in attr:
            try:
                d.append(aec._extract_attribute(v, method, date1, date2, db))
            except Exception as e:
                logger.debug("Exception in _extract_attribute(): "+str(e))
                logger.debug(traceback.print_tb(e.__traceback__))
                logger.error("Could not extract {}.".format(v))

        return d

    try:
        d=aec._extract_attribute(attr, method, date1, date2, db)
    except Exception as e:
        logger.debug("Exception in _extract_attribute(): "+str(e))
        logger.debug(traceback.print_tb(e.__traceback__))
        logger.error("Could not extract {}.".format(attr))
        return None

    return d


##----------------------------------------------------------------------##
def findattr(pattern, db="H"):
    """
    Search for an attribute path using the pattern given.
    Case insensitive.

    Parameters:
    -----------
    pattern: str
        Pattern to search, wildchar * accepted.
        example "dg*dcct*current"

    db: str
        Which database to look in, 'H' or 'T'.

    Returns:
    --------
    results: (str,)
        List of string match
    """
    if not aea._check_initialized():
        return

    if not db in ("H", "T"):
        raise AttributeError("Attribute db should be 'H' or 'T'")


    keywords=pattern.lower().split('*')

    # Select DB
    attr_table = ae._AttrTables[{'H':0, 'T':1}[db]]

    matches = [attr for attr in attr_table if all(k in attr.lower() for k in keywords)]

    return matches

##----------------------------------------------------------------------##
def infoattr(attribute, db='H'):
    """
    Get informations for an attribute and pack it into a python dict.

    Parameters
    ----------
    attribute : String
        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".

    db: str
        Which database to look in, 'H' or 'T'.

    Returns
    -------
    info : dict
        Dictionnary of propertyname:propertyvalue
    """
    if not aea._check_initialized():
        return

    if not db in ("H", "T"):
        raise AttributeError("Attribute db should be 'H' or 'T'")

    info = dict()

    for func in ("GetAttDefinitionData", "GetAttPropertiesData"):
        R=getattr(ae._Extractors[{'H':0, 'T':1}[db]], func)(attribute)
        if not R is None:
            for i in R:
                _s=i.split("::")
                info[_s[0]]=_s[1]
        else:
            logger.warning("Function %s on extractor returned None"%func)

    return info