# -*- coding: utf-8 -*-
# - Copyright (c) 2001-2016, IPython-Contrib Development Team
# - Copyright (c) 2016-, jupyter-contrib development team

"""Jupyter server extension to enable, disable and configure nbextensions."""

from __future__ import unicode_literals

import io
import json
import logging
import os.path
import posixpath
import re

import yaml
from notebook import version_info as nb_version_info
from notebook.base.handlers import APIHandler, IPythonHandler
from notebook.utils import url_path_join as ujoin
from notebook.utils import path2url
from tornado import web

# attempt to use LibYaml if available
try:
    from yaml import CSafeLoader as SafeLoader
except ImportError:
    from yaml import SafeLoader

if nb_version_info < (5, 2, 0):
    from notebook.base.handlers import json_errors
else:
    # for notebook >= 5.2.0, instead of using json_errors, we must subclass
    # APIHandler. Since we already do this, just do nothing extra
    def json_errors(method):
        return method

__version__ = '0.4.1'

absolute_url_re = re.compile(r'^(f|ht)tps?://')


def _process_nbextension_spec(spec, relative_url_base=''):
    """
    Sanity-check and preprocess a spec loaded from a yaml descriptor file.

    Returns *either* a processed dict *or* a string error message describing
    why the spec was not suitable.
    """
    if not isinstance(spec, dict):
        return 'spec is not a dict, but an instance of {}'.format(type(spec))
    if 'Type' not in spec:
        return 'spec has no Type key'
    valid_types = {'IPython Notebook Extension', 'Jupyter Notebook Extension'}
    if str(spec['Type']).strip() not in valid_types:
        return 'spec has invalid value for Type key: {!r}'.format(spec['Type'])
    if 'Main' not in spec and 'require' not in spec:
        return 'spec has neither "Main" nor "require" key'
    # strip .js file extension from Main to give require path
    if 'require' not in spec:
        spec['require'] = os.path.splitext(spec['Main'])[0]

    spec.setdefault('Name', spec['require'])
    spec.setdefault('Compatibility', '?.x')
    spec.setdefault('Section', 'notebook')

    # generate relative URLs within the nbextensions namespace,
    # from urls relative to the yaml file
    for from_key, to_key in {
            'Link': 'readme', 'Icon': 'icon', 'Main': 'require'}.items():
        # check for the to_key first, use from_key as backup
        # str needed in python 3, otherwise it ends up bytes
        from_val = str(spec.get(to_key, ''))
        if not from_val:
            from_val = str(spec.get(from_key, ''))
        if not from_val:
            continue
        if absolute_url_re.match(from_val):
            spec[to_key] = from_val
        else:
            spec[to_key] = posixpath.normpath(
                ujoin(relative_url_base, from_val))
    return spec


def get_configurable_nbextensions(
        nbextension_dirs, exclude_dirs=('mathjax',), as_dict=False, log=None):
    """Build a list of configurable nbextensions based on YAML descriptor files.

    descriptor files must:
      - be located under one of nbextension_dirs
      - have the file extension '.yaml' or '.yml'
      - contain (at minimum) the following keys:
        - Type: must be 'IPython Notebook Extension' or
                'Jupyter Notebook Extension'
        - Main: relative url of the nbextension's main javascript file
    """
    extension_dict = {}

    # Traverse through nbextension subdirectories to find all yaml files
    # However, don't check directories twice. See
    #   github.com/Jupyter-contrib/jupyter_nbextensions_configurator/issues/25
    already_checked = set()
    for root_nbext_dir in nbextension_dirs:
        if root_nbext_dir in already_checked:
            continue
        else:
            already_checked.add(root_nbext_dir)
        if log:
            log.debug(
                'Looking for nbextension yaml descriptor files in {}'.format(
                    root_nbext_dir))
        for direct, dirs, files in os.walk(root_nbext_dir, followlinks=True):
            # filter to exclude directories
            dirs[:] = [d for d in dirs if d not in exclude_dirs]
            for filename in files:
                if os.path.splitext(filename)[1] not in ['.yml', '.yaml']:
                    continue
                yaml_path = os.path.join(direct, filename)
                yaml_relpath = os.path.relpath(yaml_path, root_nbext_dir)
                with io.open(yaml_path, 'r', encoding='utf-8') as stream:
                    try:
                        extension = yaml.load(stream, Loader=SafeLoader)
                    except yaml.YAMLError:
                        if log:
                            log.warning(
                                'Failed to load yaml file {}'.format(
                                    yaml_relpath))
                        continue
                extension = _process_nbextension_spec(
                    extension,
                    relative_url_base=path2url(os.path.dirname(yaml_relpath)))
                if not isinstance(extension, dict):
                    continue
                require = extension['require']

                if log:
                    if require in extension_dict:
                        msg = 'nbextension {!r} has duplicate listings'.format(
                            extension['require'])
                        msg += ' in both {!r} and {!r}'.format(
                            yaml_path, extension_dict[require]['yaml_path'])
                        log.warning(msg)
                        extension['duplicate'] = True
                    else:
                        log.debug('Found nbextension {!r} in {}'.format(
                            extension['Name'], yaml_relpath))

                extension_dict[require] = {
                    'yaml_path': yaml_path, 'extension': extension}
    if as_dict:
        return extension_dict
    return [val['extension'] for val in extension_dict.values()]


class ConfiguratorLogger(logging.LoggerAdapter):
    """Logging adapter to prepend the serverextension name to log messages."""

    def __init__(self, logger):
        super(ConfiguratorLogger, self).__init__(logger, {})

    def process(self, msg, kwargs):
        return '[{}] {}'.format(__name__, msg), kwargs


class NBExtensionHandlerJSON(APIHandler):
    """
    Returns a json list describing the configurable nbextensions.

    Based on part of notebook.services.config.handlers.ConfigHandler
    """

    @APIHandler.log.getter
    def log(self):
        return ConfiguratorLogger(super(NBExtensionHandlerJSON, self).log)

    @web.authenticated
    @json_errors
    def get(self):
        self.set_header("Content-Type", 'application/json')
        nbapp_webapp = self.application
        nbextension_dirs = nbapp_webapp.settings['nbextensions_path']
        extension_list = get_configurable_nbextensions(
            nbextension_dirs=nbextension_dirs, log=self.log)
        self.finish(json.dumps(extension_list))


class NBExtensionHandlerPage(IPythonHandler):
    """Renders the nbextension configuration interface."""

    @IPythonHandler.log.getter
    def log(self):
        return ConfiguratorLogger(super(NBExtensionHandlerPage, self).log)

    @web.authenticated
    def get(self):
        """Render the nbextension configuration interface."""
        self.finish(self.render_template(
            'nbextensions_configurator.html',
            page_title='Nbextensions Configuration',
            **self.application.settings
        ))


class RenderExtensionHandler(IPythonHandler):
    """Renders markdown files as pages."""

    @IPythonHandler.log.getter
    def log(self):
        return ConfiguratorLogger(super(RenderExtensionHandler, self).log)

    @web.authenticated
    def get(self, path):
        """Render given markdown file."""
        if not path.endswith('.md'):
            # for all non-markdown items, we redirect to the actual file
            return self.redirect(self.base_url + path)
        self.finish(self.render_template(
            'rendermd.html',
            md_url=path,
            page_title=path,
            **self.application.settings
        ))


def load_jupyter_server_extension(nbapp):
    """Load and initialise the server extension."""
    logger = ConfiguratorLogger(nbapp.log)
    logger.debug('Loading {}'.format(__version__))
    webapp = nbapp.web_app

    # ensure our template gets into search path
    templates_dir = os.path.join(os.path.dirname(__file__), 'templates')
    logger.debug('  Editing templates path to add {}'.format(templates_dir))
    rootloader = webapp.settings['jinja2_env'].loader
    for loader in getattr(rootloader, 'loaders', [rootloader]):
        if hasattr(loader, 'searchpath') and \
                templates_dir not in loader.searchpath:
            loader.searchpath.append(templates_dir)

    base_url = webapp.settings['base_url']

    # make sure our static files are available
    static_files_path = os.path.normpath(os.path.join(
        os.path.dirname(__file__), 'static'))
    logger.debug(
        '  Editing nbextensions path to add {}'.format(static_files_path))
    if static_files_path not in webapp.settings['nbextensions_path']:
        webapp.settings['nbextensions_path'].append(static_files_path)

    # add our new custom handlers
    logger.debug('  Adding new handlers')
    new_handlers = [(ujoin(base_url, '/nbextensions/' + u), h) for u, h in [
        (r"?", NBExtensionHandlerPage),
        (r"nbextensions_configurator/list$", NBExtensionHandlerJSON),
        (r"nbextensions_configurator/rendermd/(.*)", RenderExtensionHandler),
    ]]
    webapp.add_handlers(".*$", new_handlers)

    logger.info('enabled {}'.format(__version__))

def _jupyter_nbextension_paths():
    return [
        dict(
            section="notebook",
            src="static/nbextensions_configurator",
            dest="nbextensions_configurator",
            require='nbextensions_configurator/config_menu/main',
        ),
        dict(
            section="tree",
            src="static/nbextensions_configurator",
            dest="nbextensions_configurator",
            require='nbextensions_configurator/tree_tab/main',
        ),
    ]

def _jupyter_server_extension_paths():
    return [{
        'module': __name__
    }]
