#!/usr/bin/env python
# -*- encoding: utf-8 -*-

# Copyright 2011-2020, Nigel Small
#
# 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

from io import StringIO

from py2neo.compat import integer_types, numeric_types, string_types, ustr
from py2neo.cypher import cypher_repr, cypher_str


def html_escape(s):
    return (s.replace(u"&", u"&amp;")
             .replace(u"<", u"&lt;")
             .replace(u">", u"&gt;")
             .replace(u'"', u"&quot;")
             .replace(u"'", u"&#039;"))


class Table(list):
    """ Immutable list of records.
    """

    def __init__(self, records, keys=None):
        super(Table, self).__init__(map(tuple, records))
        if keys:
            k = list(map(ustr, keys))
        else:
            try:
                k = records.keys()
            except AttributeError:
                raise ValueError("Missing keys")
        width = len(k)
        t = [set() for _ in range(width)]
        o = [False] * width
        for record in self:
            for i, value in enumerate(record):
                if value is None:
                    o[i] = True
                else:
                    t[i].add(type(value))
        f = []
        for i, _ in enumerate(k):
            f.append({
                "type": t[i].copy().pop() if len(t[i]) == 1 else tuple(t[i]),
                "numeric": all(t_ in numeric_types for t_ in t[i]),
                "optional": o[i],
            })
        self._keys = k
        self._fields = f

    def __repr__(self):
        s = StringIO()
        self.write(file=s, header=True)
        return s.getvalue()

    def _repr_html_(self):
        """ Return a string containing an HTML representation of this table.
        This method is used by Jupyter notebooks to display the table natively within a browser.
        Internally, this method calls :meth:`.write_html` with `header=True`, writing the output into an ``io.StringIO`` instance.
        """
        s = StringIO()
        self.write_html(file=s, header=True)
        return s.getvalue()

    def keys(self):
        """ Return a list of field names for this table.
        """
        return list(self._keys)

    def field(self, key):
        """ Return a dictionary of metadata for a given field.
        The metadata includes the following values:

        `type`
            Single class or tuple of classes representing the
            field values.

        `numeric`
            :const:`True` if all field values are of a numeric
            type, :const:`False` otherwise.

        `optional`
            :const:`True` if any field values are :const:`None`,
            :const:`False` otherwise.

        """
        if isinstance(key, integer_types):
            return self._fields[key]
        elif isinstance(key, string_types):
            try:
                index = self._keys.index(key)
            except ValueError:
                raise KeyError(key)
            else:
                return self._fields[index]
        else:
            raise TypeError(key)

    def _range(self, skip, limit):
        if skip is None:
            skip = 0
        if limit is None or skip + limit > len(self):
            return range(skip, len(self))
        else:
            return range(skip, skip + limit)

    def write(self, file=None, header=None, skip=None, limit=None, auto_align=True,
              padding=1, separator=u"|", newline=u"\r\n"):
        """ Write data to a human-readable ASCII art table.

        :param file: file-like object capable of receiving output
        :param header: boolean flag for addition of column headers
        :param skip: number of records to skip before beginning output
        :param limit: maximum number of records to include in output
        :param auto_align: if :const:`True`, right-justify numeric values
        :param padding: number of spaces to include between column separator and value
        :param separator: column separator character
        :param newline: newline character sequence
        :return: the number of records included in output
        """

        space = u" " * padding
        widths = [1 if header else 0] * len(self._keys)

        def calc_widths(values, **_):
            strings = [cypher_str(value).splitlines(False) for value in values]
            for i, s in enumerate(strings):
                w = max(map(len, s)) if s else 0
                if w > widths[i]:
                    widths[i] = w

        def write_line(values, underline=u""):
            strings = [cypher_str(value).splitlines(False) for value in values]
            height = max(map(len, strings)) if strings else 1
            for y in range(height):
                line_text = u""
                underline_text = u""
                for x, _ in enumerate(values):
                    try:
                        text = strings[x][y]
                    except IndexError:
                        text = u""
                    if auto_align and self._fields[x]["numeric"]:
                        text = space + text.rjust(widths[x]) + space
                        u_text = underline * len(text)
                    else:
                        text = space + text.ljust(widths[x]) + space
                        u_text = underline * len(text)
                    if x > 0:
                        text = separator + text
                        u_text = separator + u_text
                    line_text += text
                    underline_text += u_text
                if underline:
                    line_text += newline + underline_text
                line_text += newline
                print(line_text, end=u"", file=file)

        def apply(f):
            count = 0
            for count, index in enumerate(self._range(skip, limit), start=1):
                if count == 1 and header:
                    f(self.keys(), underline=u"-")
                f(self[index])
            return count

        apply(calc_widths)
        return apply(write_line)

    def write_html(self, file=None, header=None, skip=None, limit=None, auto_align=True):
        """ Write data to an HTML table.

        :param file: file-like object capable of receiving output
        :param header: boolean flag for addition of column headers
        :param skip: number of records to skip before beginning output
        :param limit: maximum number of records to include in output
        :param auto_align: if :const:`True`, right-justify numeric values
        :return: the number of records included in output
        """

        def write_tr(values, tag):
            print(u"<tr>", end="", file=file)
            for i, value in enumerate(values):
                if tag == "th":
                    template = u'<{}>{}</{}>'
                elif auto_align and self._fields[i]["numeric"]:
                    template = u'<{} style="text-align:right">{}</{}>'
                else:
                    template = u'<{} style="text-align:left">{}</{}>'
                print(template.format(tag, html_escape(cypher_str(value)), tag), end="", file=file)
            print(u"</tr>", end="", file=file)

        count = 0
        print(u"<table>", end="", file=file)
        for count, index in enumerate(self._range(skip, limit), start=1):
            if count == 1 and header:
                write_tr(self.keys(), u"th")
            write_tr(self[index], u"td")
        print(u"</table>", end="", file=file)
        return count

    def write_separated_values(self, separator, file=None, header=None, skip=None, limit=None,
                               newline=u"\r\n", quote=u"\""):
        """ Write data to a delimiter-separated file.

        :param separator: field separator character
        :param file: file-like object capable of receiving output
        :param header: boolean flag or string style tag, such as 'i' or 'cyan',
            for addition of column headers
        :param skip: number of records to skip before beginning output
        :param limit: maximum number of records to include in output
        :param newline: newline character sequence
        :param quote: quote character
        :return: the number of records included in output
        """
        escaped_quote = quote + quote
        quotable = separator + newline + quote

        def header_row(names):
            from py2neo.vendor.pansi import ansi
            if isinstance(header, string_types):
                if hasattr(ansi, header):
                    template = "{%s}{}{_}" % header
                else:
                    t = [tag for tag in dir(ansi) if
                         not tag.startswith("_") and isinstance(getattr(ansi, tag), str)]
                    raise ValueError("Unknown style tag %r\n"
                                     "Available tags are: %s" % (header, ", ".join(map(repr, t))))
            else:
                template = "{}"
            for name in names:
                yield template.format(name, **ansi)

        def data_row(values):
            for value in values:
                if value is None:
                    yield ""
                    continue
                if isinstance(value, string_types):
                    value = ustr(value)
                    if any(ch in value for ch in quotable):
                        value = quote + value.replace(quote, escaped_quote) + quote
                else:
                    value = cypher_repr(value)
                yield value

        count = 0
        for count, index in enumerate(self._range(skip, limit), start=1):
            if count == 1 and header:
                print(*header_row(self.keys()), sep=separator, end=newline, file=file)
            print(*data_row(self[index]), sep=separator, end=newline, file=file)
        return count

    def write_csv(self, file=None, header=None, skip=None, limit=None):
        """ Write the data as RFC4180-compatible comma-separated values.
        This is a customised call to :meth:`.write_separated_values`.
        """
        return self.write_separated_values(u",", file, header, skip, limit)

    def write_tsv(self, file=None, header=None, skip=None, limit=None):
        """ Write the data as tab-separated values.
        This is a customised call to :meth:`.write_separated_values`.
        """
        return self.write_separated_values(u"\t", file, header, skip, limit)
