"""
********************************************************************************
* Name: tethys_gizmos.py
* Author: Nathan Swain
* Created On: 2014
* Copyright: (c) Brigham Young University 2014
* License: BSD 2-Clause
********************************************************************************
"""
import os
import json
import time
import inspect
from datetime import datetime
from django.conf import settings
from django import template
from django.template.loader import get_template
from django.template import TemplateSyntaxError
from django.templatetags.static import static
from django.core.serializers.json import DjangoJSONEncoder

import plotly  # noqa: F401
from plotly.offline.offline import get_plotlyjs
from tethys_apps.harvester import SingletonHarvester

from ..gizmo_options.base import TethysGizmoOptions
import tethys_sdk.gizmos

GIZMO_NAME_PROPERTY = 'gizmo_name'
GIZMO_NAME_MAP = {}
EXTENSION_PATH_MAP = {}

# Add gizmos to GIZMO_NAME_MAP
for name, cls in tethys_sdk.gizmos.__dict__.items():
    if inspect.isclass(cls) and issubclass(cls, TethysGizmoOptions) and hasattr(cls, GIZMO_NAME_PROPERTY):
        GIZMO_NAME_MAP[cls.gizmo_name] = cls


# Add extension gizmos to the GIZMO_NAME_MAP
harvester = SingletonHarvester()
extension_modules = harvester.extension_modules

for module_name, extension_module in extension_modules.items():
    try:
        gizmo_module = __import__('{}.gizmos'.format(extension_module), fromlist=[''])
        for name, cls in gizmo_module.__dict__.items():
            if inspect.isclass(cls) and issubclass(cls, TethysGizmoOptions) and hasattr(cls, GIZMO_NAME_PROPERTY):
                GIZMO_NAME_MAP[cls.gizmo_name] = cls
                gizmo_module_path = gizmo_module.__path__[0]
                EXTENSION_PATH_MAP[cls.gizmo_name] = os.path.abspath(os.path.dirname(gizmo_module_path))
    except ImportError:
        # TODO: Add Log?
        continue

register = template.Library()

CSS_OUTPUT_TYPE = 'css'
CSS_GLOBAL_OUTPUT_TYPE = 'global_css'
JS_OUTPUT_TYPE = 'js'
JS_GLOBAL_OUTPUT_TYPE = 'global_js'
CSS_EXTENSION = 'css'
JS_EXTENSION = 'js'
EXTERNAL_INDICATOR = '://'
CSS_OUTPUT_TYPES = (CSS_OUTPUT_TYPE, CSS_GLOBAL_OUTPUT_TYPE)
JS_OUTPUT_TYPES = (JS_OUTPUT_TYPE, JS_GLOBAL_OUTPUT_TYPE)
GLOBAL_OUTPUT_TYPES = (CSS_GLOBAL_OUTPUT_TYPE, JS_GLOBAL_OUTPUT_TYPE)
VALID_OUTPUT_TYPES = CSS_OUTPUT_TYPES + JS_OUTPUT_TYPES


class HighchartsDateEncoder(DjangoJSONEncoder):
    """
    Special Json Encoder for Tethys
    """
    def default(self, obj):
        # Highcharts date serializer
        if isinstance(obj, datetime):
            return time.mktime(obj.timetuple()) * 1000
        return super().default(obj)


class SetVarNode(template.Node):

    def __init__(self, var_name, var_value):
        self.var_names = var_name.split('.')
        self.var_name = self.var_names.pop()
        self.var_value = var_value

    def render(self, context):
        try:
            value = template.Variable(self.var_value).resolve(context)
        except template.VariableDoesNotExist:
            value = ''

        for name in self.var_names:
            context = context[name]

        context[self.var_name] = value

        return ''


@register.tag(name='set')
def set_var(parser, token):
    """
    {% set some_var = '123' %}
    """
    parts = token.split_contents()
    if len(parts) < 4:
        raise template.TemplateSyntaxError("'set' tag must be of the form: {% set <var_name> = <var_value> %}")

    return SetVarNode(parts[1], parts[3])


@register.filter(is_safe=True)
def isstring(value):
    """
    Filter that returns a type
    """
    if value is str:
        return True
    else:
        return False


@register.filter
def return_item(container, i):
    try:
        return container[i]
    except Exception:
        return None


def json_date_handler(obj):
    if isinstance(obj, datetime):
        return time.mktime(obj.timetuple()) * 1000
    else:
        return obj


@register.filter
def jsonify(data):
    """
    Convert python data structures into a JSON string
    """
    return json.dumps(data, default=json_date_handler)


@register.filter
def divide(value, divisor):
    """
    Divide value by divisor
    """
    v = float(value)
    d = float(divisor)

    return v / d


class TethysGizmoIncludeDependency(template.Node):
    """
    Custom template include node that returns Tethys gizmos
    """
    def __init__(self, gizmo_name, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._load_gizmo_name(gizmo_name)

    def _load_gizmo_name(self, gizmo_name):
        """
        This loads the rendered gizmos into context
        """
        self.gizmo_name = gizmo_name

        if self.gizmo_name is not None:
            # Handle case where gizmo_name is a string literal
            if self.gizmo_name[0] in ('"', "'"):
                self.gizmo_name = self.gizmo_name.replace("'", '')
                self.gizmo_name = self.gizmo_name.replace('"', '')

    def _load_gizmos_rendered(self, context):
        """
        This loads the rendered gizmos into context
        """
        # Add gizmo name to 'gizmos_rendered' context variable (used to load static  libraries
        if 'gizmos_rendered' not in context:
            context.update({'gizmos_rendered': []})

        # add the gizmo in the tag to gizmos_rendered list
        if self.gizmo_name is not None:
            if self.gizmo_name not in context['gizmos_rendered']:
                if self.gizmo_name not in GIZMO_NAME_MAP:
                    raise TemplateSyntaxError('The gizmo name "{0}" is invalid.'.format(self.gizmo_name))
                context['gizmos_rendered'].append(self.gizmo_name)

    def render(self, context):
        """
        Load in the gizmos to be rendered
        """
        try:
            self._load_gizmos_rendered(context)
        except Exception as e:
            if settings.TEMPLATE_DEBUG:
                raise e

        return ''


class TethysGizmoIncludeNode(TethysGizmoIncludeDependency):
    """
    Custom template include node that returns Tethys gizmos
    """
    def __init__(self, options, gizmo_name, *args, **kwargs):
        self.options = options
        super().__init__(gizmo_name, *args, **kwargs)

    def render(self, context):
        resolved_options = template.Variable(self.options).resolve(context)

        try:
            if self.gizmo_name is None or self.gizmo_name not in GIZMO_NAME_MAP:
                if hasattr(resolved_options, GIZMO_NAME_PROPERTY):
                    self._load_gizmo_name(resolved_options.gizmo_name)
                else:
                    raise TemplateSyntaxError('A valid gizmo name is required for this input format.')

            self._load_gizmos_rendered(context)

            # Derive path to gizmo template
            if self.gizmo_name not in EXTENSION_PATH_MAP:
                # Determine path to gizmo template
                gizmo_templates_root = os.path.join('tethys_gizmos', 'gizmos')

            else:
                gizmo_templates_root = os.path.join(EXTENSION_PATH_MAP[self.gizmo_name], 'templates', 'gizmos')

            gizmo_file_name = '{0}.html'.format(self.gizmo_name)
            template_name = os.path.join(gizmo_templates_root, gizmo_file_name)

            # reset gizmo_name in case Node is rendered with different options
            self._load_gizmo_name(None)

            # Retrieve the gizmo template and render
            t = get_template(template_name)
            return t.render(resolved_options)

        except Exception:
            if hasattr(settings, 'TEMPLATES'):
                for template_settings in settings.TEMPLATES:
                    if 'OPTIONS' in template_settings \
                            and 'debug' in template_settings['OPTIONS'] \
                            and template_settings['OPTIONS']['debug']:
                        raise
            return ''


@register.tag
def gizmo(parser, token):
    """
    Similar to the include tag, gizmo loads special templates called gizmos that come with the django-tethys_gizmo
    app. Gizmos provide tools for developing user interface elements with minimal code. Examples include date pickers,
    maps, and interactive plots.

    To insert a gizmo, use the "gizmo" tag and give it a Gizmo object of configuration parameters.

    Example::

        {% load tethys_gizmos %}

        {% gizmo options %}

    The old method of using the gizmo name is still supported.

    Example::

        {% load tethys_gizmos %}

        {% gizmo gizmo_name options %}

    .. note: The Gizmo "options" object must be a template context variable.

    .. note: All supporting css and javascript libraries are loaded using the gizmo_dependency tag (see below).
    """
    gizmo_arg_list = token.split_contents()[1:]
    if len(gizmo_arg_list) == 1:
        gizmo_options = gizmo_arg_list[0]
        gizmo_name = None
    elif len(gizmo_arg_list) == 2:
        gizmo_name, gizmo_options = gizmo_arg_list
    else:
        raise TemplateSyntaxError('"gizmo" tag takes at least one argument: the gizmo options object.')

    return TethysGizmoIncludeNode(gizmo_options, gizmo_name)


@register.tag
def import_gizmo_dependency(parser, token):
    """
    The gizmo dependency tag will add the dependencies for the gizmo specified
    so that is will be loaded when using the *gizmo_dependencies* tag.

    To manually import a gizmo's dependency, use the "import_gizmo_dependency"
    tag and give it the name of a gizmo. It needs to be inside of the
    "import_gizmos" block.

    Example::

        {% load tethys_gizmos %}

        {% block import_gizmos %}
            {% import_gizmo_dependency example_gizmo %}
            {% import_gizmo_dependency "example_gizmo" %}
        {% endblock %}

    .. note: All supporting css and javascript libraries are loaded using the gizmo_dependencies tag (see below).
    """
    try:
        tag_name, gizmo_name = token.split_contents()

    except ValueError:
        raise TemplateSyntaxError('"%s" tag requires exactly one argument' % token.contents.split()[0])

    return TethysGizmoIncludeDependency(gizmo_name)


class TethysGizmoDependenciesNode(template.Node):
    """
    Loads gizmo dependencies and renders in "script" or "link" tag appropriately.
    """

    def __init__(self, output_type, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.output_type = output_type

    def _append_dependency(self, dependency, dependency_list):
        """
        Add dependency to list if not already in list
        """
        if EXTERNAL_INDICATOR in dependency:
            static_url = dependency
        else:
            static_url = static(dependency)

        if static_url not in dependency_list:
            # Lookup the static url given the path
            dependency_list.append(static_url)

    def render(self, context):
        """
        Load in JS/CSS dependencies to HTML
        """
        # NOTE: Use render_context as it is recommended to do so here
        # https://docs.djangoproject.com/en/1.10/howto/custom-template-tags/

        # initialize lists to store global gizmo css/js dependencies
        if 'global_gizmo_js_list' not in context.render_context:
            context.render_context['global_gizmo_js_list'] = []

        if 'global_gizmo_css_list' not in context.render_context:
            context.render_context['global_gizmo_css_list'] = []

        # initialize lists to store gizmo css/js dependencies
        if 'gizmo_js_list' not in context.render_context:
            context.render_context['gizmo_js_list'] = []

        if 'gizmo_css_list' not in context.render_context:
            context.render_context['gizmo_css_list'] = []

        # load list of gizmo css/js dependencies
        if 'gizmo_dependencies_loaded' not in context.render_context:
            # add all gizmos in context to be loaded
            for dict_element in context:
                for key in dict_element:
                    resolved_options = template.Variable(key).resolve(context)
                    if hasattr(resolved_options, GIZMO_NAME_PROPERTY):
                        if resolved_options.gizmo_name not in context['gizmos_rendered']:
                            context['gizmos_rendered'].append(resolved_options.gizmo_name)

            for rendered_gizmo in context['gizmos_rendered']:
                # Retrieve the "gizmo_dependencies" module and find the appropriate function
                dependencies_module = GIZMO_NAME_MAP[rendered_gizmo]

                # Only append dependencies if they do not already exist
                for dependency in dependencies_module.get_gizmo_css():
                    self._append_dependency(dependency, context.render_context['gizmo_css_list'])
                for dependency in dependencies_module.get_gizmo_js():
                    self._append_dependency(dependency, context.render_context['gizmo_js_list'])
                for dependency in dependencies_module.get_vendor_css():
                    self._append_dependency(dependency, context.render_context['global_gizmo_css_list'])
                for dependency in dependencies_module.get_vendor_js():
                    self._append_dependency(dependency, context.render_context['global_gizmo_js_list'])

                # Add the main gizmo dependencies last
                for dependency in TethysGizmoOptions.get_tethys_gizmos_css():
                    self._append_dependency(dependency, context.render_context['gizmo_css_list'])
                for dependency in TethysGizmoOptions.get_tethys_gizmos_js():
                    self._append_dependency(dependency, context.render_context['gizmo_js_list'])

            context.render_context['gizmo_dependencies_loaded'] = True

        # Create markup tags
        script_tags = []
        style_tags = []

        if self.output_type == CSS_GLOBAL_OUTPUT_TYPE or self.output_type is None:
            for dependency in context.render_context['global_gizmo_css_list']:
                style_tags.append('<link href="{0}" rel="stylesheet" />'.format(dependency))

        if self.output_type == CSS_OUTPUT_TYPE or self.output_type is None:
            for dependency in context.render_context['gizmo_css_list']:
                style_tags.append('<link href="{0}" rel="stylesheet" />'.format(dependency))

        if self.output_type == JS_GLOBAL_OUTPUT_TYPE or self.output_type is None:
            for dependency in context.render_context['global_gizmo_js_list']:
                if dependency.endswith('plotly-load_from_python.js'):
                    script_tags.append(''.join(
                        [
                            '<script type="text/javascript">',
                            get_plotlyjs(),
                            '</script>',
                        ])
                    )
                else:
                    script_tags.append('<script src="{0}" type="text/javascript"></script>'.format(dependency))

        if self.output_type == JS_OUTPUT_TYPE or self.output_type is None:
            for dependency in context.render_context['gizmo_js_list']:
                script_tags.append('<script src="{0}" type="text/javascript"></script>'.format(dependency))

        # Combine all tags
        tags = style_tags + script_tags
        tags_string = '\n'.join(tags)
        return tags_string


@register.tag
def gizmo_dependencies(parser, token):
    """
    Write all gizmo dependencies (JavaScript and CSS) to HTML.

    Example::

        {% gizmo_dependencies css %}
        {% gizmo_dependencies js %}

        {% gizmo_dependencies global_css %}
        {% gizmo_dependencies global_js %}
    """
    output_type = None

    bits = token.split_contents()
    if len(bits) > 2:
        raise TemplateSyntaxError('"{0}" takes at most one argument: the type of dependencies to output '
                                  '(either "js" or "css")'.format(token.split_contents()[0]))

    elif len(bits) == 2:
        output_type = bits[1]

    # Validate output_type
    if output_type:
        # Remove quotes
        if output_type[0] in ('"', "'"):
            output_type = output_type.replace("'", '')
            output_type = output_type.replace('"', '')

        # Lowercase
        output_type = output_type.lower()

        # Check for valid values
        if output_type not in VALID_OUTPUT_TYPES:
            raise TemplateSyntaxError('Invalid output type specified: only "js", "global_js", "css" and '
                                      '"global_css" are allowed, "{0}" given.'.format(output_type))

    return TethysGizmoDependenciesNode(output_type)