
#
# spyne - Copyright (C) Spyne contributors.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#

import logging
logger = logging.getLogger(__name__)

import os
import json
import shutil

import sqlalchemy.dialects

from uuid import uuid1
from mmap import mmap, ACCESS_READ
from contextlib import closing
from os.path import join, abspath, dirname, basename, isfile

try:
    from lxml import etree
    from lxml import html
    from spyne.util.xml import get_object_as_xml, get_xml_as_object

except ImportError as _import_error:
    etree = None
    html = None

    _local_import_error = _import_error
    def get_object_as_xml(*_, **__):
        raise _local_import_error
    def get_xml_as_object(*_, **__):
        raise _local_import_error

from sqlalchemy.sql.type_api import UserDefinedType

from spyne import ValidationError
from spyne.model.relational import FileData

from spyne.util import six
from spyne.util.six import binary_type, text_type, BytesIO, StringIO
from spyne.util.fileproxy import SeekableFileProxy


class PGXml(UserDefinedType):
    def __init__(self, pretty_print=False, xml_declaration=False,
                                                              encoding='UTF-8'):
        super(PGXml, self).__init__()
        self.xml_declaration = xml_declaration
        self.pretty_print = pretty_print
        self.encoding = encoding

    def get_col_spec(self, **_):
        return "xml"

    def bind_processor(self, dialect):
        def process(value):
            if value is None or \
                            isinstance(value, (six.text_type, six.binary_type)):
                return value

            if six.PY2:
                return etree.tostring(value, pretty_print=self.pretty_print,
                                  encoding=self.encoding, xml_declaration=False)

            return etree.tostring(value, pretty_print=self.pretty_print,
                                      encoding="unicode", xml_declaration=False)

        return process

    def result_processor(self, dialect, col_type):
        def process(value):
            if value is not None:
                return etree.fromstring(value)
            else:
                return value
        return process

sqlalchemy.dialects.postgresql.base.ischema_names['xml'] = PGXml


class PGHtml(UserDefinedType):
    def __init__(self, pretty_print=False, encoding='UTF-8'):
        super(PGHtml, self).__init__()

        self.pretty_print = pretty_print
        self.encoding = encoding

    def get_col_spec(self, **_):
        return "text"

    def bind_processor(self, dialect):
        def process(value):
            if isinstance(value, (six.text_type, six.binary_type)) \
                                                               or value is None:
                return value
            else:
                return html.tostring(value, pretty_print=self.pretty_print,
                                                         encoding=self.encoding)
        return process

    def result_processor(self, dialect, col_type):
        def process(value):
            if value is not None and len(value) > 0:
                return html.fromstring(value)
            else:
                return None
        return process


class PGJson(UserDefinedType):
    def __init__(self, encoding='UTF-8'):
        self.encoding = encoding

    def get_col_spec(self, **_):
        return "json"

    def bind_processor(self, dialect):
        def process(value):
            if isinstance(value, (text_type, binary_type)) or value is None:
                return value
            else:
                if six.PY2:
                    return json.dumps(value, encoding=self.encoding)
                else:
                    return json.dumps(value)
        return process

    def result_processor(self, dialect, col_type):
        def process(value):
            if isinstance(value, (text_type, binary_type)):
                return json.loads(value)
            else:
                return value
        return process

sqlalchemy.dialects.postgresql.base.ischema_names['json'] = PGJson


class PGJsonB(PGJson):
    def get_col_spec(self, **_):
        return "jsonb"


sqlalchemy.dialects.postgresql.base.ischema_names['jsonb'] = PGJsonB


class PGObjectXml(UserDefinedType):
    def __init__(self, cls, root_tag_name=None, no_namespace=False,
                                                            pretty_print=False):
        self.cls = cls
        self.root_tag_name = root_tag_name
        self.no_namespace = no_namespace
        self.pretty_print = pretty_print

    def get_col_spec(self, **_):
        return "xml"

    def bind_processor(self, dialect):
        def process(value):
            if value is not None:
                return etree.tostring(get_object_as_xml(value, self.cls,
                    self.root_tag_name, self.no_namespace), encoding='utf8',
                          pretty_print=self.pretty_print, xml_declaration=False)
        return process

    def result_processor(self, dialect, col_type):
        def process(value):
            if value is not None:
                return get_xml_as_object(etree.fromstring(value), self.cls)
        return process


class PGObjectJson(UserDefinedType):
    def __init__(self, cls, ignore_wrappers=True, complex_as=dict, dbt='json',
                                                               encoding='utf8'):
        self.cls = cls
        self.ignore_wrappers = ignore_wrappers
        self.complex_as = complex_as
        self.dbt = dbt
        self.encoding = encoding

        from spyne.util.dictdoc import get_dict_as_object
        from spyne.util.dictdoc import get_object_as_json
        self.get_object_as_json = get_object_as_json
        self.get_dict_as_object = get_dict_as_object

    def get_col_spec(self, **_):
        return self.dbt

    def bind_processor(self, dialect):
        def process(value):
            if value is not None:
                try:
                    return self.get_object_as_json(value, self.cls,
                        ignore_wrappers=self.ignore_wrappers,
                        complex_as=self.complex_as,
                    ).decode(self.encoding)

                except Exception as e:
                    logger.debug("Failed to serialize %r to json: %r", value, e)
                    raise

        return process

    def result_processor(self, dialect, col_type):
        from spyne.util.dictdoc import JsonDocument

        def process(value):
            if value is None:
                return None

            if isinstance(value, six.binary_type):
                value = value.decode(self.encoding)

            if isinstance(value, six.text_type):
                return self.get_dict_as_object(json.loads(value), self.cls,
                        ignore_wrappers=self.ignore_wrappers,
                        complex_as=self.complex_as,
                        protocol=JsonDocument,
                    )

            return self.get_dict_as_object(value, self.cls,
                    ignore_wrappers=self.ignore_wrappers,
                    complex_as=self.complex_as,
                    protocol=JsonDocument,
                )

        return process


class PGFileJson(PGObjectJson):
    def __init__(self, store, type=None, dbt='json'):
        if type is None:
            type = FileData

        super(PGFileJson, self).__init__(type, ignore_wrappers=True,
                                                       complex_as=list, dbt=dbt)
        self.store = store

    def bind_processor(self, dialect):
        def process(value):
            if value is not None:
                if value.data is not None:
                    value.path = uuid1().hex
                    fp = join(self.store, value.path)
                    if not abspath(fp).startswith(self.store):
                        raise ValidationError(value.path, "Path %r contains "
                                          "relative path operators (e.g. '..')")

                    with open(fp, 'wb') as file:
                        for d in value.data:
                            file.write(d)

                elif value.handle is not None:
                    value.path = uuid1().hex
                    fp = join(self.store, value.path)
                    if not abspath(fp).startswith(self.store):
                        raise ValidationError(value.path, "Path %r contains "
                                          "relative path operators (e.g. '..')")

                    if isinstance(value.handle, (StringIO, BytesIO)):
                        with open(fp, 'wb') as out_file:
                            out_file.write(value.handle.getvalue())
                    else:
                        with closing(mmap(value.handle.fileno(), 0,
                                                   access=ACCESS_READ)) as data:
                            with open(fp, 'wb') as out_file:
                                out_file.write(data)

                elif value.path is not None:
                    in_file_path = value.path

                    if not isfile(in_file_path):
                        logger.error("File path in %r not found" % value)

                    if dirname(abspath(in_file_path)) != self.store:
                        dest = join(self.store, uuid1().get_hex())

                        if value.move:
                            shutil.move(in_file_path, dest)
                            logger.debug("move '%s' => '%s'",
                                                             in_file_path, dest)

                        else:
                            shutil.copy(in_file_path, dest)
                            logger.debug("copy '%s' => '%s'",
                                                             in_file_path, dest)

                        value.path = basename(dest)
                        value.abspath = dest

                else:
                    raise ValueError("Invalid file object passed in. All of "
                                           ".data, .handle and .path are None.")

                value.store = self.store
                value.abspath = join(self.store, value.path)

                return self.get_object_as_json(value, self.cls,
                    ignore_wrappers=self.ignore_wrappers,
                    complex_as=self.complex_as,
                )

        return process

    def result_processor(self, dialect, col_type):
        def process(value):
            if value is None:
                return None

            if isinstance(value, six.text_type):
                value = json.loads(value)

            elif isinstance(value, six.binary_type):
                value = json.loads(value.decode('utf8'))

            retval = self.get_dict_as_object(value, self.cls,
                    ignore_wrappers=self.ignore_wrappers,
                    complex_as=self.complex_as)

            retval.store = self.store
            retval.abspath = path = join(self.store, retval.path)
            retval.handle = None
            retval.data = [b'']

            if not os.access(path, os.R_OK):
                logger.error("File %r is not readable", path)
                return retval

            h = retval.handle = SeekableFileProxy(open(path, 'rb'))
            if os.fstat(retval.handle.fileno()).st_size > 0:
                h.mmap = mmap(h.fileno(), 0, access=ACCESS_READ)
                retval.data = (h.mmap,)
                # FIXME: Where do we close this mmap?

            return retval

        return process
