from __future__ import unicode_literals, print_function

import csv
import sys
from collections.abc import Iterable
from typing import List, Dict, Text, Any

from attr import attrs, attrib

import six
from six import binary_type, text_type
from clearml_agent.helper.base import nonstrict_in_place_sort


def print_text(text, newline=True):
    if newline:
        text += '\n'
    data = text.encode(sys.stdout.encoding or 'utf8', errors='replace')
    try:
        sys.stdout.buffer.write(data)
    except AttributeError:
        sys.stdout.write(data)


def decode_binary_lines(binary_lines, encoding='utf-8', replace_cr=False, overwrite_cr=False):
    # decode per line, if we failed decoding skip the line
    lines = []
    for b in binary_lines:
        # noinspection PyBroadException
        try:
            line = b.decode(encoding=encoding, errors='replace')
            if replace_cr:
                line = line.replace('\r', '\n')
            elif overwrite_cr:
                cr_lines = line.split('\r')
                line = cr_lines[-1] if cr_lines[-1] or len(cr_lines) < 2 else cr_lines[-2]
        except Exception:
            line = ''
        lines.append(line + '\n' if not line or line[-1] != '\n' else line)
    return lines


def ensure_text(s, encoding='utf-8', errors='strict'):
    """Coerce *s* to six.text_type.
    For Python 2:
      - `unicode` -> `unicode`
      - `str` -> `unicode`
    For Python 3:
      - `str` -> `str`
      - `bytes` -> decoded to `str`
    """
    if isinstance(s, binary_type):
        return s.decode(encoding, errors)
    elif isinstance(s, text_type):
        return s
    else:
        raise TypeError("not expecting type '%s'" % type(s))


def ensure_binary(s, encoding='utf-8', errors='strict'):
    """Coerce **s** to six.binary_type.
    For Python 2:
      - `unicode` -> encoded to `str`
      - `str` -> `str`
    For Python 3:
      - `str` -> encoded to `bytes`
      - `bytes` -> `bytes`
    """
    if isinstance(s, text_type):
        return s.encode(encoding, errors)
    elif isinstance(s, binary_type):
        return s
    else:
        raise TypeError("not expecting type '%s'" % type(s))


class ListFormatter(object):

    @attrs(init=False)
    class Table(object):
        entries = attrib(type=List[Dict])
        columns = attrib(type=List[Text])

        def __init__(self, entries, columns):
            self.entries = entries
            if isinstance(columns, str):
                columns = columns.split('#')
            self.columns = columns

        def as_rows(self):  # type: () -> Iterable[Iterable[Any]]
            return (
                map(entry.get, self.columns)
                for entry in self.entries
            )

    def __init__(self, service_name):
        self.service_name = service_name

    def get_total(self, entries):
        return '\nTotal {} {}'.format(self.service_name, len(entries))

    @classmethod
    def write_csv(cls, entries, columns, dest, headers=True):
        table = cls.Table(entries, columns)
        with open(dest, 'w') as output:
            writer = csv.DictWriter(output, fieldnames=table.columns, extrasaction='ignore')
            if headers:
                writer.writeheader()
            writer.writerows(table.entries)

    @staticmethod
    def sort_in_place(entries, key, reverse=None):
        if isinstance(key, six.string_types):
            nonstrict_in_place_sort(entries, reverse, *key.split('#'))
        elif callable(key):
            entries.sort(key=key, reverse=reverse)
        else:
            raise ValueError('"sort" argument must be either a string or a callable object')