import datetime as dt
import pandas as pd
from collections import namedtuple

from .._version import get_versions

__all__ = ['ensure_is_date_object', 'ensure_is_datetime_object', 'identity', 'make_timestamp_formatter',
           'print_generated_sequence', 'parse_date_string', 'parse_datetime_string', 'print_tohu_version']


def print_tohu_version():
    """
    Convenience helper function to print the current tohu version.
    """
    print(f"Tohu version: {get_versions()['version']}")


def identity(x):
    """
    Helper function which returns its argument unchanged.
    That is, `identity(x)` returns `x` for any input `x`.
    """
    return x


def is_clone(g):
    """
    Return True if the generator `g` is a clone of another generator, otherwise return False.
    """
    return g.parent is not None


def make_timestamp_formatter(fmt, uppercase=None):
    if fmt is None:
        formatter = identity
    else:
        if not isinstance(fmt, str):
            raise ValueError(f"Argument 'fmt' must be of type string, got '{type(fmt)}'")

        if uppercase:
            formatter = lambda ts: ts.strftime(fmt).upper()
        else:
            formatter = lambda ts: ts.strftime(fmt)

    return formatter


def make_exploded_column(df, colname_new, colname_old):
    """
    Internal helper function used by `explode_columns()`.
    """
    s = df[colname_old].apply(pd.Series).stack()
    s.name = colname_new
    return s


def explode_columns(df, colnames):
    """
    Given a dataframe with certain columns that contain lists,
    return another dataframe where the elements in each list
    are "exploded" into individual rows.

    Example:

    >>> df
       col1 col2             col3          col4
    0   foo   11  [DDD, AAA, CCC]  [dd, aa, cc]
    1   bar   22            [FFF]          [ff]
    2  quux   33       [EEE, BBB]      [ee, bb]

    >>> explode_columns(df, ['col3'])
       col1 col2 col3 col4
    0   foo   11  DDD   dd
    1   foo   11  AAA   aa
    2   foo   11  CCC   cc
    3   bar   22  FFF   ff
    4  quux   33  EEE   ee
    5  quux   33  BBB   bb

    >>> explode_columns(df, {'col3_exploded': 'col3'})
       col1 col2 col3_exploded col4
    0   foo   11           DDD   dd
    1   foo   11           AAA   aa
    2   foo   11           CCC   cc
    3   bar   22           FFF   ff
    4  quux   33           EEE   ee
    5  quux   33           BBB   bb
    """
    if isinstance(colnames, (list, tuple)):
        colnames = {name: name for name in colnames}

    remaining_columns = list(df.columns.difference(colnames.values()))
    df2 = df.set_index(remaining_columns)
    df3 = pd.concat((make_exploded_column(df2, col_new, col_old) for col_new, col_old in colnames.items()), axis=1)
    levels_to_reset = list(range(len(remaining_columns)))
    df4 = df3.reset_index(level=levels_to_reset).reset_index(drop=True)
    return df4


def print_generated_sequence(gen, num, *, sep=", ", fmt='', seed=None):
    """
    Helper function which prints a sequence of `num` items
    produced by the random generator `gen`.
    """
    if seed:
       gen.reset(seed)

    elems = [format(next(gen), fmt) for _ in range(num)]
    sep_initial = "\n\n" if '\n' in sep else " "
    print("Generated sequence:{}{}".format(sep_initial, sep.join(elems)))


def make_dummy_tuples(chars='abcde'):
    """
    Helper function to create a list of namedtuples which are useful
    for testing and debugging (especially of custom generators).

    Example
    -------
    >>> make_dummy_tuples(chars='abcd')
    [Quux(x='AA', y='aa'),
     Quux(x='BB', y='bb'),
     Quux(x='CC', y='cc'),
     Quux(x='DD', y='dd')]
    """
    Quux = namedtuple('Quux', ['x', 'y'])
    some_tuples = [Quux((c*2).upper(), c*2) for c in chars]
    return some_tuples


class TohuDateError(Exception):
    """
    Custom exception
    """


def parse_date_string(s):
    try:
        date = dt.datetime.strptime(s, "%Y-%m-%d").date()
    except ValueError:
        raise TohuDateError(
            "If input is a string, it must represent a timestamp of the form 'YYYY-MM-DD HH:MM:SS' "
            f"or a date of the form YYYY-MM-DD. Got: '{s}'"
        )
    return date


def ensure_is_date_object(x):
    """
    Ensure input represents a valid date and return the corresponding `datetime.date` object.

    Valid inputs:

      - string of the form "YYYY-MM-DD"
      - dt.date object
      - pd.Timestamp of the form "YYYY-MM-DD 00:00:00" with freq='D' (as is generated by pd.date_range())
    """
    error_msg = f"Cannot convert input to date object: {x} (type: {type(x)})"

    if isinstance(x, dt.date):
        if isinstance(x, pd.Timestamp):
            if x.freq != 'D':
                raise TohuDateError("Pandas Timestamp must have freq='D' set. Got: freq={x.freq!r}")
            elif pd.Timestamp(x.date()) == x:
                return x.date()
            else:
                raise TohuDateError(error_msg)
        elif isinstance(x, dt.datetime):
            raise TohuDateError(error_msg)
        else:
            return x
    elif isinstance(x, str):
        return parse_date_string(x)
    else:
        raise TohuDateError(error_msg)


class TohuTimestampError(Exception):
    """
    Custom exception
    """


def is_date_object(x):
    return isinstance(x, dt.date) and not isinstance(x, dt.datetime)


def is_date_string(x):
    if not isinstance(x, str):
        return False
    else:
        try:
            dt.datetime.strptime(x, "%Y-%m-%d")
            return True
        except ValueError:
            return False


def parse_datetime_string(s, optional_offset=None):
    try:
        ts = dt.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
    except ValueError:
        optional_offset = optional_offset or dt.timedelta(seconds=0)
        try:
            ts = dt.datetime.strptime(s, "%Y-%m-%d") + optional_offset
        except ValueError:
            raise ValueError(
                "If input is a string, it must represent a timestamp of the form 'YYYY-MM-DD HH:MM:SS' "
                f"or a date of the form YYYY-MM-DD. Got: '{s}'"
            )
    return ts


def ensure_is_datetime_object(x, optional_offset=None):
    if isinstance(x, dt.datetime):
        return x
    elif isinstance(x, dt.date):
        optional_offset = optional_offset or dt.timedelta(seconds=0)
        return dt.datetime(x.year, x.month, x.day) + optional_offset
    elif isinstance(x, str):
        return parse_datetime_string(x, optional_offset)
    else:
        raise TohuTimestampError(f"Cannot convert input to datetime object: {x} (type: {type(x)})")
