from __future__ import unicode_literals, absolute_import

import os
import json
import zipfile
import logging
import shutil
from builtins import str
from io import open
from datetime import datetime
from future.utils import viewvalues
from jinja2 import Environment, PackageLoader, contextfunction, Markup
from iotile_analytics.core.exceptions import UsageError
from future.utils import viewitems
from bokeh.embed import file_html
from bokeh.layouts import column
from bokeh.resources import Resources
from ..app import AnalyticsObject
from .analysis_template import AnalysisTemplate


@contextfunction
def include_file(ctx, name):
    """Jinja2 function to allow including literal files into a template.

    This is used for emdedding jquery into our unhosted reports.
    """

    env = ctx.environment
    return Markup(env.loader.get_source(env, name)[0])


class LiveReport(AnalysisTemplate, AnalyticsObject):
    """Base class for all dynamic html documents that can be exported.

    A LiveReport is basically a Bokeh embedded document that can reference
    external files.  If external files are referenced, they must be done so
    using one of the primitive methods defined by this class so that we are
    able to properly export the files in a way that allows them to be loaded
    outside of the context of a bokeh application or notebook.

    Structuring a Bokeh document so that we can export it is the primary job
    of the LiveReport class.  The primary difference between a LiveReport and
    a standard embedded Bokeh document is the method by which a LiveReport is
    able to reference a tree of local files and load them into
    ColumnDataSource objects without needing a web server.

    Args:
        target (int): Whether we are targetting a hosted or unhosted environment
            so that we generate the appropriate content.
    """

    # Export targets
    UNHOSTED = 0
    HOSTED = 1

    def __init__(self, target):
        self.models = []
        self.title = "Unnamed Live Report"
        self.external_files = {}
        self.loadable_models = {}
        self.target = target
        self._relative_dir = "data"

        self._logger = logging.getLogger(__name__)
        self._output_path = None
        self._file_handler = None

        # Internal template variables that subclasses can override to insert details into the
        # template.

        self.header = None
        self.before_content = None
        self.after_content = None
        self.footer = None

        self.extra_scripts = []
        self.extra_css = []

    @property
    def root(self):
        """Root object containing all models for this report.

        This is autogenerated from the list of models that the report
        generates but does not include any explanatory text above the
        models in before_content or after_content.
        """

        return column(*self.models)

    def mark_loadable_source(self, source, loadable_options, columns):
        """Mark a ColumnDataSource as externally loadable.

        This function returns the name of a javascript function that takes a
        single argument and loads the passed data source with one of the
        loadable options given its dictionary key which should be string or
        an integer that will be formated in decimal and used as a file name.

        It is safe to call this function multiple times with the same source
        value.  You may want to call this function multiple times if you have
        many different options for a given source and don't want to keep them
        all in memory at the same time.

        Note: this function should only be called during the render process
        from either the run() function or from prepare_render().  It will
        export the files immediately as you call mark_loadable_source in
        order to keep the minimum amount of information in memory.
        """

        if self.standalone:
            raise UsageError("You cannot call mark_loadable_source in a standalone report because that would not produce a single standalone file")

        if self._file_handler is None:
            raise UsageError("mark_loadable_source called at invalid time, must be called from prepare_render with a valid file_handler")

        ref = source.ref
        model_id = ref['id']
        safe_model_id = model_id.replace('-', '_')

        func_name = "load_model_{}".format(safe_model_id)
        trigger_name = "trigger_load_{}".format(safe_model_id)

        if loadable_options is None:
            loadable_options = {}
        elif isinstance(loadable_options, list):
            prior_length = len([x for x in viewvalues(self.external_files) if x['id'] == model_id])
            loadable_options = {str(prior_length + i): x for i, x in enumerate(loadable_options)}

        for name, values in viewitems(loadable_options):
            file_name = "{}_{}".format(safe_model_id, name)

            file_path = os.path.join("data", file_name)
            if self._output_path is not None:
                file_path = os.path.join(self._output_path, file_path)

            file_obj = {
                'name': file_name,
                'columns': columns,
                'id': model_id,
                'func': func_name
            }

            # Send the data to our file handler to actually save it
            if self.target == LiveReport.UNHOSTED:
                file_path += ".jsonp"
                encoded_data = self.render_jsonp(func_name, values)
                self._file_handler(file_path, encoded_data)
            else:
                raise UsageError("Only UNHOSTED LiveReport objects are currently supported")

            self.external_files[file_path] = file_obj

        self.loadable_models[model_id] = {
            'trigger_function': trigger_name,
            'load_function': func_name,
            'safe_id': safe_model_id
        }

        return trigger_name

    @classmethod
    def render_jsonp(cls, func, data):
        """Render a dictionary as a jsonp file.

        These files are suitable for loading from a local filesystem so they
        can be used for unhosted LiveReports that are just opened in a web
        browser.

        Args:
            func (str): The name of the function that should be called when
                this jsonp file is loaded.
            data (dict): The dict object that we should dump.

        Returns:
            bytes: Encoded jsonp data that can be written to a file
        """

        json_data = json.dumps(data)
        write_data = "{}({});".format(func, json_data)

        if not isinstance(write_data, str):
            write_data = str(write_data, 'utf-8')

        return write_data.encode('utf-8')

    @classmethod
    def find_template(cls, package, folder, name):
        """Find a template inside package data.

        This helper function will find a jinja2 template for you by looking
        inside a data inside an installed package.

        Args:
            package (str): A dotted package name for the subpackage that
                contains folder, which should not have an __init__.py file
            folder (str): The name of the folder that contains the templates
                and is located in the installed subpackage specified by package.
            name (str): The name of the template that we wish to load.

        Returns:
            jinja2.Template: The loaded template.
        """

        env = Environment(loader=PackageLoader(package, folder))
        template = env.get_template(name)
        return template

    @classmethod
    def render_string_template(cls, template_str, variables):
        """Render a string jinja2 template as a string.

        Args:
            template_str (str): The template that we wish to render.
            variables (dict): The variables that we wish to substitute
                into the template.
        Returns:
            str: The rendered string template.
        """

        env = Environment()
        template = env.from_string(template_str)
        return template.render(variables)

    def _save_file(self, path, contents):
        """Save a file to disk."""

        basedir = os.path.dirname(os.path.abspath(path))

        if os.path.exists(basedir) and not os.path.isdir(basedir):
            raise UsageError("Cannot create directory %s, it already exists and is not a directory.  Trying to save %s", basedir, path)

        if not os.path.exists(basedir):
            self._logger.debug("Creating folder %s in _save_file", basedir)
            os.makedirs(os.path.abspath(basedir))  # This cannot have .. in it, see https://docs.python.org/2/library/os.html#os.makedirs

        with open(path, "wb") as outfile:
            self._logger.debug("Saving file %s", path)
            outfile.write(contents)

    def run(self, output_path, file_handler=None):
        """Render this report to output_path.

        If this report is a standalone html file, the output path
        will have .html appended to it and be a single file.

        If this report is not standalone, the output will be
        folder that is created at output_path.

        Args:
            output_path (str): the path to the folder that we wish
                to create.
            file_handler (callable): A function that will be given a filelike
                object for each file that this report generates as well as
                the relative path to that file.  If you override this function,
                it is your responsibility to save all of these files, otherwise
                no output will be generated.  The purpose of this argument is
                to allow for streaming these files to a remote server or some
                other action that is not just saving the data to a local disk.

                The default behavior is to just save to local disk, which happens
                if file_handler is None (the default value).

        Returns:
            str: The path to the actual file or directory created.  This
                may differ from the file you pass in output_path by an
                extension or the addition of a subdirectory.
        """

        if output_path is None and file_handler is None:
            raise UsageError("You must pass an output_path and/or a file_handler to render this LiveReport")

        if file_handler is None:
            file_handler = self._save_file

        try:
            all_files = []

            # Make sure we have the path stem
            if output_path is not None and output_path.endswith(".html"):
                output_path = output_path[:-5]

            # Allow subclasses to finish any preparations before we render.
            self._output_path = output_path
            self._file_handler = file_handler
            self.prepare_render()

            # At this point, all additional files will have already been generated
            # so we know if we are a standalone report or not.
            if output_path is None:
                html_path = "index.html"
            elif self.standalone:
                html_path = output_path + ".html"
            else:
                html_path = os.path.join(output_path, 'index.html')

            env = Environment(loader=PackageLoader('iotile_analytics.interactive.reports', 'templates'))
            env.globals['include_file'] = include_file

            if self.target == self.UNHOSTED:
                template = env.get_template('unhosted_file.html')
            else:
                raise UsageError("HOSTED mode for LiveReports is not yet supported")

            template_vars = {
                'loadable_models': self.loadable_models,
                'timestamp': datetime.utcnow().isoformat(),
                'header': self.header,
                'before_content': self.before_content,
                'after_content': self.after_content,
                'footer': self.footer,
                'extra_scripts': self.extra_scripts,
                'extra_css': self.extra_css
            }

            # This does not return unicode on python 2 so we need to massage it
            rendered_html = file_html(self.models, Resources('inline'), self.title, template, template_vars)
            if not isinstance(rendered_html, str):
                rendered_html = str(rendered_html, 'utf-8')

            file_handler(html_path, rendered_html.encode('utf-8'))

            all_files.append(html_path)
            all_files.extend(self.external_files)
        finally:
            self._output_path = None
            self._file_handler = None

        return all_files

    def prepare_render(self):
        """Hook for any subclass that needs to setup before rendering."""
        pass
