'''
Helper module
*************

Misc. helper functions for other classes.
'''

import copy
import types
import time
import logging
import datetime
import pickle
import hashlib
import uuid


UNIX_EPOCH = datetime.datetime(month=1, year=1970, day=1)


def list_to_dict(some_list, list_of_keys):
    if len(some_list) != len(list_of_keys):
        raise Exception('Length of list elements and key list must be equal.')
    out = {
        list_of_keys[index]: item for index, item in enumerate(some_list)}
    return out


def timestamp_to_redshift(timestamp):
    if isinstance(timestamp, (str,)):
        return timestamp
    return timestamp.strftime('%b %d,%Y  %H:%M:%S')


def milliseconds_epoch_to_datetime(milliseconds_epoch):
    if isinstance(milliseconds_epoch, (datetime.datetime, str,)):
        return milliseconds_epoch
    logging.info('milliseconds_epoch_to_datetime: ' + str(milliseconds_epoch))
    out = UNIX_EPOCH + datetime.timedelta(seconds=(
        int(milliseconds_epoch)/1000))
    logging.info('milliseconds_epoch_to_datetime output: ' + str(out))
    return out


def seconds_epoch_to_datetime(seconds_epoch):
    return UNIX_EPOCH + datetime.timedelta(seconds=(seconds))


def to_bool(thing):
    if isinstance(thing, (str,)):
        return len(thing) > 0 and thing[0].lower() in ['t', 'y']
    elif isinstance(thing, (int, float,)):
        return thing > 0
    elif isinstance(thing, (bool,)):
        return thing
    else:
        raise Exception(
            'Do not know how to convert {thing} to bool'.format(
                thing=str(thing)))


def get_value(
    dictionary, path, delimiter='.',
        use_default_value=False, default_value=None):

    # dictionary = copy.deepcopy(dictionary)
    if isinstance(path, (str,)):
        path = path.split(delimiter)
    elif isinstance(path, (list, tuple,)):
        pass
    else:
        raise Exception('what?')
    if len(path) == 0:
        dictionary = dictionary
    else:
        for step in path:
            if dictionary is None or isinstance(dictionary, (dict,)):
                dictionary = (dictionary or {}).get(step, default_value)
            else:
                dictionary = dictionary[step.index]
    return dictionary


def hi(*args, **kwargs):
    return 'hi'


def pdb(*args, **kwargs):
    import pdb; pdb.set_trace()


def convert_date_format(date_string, source_format=None, target_format=None):
    return datetime.datetime.strptime(
        date_string, source_format).strftime(target_format)


def engaging_networks_date(result, **kwargs):
    '''
    This is a very redundant function, but it's here mostly to test the
    functionality of the ``post_process_function``.
    '''
    out = datetime.datetime.strptime(result[0]['date_modified'], '%Y-%m-%d')
    out = out.strftime('%Y%m%d')
    return out


def set_value(dictionary, path, value):
    for step in path[:-1]:
        if not isinstance(step, (ListIndex,)):
            if step not in dictionary:
                raise Exception('Path not found.')
            dictionary = dictionary[step]
        else:
            dictionary = dictionary[step.index]
    dictionary[
        path[-1] if not isinstance(path[-1], (ListIndex,))
        else path[-1].index] = value


def iterate_leaves(dictionary, keypath=None):
    keypath = keypath or []
    for key, value in dictionary.items():
        if not isinstance(value, (dict,)):
            yield keypath + [key], value
        else:
            for i in iterate_leaves(value, keypath=keypath + [key]):
                yield i


def remap_dictionary(
    source_dictionary, target_dictionary,
        use_default_value=False, default_value=None):
    target_dictionary = copy.deepcopy(target_dictionary)
    for path, value in iterate_leaves(target_dictionary):
        set_value(
            target_dictionary,
            path,
            get_value(
                source_dictionary,
                value,
                use_default_value=use_default_value,
                default_value=default_value))
    return target_dictionary


def now_milliseconds():
    return str(int(time.time() * 1000))


def two_weeks_ago():
    return str(int(time.time() * 1000 - (30 * (24 * 60 * 60 * 1000))))


def now_redshift():
    return datetime.datetime.now().strftime('%b %d,%Y  %H:%M:%S')


def now_datetime():
    return datetime.datetime.now()


def two_weeks_ago_datetime():
    return datetime.datetime.now() - datetime.timedelta(days=14)


def datetime_to_redshift(datetime_obj):
    return datetime_obj.strftime('%b %d,%Y  %H:%M:%S')


def datetime_to_milliseconds(datetime_obj):
    return int((datetime_obj - UNIX_EPOCH).total_seconds() * 1000)


class SafeMap(dict):
    def __missing__(self, key):
        return '{' + str(key) + '}'


class ListIndex:
    def __init__(self, index):
        self.index = index

    def __repr__(self):
        return 'ListIndex({index})'.format(index=str(self.index))

    def __eq__(self, other):
        return isinstance(other, (ListIndex,)) and self.index == other.index

    def __hash__(self):
        return self.index


def all_paths(thing, path=None, starting_path=None):
    path = path or tuple()
    starting_path = starting_path or []
    thing = get_value(thing, starting_path)

    if isinstance(thing, (dict,)):
        for key, value in thing.items():
            yield path + (key,)
            for i in all_paths(value, path=path + (key,)):
                yield i

    elif isinstance(thing, (list,)):
        for index, item in enumerate(thing):
            yield path + (ListIndex(index),)
            for i in all_paths(item, path=path + (ListIndex(index),)):
                yield i
    else:
        yield path


def last_element_list_index(path):
    return isinstance(path[-1], (ListIndex,))


def remove_list_indexes(path):
    return tuple(step for step in path if not isinstance(step, (ListIndex,)))


def matching_tail_paths(target_path, structure, starting_path=None):
    target_path = tuple(target_path)
    if starting_path is not None:
        starting_path = tuple(starting_path)
    seen = set()
    for path in all_paths(structure, starting_path=starting_path):
        if path in seen:
            continue
        else:
            seen.add(path)
        if last_element_list_index(path):
            continue

        temp_list = remove_list_indexes(path)

        if len(temp_list) < len(target_path):
            continue
        tail_of_path = temp_list[-1 * len(target_path):]
        if lists_equal(tail_of_path, target_path):
            logging.info('tail path: ' + str(target_path) + ' ' + str(path))
            yield (starting_path or tuple([])) + path


def lists_equal(l1, l2):
    return l1 == l2


def nevermind(thing):
    return thing


def replace_by_path(
    dictionary, target_path, target_value=None, function=None,
        function_args=None, function_kwargs=None, starting_path=None):

    function = function or nevermind
    function_args = function_args or tuple([])
    function_kwargs = function_kwargs or {}
    #dictionary_clone = copy.deepcopy(dictionary)
    dictionary_clone = dictionary
    for path in matching_tail_paths(
            target_path, dictionary_clone, starting_path=starting_path):
        current_value = get_value(dictionary, path)
        one_target_value = target_value or function(
            current_value, *function_args, **function_kwargs)
        set_value(dictionary, path, one_target_value)


def aggregate_values(dictionary, target_path, values=False):
    aggregated_values = []
    for path in matching_tail_paths(target_path, dictionary):
        current_value = get_value(dictionary, path)
        aggregated_values.append(
            list(current_value.values()) if values else current_value)
    logging.debug('aggregated_values: ' + str(aggregated_values))
    return aggregated_values if not values else aggregated_values[0]


class UberDict(dict):

    def rget(self, value):
        for i in meets_condition(self, lambda x: isinstance(x, (dict,))):
            for _key, _value in i.items():
                if _value == value:
                    yield _key


def iterate(thing, path=None, seen=None, start_path=None):
    '''
    TODO: allow specification of `start_path` to speed this up.
    '''
    hashed_thing = hash(str(thing))
    path = path or []
    start_path = start_path or []
    seen = seen or set([])
    if hashed_thing not in seen:
        if isinstance(thing, (list, tuple, dict,)):
            yield thing
        seen.add(hashed_thing)
        if isinstance(thing, (dict,)):
            for key, value in thing.items():
                for item in iterate(value, path=path + [key], seen=seen):
                    yield item
        elif isinstance(thing, (list, tuple,)):
            for index, item in enumerate(thing):
                for thingie in iterate(item, path=path + [ListIndex(index)], seen=seen):
                    yield thingie
        else:
            yield thing


def meets_condition(thing, func):
    for item in iterate(thing):
        if func(item):
            yield item




if __name__ == '__main__':
    import json
    d = json.load(open('./sample.json', 'r'))
    counter = 0
    while counter < 100:
        replace_by_path(d, ['vid'], target_value='hithere')
        counter += 1
        print(counter)
    import random
    logging.basicConfig(level=logging.INFO)
    replace_by_path(d, ['vid'], function=lambda x: str(x) + str(random.random()))
