#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2013-2016 DNAnexus, Inc.
#
# This file is part of dx-toolkit (DNAnexus platform client libraries).
#
#   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.


from __future__ import print_function, unicode_literals, division, absolute_import

import re
from collections.abc import Mapping
from .printing import (GREEN, BLUE, YELLOW, WHITE, BOLD, ENDC)

TIME_UNITS = [
    ('miliseconds', 1000),
    ('seconds', 60),
    ('minutes', 60),
    ('hours', 24),
    ('days', 365),
    ('years', None)
]

REPLACEMENT_TABLE = (
    '\\x00',    #  0x00 -> NULL
    '\\x01',    #  0x01 -> START OF HEADING
    '\\x02',    #  0x02 -> START OF TEXT
    '\\x03',    #  0x03 -> END OF TEXT
    '\\x04',    #  0x04 -> END OF TRANSMISSION
    '\\x05',    #  0x05 -> ENQUIRY
    '\\x06',    #  0x06 -> ACKNOWLEDGE
    '\\x07',    #  0x07 -> BELL
    '\\x08',    #  0x08 -> BACKSPACE
    '\\t',      #  0x09 -> HORIZONTAL TABULATION
    '\\n',      #  0x0A -> LINE FEED
    '\\x0b',    #  0x0B -> VERTICAL TABULATION
    '\\x0c',    #  0x0C -> FORM FEED
    '\\r',      #  0x0D -> CARRIAGE RETURN
    '\\x0e',    #  0x0E -> SHIFT OUT
    '\\x0f',    #  0x0F -> SHIFT IN
    '\\x10',    #  0x10 -> DATA LINK ESCAPE
    '\\x11',    #  0x11 -> DEVICE CONTROL ONE
    '\\x12',    #  0x12 -> DEVICE CONTROL TWO
    '\\x13',    #  0x13 -> DEVICE CONTROL THREE
    '\\x14',    #  0x14 -> DEVICE CONTROL FOUR
    '\\x15',    #  0x15 -> NEGATIVE ACKNOWLEDGE
    '\\x16',    #  0x16 -> SYNCHRONOUS IDLE
    '\\x17',    #  0x17 -> END OF TRANSMISSION BLOCK
    '\\x18',    #  0x18 -> CANCEL
    '\\x19',    #  0x19 -> END OF MEDIUM
    '\\x1a',    #  0x1A -> SUBSTITUTE
    '\\x1b',    #  0x1B -> ESCAPE
    '\\x1c',    #  0x1C -> FILE SEPARATOR
    '\\x1d',    #  0x1D -> GROUP SEPARATOR
    '\\x1e',    #  0x1E -> RECORD SEPARATOR
    '\\x1f'     #  0x1F -> UNIT SEPARATOR
    )

def escape_unicode_string(u):
    """
    Escapes the nonprintable chars 0-31 and 127, and backslash;
    preferably with a friendly equivalent such as '\n' if available, but
    otherwise with a Python-style backslashed hex escape.
    """
    def replacer(matchobj):
        if ord(matchobj.group(1)) == 127:
            return "\\x7f"
        if ord(matchobj.group(1)) == 92: # backslash
            return "\\\\"
        return REPLACEMENT_TABLE[ord(matchobj.group(1))]
    return re.sub("([\\000-\\037\\134\\177])", replacer, u)

def format_tree(tree, root=None):
    ''' Tree pretty printer.
    Expects trees to be given as mappings (dictionaries). Keys will be printed; values will be traversed if they are
    mappings. To preserve order, use collections.OrderedDict.

    Example:

        print format_tree(collections.OrderedDict({'foo': 0, 'bar': {'xyz': 0}}))

    '''
    formatted_tree = [root] if root is not None else []
    def _format(tree, prefix='    '):
        nodes = list(tree.keys())
        for i in range(len(nodes)):
            node = nodes[i]
            if i == len(nodes)-1 and len(prefix) > 1:
                my_prefix = prefix[:-4] + '└── '
                my_multiline_prefix = prefix[:-4] + '    '
            else:
                my_prefix = prefix[:-4] + '├── '
                my_multiline_prefix = prefix[:-4] + '│   '
            n = 0
            for line in node.splitlines():
                if n == 0:
                    formatted_tree.append(my_prefix + line)
                else:
                    formatted_tree.append(my_multiline_prefix + line)
                n += 1

            if isinstance(tree[node], Mapping):
                subprefix = prefix
                if i < len(nodes)-1 and len(prefix) > 1 and prefix[-4:] == '    ':
                    subprefix = prefix[:-4] + '│   '
                _format(tree[node], subprefix + '    ')
    _format(tree)
    return '\n'.join(formatted_tree)

def format_table(table, column_names=None, column_specs=None, max_col_width=32,
                 report_dimensions=False):
    ''' Table pretty printer.
    Expects tables to be given as arrays of arrays.

    Example:

        print format_table([[1, "2"], [3, "456"]], column_names=['A', 'B'])

    '''
    if len(table) > 0:
        col_widths = [0] * len(list(table)[0])
    elif column_specs is not None:
        col_widths = [0] * (len(column_specs) + 1)
    elif column_names is not None:
        col_widths = [0] * len(column_names)
    my_column_names = []
    if column_specs is not None:
        column_names = ['Row']
        column_names.extend([col['name'] for col in column_specs])
        column_specs = [{'name': 'Row', 'type': 'float'}] + column_specs
    if column_names is not None:
        for i in range(len(column_names)):
            my_col = str(column_names[i])
            if len(my_col) > max_col_width:
                my_col = my_col[:max_col_width-1] + '…'
            my_column_names.append(my_col)
            col_widths[i] = max(col_widths[i], len(my_col))
    my_table = []
    for row in table:
        my_row = []
        for i in range(len(row)):
            my_item = escape_unicode_string(str(row[i]))
            if len(my_item) > max_col_width:
                my_item = my_item[:max_col_width-1] + '…'
            my_row.append(my_item)
            col_widths[i] = max(col_widths[i], len(my_item))
        my_table.append(my_row)

    def border(i):
        return WHITE() + i + ENDC()

    type_colormap = {'boolean': BLUE(),
                     'integer': YELLOW(),
                     'float': WHITE(),
                     'string': GREEN()}
    for i in 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64':
        type_colormap[i] = type_colormap['integer']
    type_colormap['double'] = type_colormap['float']

    def col_head(i):
        if column_specs is not None:
            return BOLD() + type_colormap[column_specs[i]['type']] + column_names[i] + ENDC()
        else:
            return BOLD() + WHITE() + column_names[i] + ENDC()

    formatted_table = [border('┌') + border('┬').join(border('─')*i for i in col_widths) + border('┐')]
    if len(my_column_names) > 0:
        padded_column_names = [col_head(i) + ' '*(col_widths[i]-len(my_column_names[i])) for i in range(len(my_column_names))]
        formatted_table.append(border('│') + border('│').join(padded_column_names) + border('│'))
        formatted_table.append(border('├') + border('┼').join(border('─')*i for i in col_widths) + border('┤'))

    for row in my_table:
        padded_row = [row[i] + ' '*(col_widths[i]-len(row[i])) for i in range(len(row))]
        formatted_table.append(border('│') + border('│').join(padded_row) + border('│'))
    formatted_table.append(border('└') + border('┴').join(border('─')*i for i in col_widths) + border('┘'))

    if report_dimensions:
        return '\n'.join(formatted_table), len(formatted_table), sum(col_widths) + len(col_widths) + 1
    else:
        return '\n'.join(formatted_table)

def flatten_json_array(json_string, array_name):
    """
    Flattens all arrays with the same name in the JSON string

    :param json_string: JSON string
    :type json_string: str
    :param array_name: Array name to flatten
    :type array_name: str
    """

    result = re.sub('"{}": \\[\r?\n\\s*'.format(array_name), '"{}": ['.format(array_name), json_string, flags=re.MULTILINE)
    flatten_regexp = re.compile('"{}": \\[(.*)(?<=,)\r?\n\\s*'.format(array_name), flags=re.MULTILINE)
    while flatten_regexp.search(result):
        result = flatten_regexp.sub('"{}": [\\1 '.format(array_name), result)
    result = re.sub('"{}": \\[(.*)\r?\n\\s*\\]'.format(array_name), '"{}": [\\1]'.format(array_name), result, flags=re.MULTILINE)
    return result

def format_timedelta(timedelta, in_seconds=False, largest_units=None, auto_singulars=False):
    """
    Formats timedelta (duration) to a human readable form

    :param timedelta: Duration in miliseconds or seconds (see in_seconds)
    :type timedelta: int
    :param in_seconds: Whether the given duration is in seconds
    :type in_seconds: bool
    :param largest_units: Largest units to be displayed. Allowed values are miliseconds, seconds, minutes, hours, days and years
    :type largest_units: str
    :param auto_singulars: Automatically use singular when value of given units is 1
    :type auto_singulars: bool
    """

    units = TIME_UNITS[1:] if in_seconds else TIME_UNITS

    if largest_units is None:
        largest_units = units[-1][0]
    elif largest_units not in map(lambda x: x[0], units):
        raise ValueError('Invalid largest units specified')

    if timedelta == 0:
        return '0 ' + units[0][0]

    out_str = ''

    for name, diviser in units:
        if timedelta == 0:
            break

        if largest_units == name:
            diviser = None

        val = timedelta % diviser if diviser else timedelta
        if val != 0:
            out_str = str(val) + ' ' + (name[:-1] if auto_singulars and val == 1 else name) + ', ' + out_str

        if diviser is None:
            break

        timedelta //= diviser

    return out_str.strip(', ')
