# -*- coding: utf-8 -*-

##
# Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
# This file is part of openmano
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact with: nfvlabs@tid.es
##

'''
utils is a module that implements functions that are used by all openmano modules,
dealing with aspects such as reading/writing files, formatting inputs/outputs for quick translation
from dictionaries to appropriate database dictionaries, etc.
'''
__author__="Alfonso Tierno, Gerardo Garcia"
__date__ ="$08-sep-2014 12:21:22$"

import datetime
import time
import warnings
from functools import reduce, partial, wraps
from itertools import tee

from itertools import filterfalse

from jsonschema import exceptions as js_e
from jsonschema import validate as js_v

from inspect import getfullargspec as getspec

#from bs4 import BeautifulSoup

def read_file(file_to_read):
    """Reads a file specified by 'file_to_read' and returns (True,<its content as a string>) in case of success or (False, <error message>) in case of failure"""
    try:
        f = open(file_to_read, 'r')
        read_data = f.read()
        f.close()
    except Exception as e:
        return (False, str(e))

    return (True, read_data)

def write_file(file_to_write, text):
    """Write a file specified by 'file_to_write' and returns (True,NOne) in case of success or (False, <error message>) in case of failure"""
    try:
        f = open(file_to_write, 'w')
        f.write(text)
        f.close()
    except Exception as e:
        return (False, str(e))

    return (True, None)

def format_in(http_response, schema):
    try:
        client_data = http_response.json()
        js_v(client_data, schema)
        #print "Input data: ", str(client_data)
        return True, client_data
    except js_e.ValidationError as exc:
        print("validate_in error, jsonschema exception ", exc.message, "at", exc.path)
        return False, ("validate_in error, jsonschema exception ", exc.message, "at", exc.path)

def remove_extra_items(data, schema):
    deleted = []
    if isinstance(data, (tuple, list)):
        for d in data:
            a = remove_extra_items(d, schema['items'])
            if a:
                deleted.append(a)
    elif isinstance(data, dict):
        # TODO deal with patternProperties
        if 'properties' not in schema:
            return None
        to_delete = []
        for k in data.keys():
            if k in schema['properties']:
                a = remove_extra_items(data[k], schema['properties'][k])
                if a:
                    deleted.append({k: a})
            elif not schema.get('additionalProperties'):
                to_delete.append(k)
                deleted.append(k)
        for k in to_delete:
            del data[k]
    if len(deleted) == 0:
        return None
    elif len(deleted) == 1:
        return deleted[0]

    return deleted

#def format_html2text(http_content):
#    soup=BeautifulSoup(http_content)
#    text = soup.p.get_text() + " " + soup.pre.get_text()
#    return text


def delete_nulls(var):
    if isinstance(var, dict):
        to_delete = []
        for k in var.keys():
            if var[k] is None:
                to_delete.append([k])
            elif isinstance(var[k], (dict, list, tuple)):
                if delete_nulls(var[k]):
                    to_delete.append(k)
        for k in to_delete:
            del var[k]
        if len(var) == 0:
            return True
    elif isinstance(var, (list, tuple)):
        for k in var:
            if isinstance(k, dict):
                delete_nulls(k)
        if len(var) == 0:
            return True
    return False


def convert_bandwidth(data, reverse=False):
    '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
    It assumes that bandwidth is well formed
    Attributes:
        'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid
        'reverse': by default convert form str to int (Mbps), if True it convert from number to units
    Return:
        None
    '''
    if isinstance(data, dict):
        for k in data.keys():
            if isinstance(data[k], (dict, tuple, list)):
                convert_bandwidth(data[k], reverse)
        if "bandwidth" in data:
            try:
                value=str(data["bandwidth"])
                if not reverse:
                    pos = value.find("bps")
                    if pos>0:
                        if value[pos-1]=="G":
                            data["bandwidth"] = int(data["bandwidth"][:pos-1]) * 1000
                        elif value[pos-1]=="k":
                            data["bandwidth"]= int(data["bandwidth"][:pos-1]) // 1000
                        else:
                            data["bandwidth"]= int(data["bandwidth"][:pos])
                else:
                    value = int(data["bandwidth"])
                    if value % 1000 == 0 and value > 1000:
                        data["bandwidth"] = str(value // 1000) + " Gbps"
                    else:
                        data["bandwidth"] = str(value) + " Mbps"
            except:
                print("convert_bandwidth exception for type", type(data["bandwidth"]), " data", data["bandwidth"])
                return
    if isinstance(data, (tuple, list)):
        for k in data:
            if isinstance(k, (dict, tuple, list)):
                convert_bandwidth(k, reverse)

def convert_float_timestamp2str(var):
    '''Converts timestamps (created_at, modified_at fields) represented as float
    to a string with the format '%Y-%m-%dT%H:%i:%s'
    It enters recursively in the dict var finding this kind of variables
    '''
    if type(var) is dict:
        for k,v in var.items():
            if type(v) is float and k in ("created_at", "modified_at"):
                var[k] = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(v) )
            elif type(v) is dict or type(v) is list or type(v) is tuple:
                convert_float_timestamp2str(v)
        if len(var) == 0: return True
    elif type(var) is list or type(var) is tuple:
        for v in var:
            convert_float_timestamp2str(v)

def convert_datetime2str(var):
    '''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s'
    It enters recursively in the dict var finding this kind of variables
    '''
    if type(var) is dict:
        for k,v in var.items():
            if type(v) is datetime.datetime:
                var[k]= v.strftime('%Y-%m-%dT%H:%M:%S')
            elif type(v) is dict or type(v) is list or type(v) is tuple:
                convert_datetime2str(v)
        if len(var) == 0: return True
    elif type(var) is list or type(var) is tuple:
        for v in var:
            convert_datetime2str(v)

def convert_str2boolean(data, items):
    '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
    Done recursively
    Attributes:
        'data': dictionary variable to be checked. None or empty is considered valid
        'items': tuple of keys to convert
    Return:
        None
    '''
    if type(data) is dict:
        for k in data.keys():
            if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list:
                convert_str2boolean(data[k], items)
            if k in items:
                if type(data[k]) is str:
                    if   data[k]=="false" or data[k]=="False": data[k]=False
                    elif data[k]=="true"  or data[k]=="True":  data[k]=True
    if type(data) is tuple or type(data) is list:
        for k in data:
            if type(k) is dict or type(k) is tuple or type(k) is list:
                convert_str2boolean(k, items)

def check_valid_uuid(uuid):
    id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
    id_schema2 = {"type" : "string", "pattern": "^[a-fA-F0-9]{32}$"}
    try:
        js_v(uuid, id_schema)
        return True
    except js_e.ValidationError:
        try:
            js_v(uuid, id_schema2)
            return True
        except js_e.ValidationError:
            return False
    return False


def expand_brackets(text):
    """
    Change a text with TEXT[ABC..] into a list with [TEXTA, TEXTB, TEXC, ...
    if no bracket is used it just return the a list with the single text
    It uses recursivity to allow several [] in the text
    :param text:
    :return:
    """
    if text is None:
        return (None, )
    start = text.find("[")
    end = text.find("]")
    if start < 0 or end < 0:
        return [text]
    text_list = []
    for char in text[start+1:end]:
        text_list += expand_brackets(text[:start] + char + text[end+1:])
    return text_list

def deprecated(message):
  def deprecated_decorator(func):
      def deprecated_func(*args, **kwargs):
          warnings.warn("{} is a deprecated function. {}".format(func.__name__, message),
                        category=DeprecationWarning,
                        stacklevel=2)
          warnings.simplefilter('default', DeprecationWarning)
          return func(*args, **kwargs)
      return deprecated_func
  return deprecated_decorator


def truncate(text, max_length=1024):
    """Limit huge texts in number of characters"""
    text = str(text)
    if text and len(text) >= max_length:
        return text[:max_length//2-3] + " ... " + text[-max_length//2+3:]
    return text


def merge_dicts(*dicts, **kwargs):
    """Creates a new dict merging N others and keyword arguments.
    Right-most dicts take precedence.
    Keyword args take precedence.
    """
    return reduce(
        lambda acc, x: acc.update(x) or acc,
        list(dicts) + [kwargs], {})


def remove_none_items(adict):
    """Return a similar dict without keys associated to None values"""
    return {k: v for k, v in adict.items() if v is not None}


def filter_dict_keys(adict, allow):
    """Return a similar dict, but just containing the explicitly allowed keys

    Arguments:
        adict (dict): Simple python dict data struct
        allow (list): Explicits allowed keys
    """
    return {k: v for k, v in adict.items() if k in allow}


def filter_out_dict_keys(adict, deny):
    """Return a similar dict, but not containing the explicitly denied keys

    Arguments:
        adict (dict): Simple python dict data struct
        deny (list): Explicits denied keys
    """
    return {k: v for k, v in adict.items() if k not in deny}


def expand_joined_fields(record):
    """Given a db query result, explode the fields that contains `.` (join
    operations).

    Example
        >> expand_joined_fiels({'wim.id': 2})
        # {'wim': {'id': 2}}
    """
    result = {}
    for field, value in record.items():
        keys = field.split('.')
        target = result
        target = reduce(lambda target, key: target.setdefault(key, {}),
                        keys[:-1], result)
        target[keys[-1]] = value

    return result


def ensure(condition, exception):
    """Raise an exception if condition is not met"""
    if not condition:
        raise exception


def partition(predicate, iterable):
    """Create two derived iterators from a single one
    The first iterator created will loop thought the values where the function
    predicate is True, the second one will iterate over the values where it is
    false.
    """
    iterable1, iterable2 = tee(iterable)
    return filter(predicate, iterable2), filterfalse(predicate, iterable1)


def pipe(*functions):
    """Compose functions of one argument in the opposite order,
    So pipe(f, g)(x) = g(f(x))
    """
    return lambda x: reduce(lambda acc, f: f(acc), functions, x)


def compose(*functions):
    """Compose functions of one argument,
    So compose(f, g)(x) = f(g(x))
    """
    return lambda x: reduce(lambda acc, f: f(acc), functions[::-1], x)


def safe_get(target, key_path, default=None):
    """Given a path of keys (eg.: "key1.key2.key3"), return a nested value in
    a nested dict if present, or the default value
    """
    keys = key_path.split('.')
    target = reduce(lambda acc, key: acc.get(key) or {}, keys[:-1], target)
    return target.get(keys[-1], default)


class Attempt(object):
    """Auxiliary class to be used in an attempt to retry executing a failing
    procedure

    Attributes:
        count (int): 0-based "retries" counter
        max_attempts (int): maximum number of "retries" allowed
        info (dict): extra information about the specific attempt
            (can be used to produce more meaningful error messages)
    """
    __slots__ = ('count', 'max', 'info')

    MAX = 3

    def __init__(self, count=0, max_attempts=MAX, info=None):
        self.count = count
        self.max = max_attempts
        self.info = info or {}

    @property
    def countdown(self):
        """Like count, but in the opposite direction"""
        return self.max - self.count

    @property
    def number(self):
        """1-based counter"""
        return self.count + 1


def inject_args(fn=None, **args):
    """Partially apply keyword arguments in a function, but only if the function
    define them in the first place
    """
    if fn is None:  # Allows calling the decorator directly or with parameters
        return partial(inject_args, **args)

    spec = getspec(fn)
    return wraps(fn)(partial(fn, **filter_dict_keys(args, spec.args)))


def get_arg(name, fn, args, kwargs):
    """Find the value of an argument for a function, given its argument list.

    This function can be used to display more meaningful errors for debugging
    """
    if name in kwargs:
        return kwargs[name]

    spec = getspec(fn)
    if name in spec.args:
        i = spec.args.index(name)
        return args[i] if i < len(args) else None

    return None
