# Copyright (c) 2017 Civic Knowledge. This file is licensed under the terms of the
# Revised BSD License, included in this distribution as LICENSE

"""
Extensions to the MetatabDoc, Resources and References, etc.
"""

EMPTY_SOURCE_HEADER = '_NONE_'  # Marker for a column that is in the destination table but not in the source

from pathlib import Path

from metatab import MetatabDoc, WebResolver
from rowgenerators import Source, parse_app_url
from rowgenerators.exceptions import AppUrlError, RowGeneratorError

from metapack.appurl import (
    MetapackDocumentUrl,
    MetapackResourceUrl,
    MetapackUrl
)
from metapack.exc import MetatabFileNotFound
from metapack.package import Downloader
from metapack.util import datetime_now

from .html import linkify
from .util import slugify


class Resolver(WebResolver):
    def get_row_generator(self, ref, cache=None):

        try:
            return ref.metadata_url.generator
        except AttributeError:
            return super().get_row_generator(ref, cache)


class MetapackDoc(MetatabDoc):
    lib_dir_names = ('lib', 'pylib')  # Names of subdirs to look for to find a loadable module

    def __init__(self, ref=None, decl=None, cache=None, resolver=None, package_url=None, clean_cache=False,
                 downloader=None):

        # assert isinstance(ref, (MetapackDocumentUrl, MetapackResourceUrl)), (type(ref), ref)

        if downloader:
            self.downloader = downloader
        elif cache:
            self.downloader = Downloader.get_instance(cache)
        else:
            self.downloader = Downloader.get_instance()

        cache = self.downloader.cache

        if not isinstance(ref, (MetapackDocumentUrl)) and not isinstance(ref, Source) and ref is not None:
            ref = MetapackDocumentUrl(str(ref), downloader=self.downloader)


        self.register_term_class('root.resource', 'metapack.terms.Resource')
        self.register_term_class('root.reference', 'metapack.terms.Reference')
        self.register_term_class('root.distribution', 'metapack.terms.Distribution')
        self.register_term_class('root.sql', 'metapack.terms.SqlQuery')

        resolver = resolver or Resolver()

        assert resolver is not None

        if package_url is None:
            try:
                package_url = ref.package_url
            except AttributeError:
                # For iterators, generators
                package_url = None

        try:

            super().__init__(ref, decl, package_url, cache, resolver, clean_cache)
        except RowGeneratorError as e:
            raise MetatabFileNotFound("Failed to get Metatabfile for reference: '{}' ".format(ref))

        self.default_resource = None  # Set externally in open_package when the URL has a resource.

    def __enter__(self):
        """Context Management entry. Does nothing"""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context management exit. Writes the document as a CSV file"""

        if not exc_type:
            self.write_csv()

    @property
    def path(self):
        """Return the path to the file, if the ref is a file"""

        if self.ref.inner.proto != 'file':
            return None

        return self.ref.path

    @property
    def name(self):
        """Return the name from the metatab document, or the identity, and as a last resort,
        the slugified file reference"""
        return self.get_value('root.name', self.get_value('root.identifier', slugify(self._ref)))

    @property
    def nonver_name(self):
        """Return the non versioned name"""
        nv = self.as_version(None)
        if not nv:
            import re
            nv = re.sub(r'-[^-]+$', '', self.name)

        return nv

    @property
    def identifier(self):

        # Maybe there are multiple values
        t = None
        for t in self['Root'].find('root.identifier'):
            pass

        if t:
            return t.value

        return None

    def wrappable_term(self, term):
        """Return the Root.Description, possibly combining multiple terms.
        :return:
        """

        return ' '.join(e.value.strip() for e in self['Root'].find(term) if e and e.value)

    def set_wrappable_term(self, v, term):
        """Set the Root.Description, possibly splitting long descriptions across multiple terms. """

        import textwrap

        for t in self['Root'].find(term):
            self.remove_term(t)

        for l in textwrap.wrap(v, 80):
            self['Root'].new_term(term, l)

    @property
    def description(self):
        return self.wrappable_term('Root.Description')

    @description.setter
    def description(self, v):
        return self.set_wrappable_term(v, 'Root.Description')

    @property
    def abstract(self):
        return self.wrappable_term('Root.Abstract')

    @description.setter
    def abstract(self, v):
        return self.set_wrappable_term(v, 'Root.Abstract')

    @property
    def env(self):
        """Return the module associated with a package's python library"""

        try:
            r = self.get_lib_module_dict()
        except ImportError:
            r = {}

        return r

    def set_sys_path(self):
        from os.path import join, isdir, dirname, abspath
        import sys

        if not self.ref:
            return False

        u = parse_app_url(self.ref)
        doc_dir = dirname(abspath(u.path))
        # Add the dir with the metatab file to the system path

        for lib_dir_name in self.lib_dir_names:
            if isdir(join(doc_dir, lib_dir_name)):
                if not 'docdir' in sys.path:
                    sys.path.insert(0, doc_dir)
                return True

        return False

    def get_lib_module_dict(self):
        """Load the 'lib' directory as a python module, so it can be used to provide functions
        for rowpipe transforms. This only works filesystem packages"""

        from importlib import import_module


        if not self.ref:
            return {}

        u = parse_app_url(self.ref)

        if u.scheme == 'file':

            if not self.set_sys_path():
                return {}

            for module_name in self.lib_dir_names:

                try:
                    m = import_module(module_name)
                    d =  {k: v for k, v in m.__dict__.items() if not k.startswith('__')}
                    return d

                except ModuleNotFoundError as e:
                    # We need to know if it is the datapackage's module that is missing
                    # or if it is a module that it imported
                    if not module_name in str(e):
                        raise # If not our module, it's a real error.

                    continue


        else:
            return {}

    def resources(self, term='Root.Resource', section='Resources'):
        return self.find(term=term, section=section)

    def resource(self, name=None, term='Root.Resource', section='Resources'):
        return self.find_first(term=term, name=name, section=section)

    def references(self, term='Root.*', section='References'):
        return self.find(term=term, section=section)

    def reference(self, name=None, term='Root.Reference', section='References'):
        return self.find_first(term=term, name=name, section=section)

    def _repr_html_(self, **kwargs):
        """Produce HTML for Jupyter Notebook"""
        from jinja2 import Template
        from markdown import markdown as convert_markdown

        extensions = [
            'markdown.extensions.extra',
            'markdown.extensions.admonition'
        ]

        return convert_markdown(self.markdown, extensions)

    def _repr_pretty_(self, p, cycle):
        p.text(self.markdown)

    def __str__(self):
        return self.markdown

    @property
    def html(self):
        from .html import html
        return html(self)

    @property
    def markdown(self):
        from .html import markdown

        return markdown(self)

    def sort_by_term(self):
        self['Root'].sort_by_term(order=[
            'Root.Declare',
            'Root.Title',
            'Root.Description',
            'Root.Identifier',
            'Root.Name',
            'Root.Dataset',
            'Root.Origin',
            'Root.Time',
            'Root.Space',
            'Root.Grain',
            'Root.Variant',
            'Root.Version',
            'Root.Group',
            'Root.Tag',
            'Root.Keyword',
            'Root.Subject',
            'Root.Created',
            'Root.Modified',
            'Root.Issued',
            'Root.Access',
            'Root.Access',
            'Root.Distribution'
        ])


    def write_csv(self, path=None):
        """Write CSV file. Sorts the sections before calling the superclass write_csv"""

        # Sort the Sections

        self.sort_sections(['Root', 'Contacts', 'Documentation', 'References', 'Resources', 'Citations', 'Schema'])

        # Sort Terms in the root section

        # Re-wrap the description and abstract
        if self.description:
            self.description = self.description

        if self.abstract:
            self.description = self.abstract

        t = self['Root'].get_or_new_term('Root.Modified')
        t.value = datetime_now()

        self.sort_by_term()

        return super().write_csv(str(path))

    def write_ipynb(self, path:Path=None):
        from metapack_jupyter.convert import write_metatab_notebook

        write_metatab_notebook(self, path)


    def write(self, path=None):
        from metatab.exc import FormatError

        path = self._write_path(path)

        if path.suffix == '.txt':
            self.write_lines(path)
        elif path.suffix == '.csv':
            self.write_csv(path)
        elif path.suffix == '.ipynb':
            self.write_ipynb(path)
        else:
            raise FormatError("Can't write to filetype of {}".format(path.suffix))
