# -*- coding: utf-8 -*-
"""
The generator class and related utility functions.
"""

from hyde.exceptions import HydeException
from hyde.fs import File, Folder
from hyde.model import Context, Dependents
from hyde.plugin import Plugin
from hyde.template import Template
from hyde.site import Node, Resource

from contextlib import contextmanager
from datetime import datetime

from shutil import copymode
from hyde.util import getLoggerWithNullHandler
logger = getLoggerWithNullHandler('hyde.engine')


class Generator(object):
    """
    Generates output from a node or resource.
    """

    def __init__(self, site):
        super(Generator, self).__init__()
        self.site = site
        self.generated_once = False
        self.deps = Dependents(site.sitepath)
        self.create_context()
        self.template = None
        Plugin.load_all(site)

        self.events = Plugin.get_proxy(self.site)

    def create_context(self):
        site = self.site
        self.__context__ = dict(site=site)
        if hasattr(site.config, 'context'):
            site.context = Context.load(site.sitepath, site.config.context)
            self.__context__.update(site.context)


    @contextmanager
    def context_for_resource(self, resource):
        """
        Context manager that intializes the context for a given
        resource and rolls it back after the resource is processed.
        """
        self.__context__.update(
            resource=resource,
            node=resource.node,
            time_now=datetime.now())
        yield self.__context__
        self.__context__.update(resource=None, node=None)

    def context_for_path(self, path):
        resource = self.site.resource_from_path(path)
        if not resource:
            return {}
        ctx = self.__context__.copy
        ctx.resource = resource
        return ctx

    def load_template_if_needed(self):
        """
        Loads and configures the template environement from the site
        configuration if its not done already.
        """

        class GeneratorProxy(object):
            """
            An interface to templates and plugins for
            providing restricted access to the methods.
            """

            def __init__(self, preprocessor=None, postprocessor=None, context_for_path=None):
                self.preprocessor = preprocessor
                self.postprocessor = postprocessor
                self.context_for_path = context_for_path

        if not self.template:
            logger.info("Generating site at [%s]" % self.site.sitepath)
            self.template = Template.find_template(self.site)
            logger.debug("Using [%s] as the template",
                            self.template.__class__.__name__)

            logger.info("Configuring the template environment")
            self.template.configure(self.site,
                        engine=GeneratorProxy(
                            context_for_path=self.context_for_path,
                            preprocessor=self.events.begin_text_resource,
                            postprocessor=self.events.text_resource_complete))
            self.events.template_loaded(self.template)

    def initialize(self):
        """
        Start Generation. Perform setup tasks and inform plugins.
        """
        logger.debug("Begin Generation")
        self.events.begin_generation()

    def load_site_if_needed(self):
        """
        Checks if the site requries a reload and loads if
        necessary.
        """
        self.site.reload_if_needed()

    def finalize(self):
        """
        Generation complete. Inform plugins and cleanup.
        """
        logger.debug("Generation Complete")
        self.events.generation_complete()

    def get_dependencies(self, resource):
        """
        Gets the dependencies for a given resource.
        """

        rel_path = resource.relative_path
        deps = self.deps[rel_path] if rel_path in self.deps \
                    else self.update_deps(resource)
        return deps

    def update_deps(self, resource):
        """
        Updates the dependencies for the given resource.
        """
        if not resource.source_file.is_text:
            return []
        rel_path = resource.relative_path
        deps = []
        if hasattr(resource, 'depends'):
            user_deps = resource.depends
            for dep in user_deps:
                deps.append(dep)
                dep_res = self.site.content.resource_from_relative_path(dep)
                if dep_res:
                    deps.extend(self.get_dependencies(dep_res))
        if resource.uses_template:
            deps.extend(self.template.get_dependencies(rel_path))
        deps = list(set(deps))
        if None in deps:
            deps.remove(None)
        self.deps[rel_path] = deps
        return deps

    def has_resource_changed(self, resource):
        """
        Checks if the given resource has changed since the
        last generation.
        """
        logger.debug("Checking for changes in %s" % resource)
        self.load_template_if_needed()
        self.load_site_if_needed()

        target = File(self.site.config.deploy_root_path.child(
                                resource.relative_deploy_path))
        if not target.exists or target.older_than(resource.source_file):
            logger.debug("Found changes in %s" % resource)
            return True
        if resource.source_file.is_binary:
            logger.debug("No Changes found in %s" % resource)
            return False
        if self.site.config.needs_refresh() or \
           not target.has_changed_since(self.site.config.last_modified):
            logger.debug("Site configuration changed")
            return True

        deps = self.get_dependencies(resource)
        if not deps or None in deps:
            logger.debug("No changes found in %s" % resource)
            return False
        content = self.site.content.source_folder
        layout = Folder(self.site.sitepath).child_folder('layout')
        logger.debug("Checking for changes in dependents:%s" % deps)
        for dep in deps:
            if not dep:
                return True
            source = File(content.child(dep))
            if not source.exists:
                source = File(layout.child(dep))
            if not source.exists:
                return True
            if target.older_than(source):
                return True
        logger.debug("No changes found in %s" % resource)
        return False

    def generate_all(self, incremental=False):
        """
        Generates the entire website
        """
        logger.info("Reading site contents")
        self.load_template_if_needed()
        self.template.clear_caches()
        self.initialize()
        self.load_site_if_needed()
        self.events.begin_site()
        logger.info("Generating site to [%s]" %
                        self.site.config.deploy_root_path)
        self.__generate_node__(self.site.content, incremental)
        self.events.site_complete()
        self.finalize()
        self.generated_once = True

    def generate_node_at_path(self, node_path=None, incremental=False):
        """
        Generates a single node. If node_path is non-existent or empty,
        generates the entire site.
        """
        if not self.generated_once and not incremental:
            return self.generate_all()
        self.load_template_if_needed()
        self.load_site_if_needed()
        node = None
        if node_path:
            node = self.site.content.node_from_path(node_path)
        self.generate_node(node, incremental)

    @contextmanager
    def events_for(self, obj):
        if not self.generated_once:
            self.events.begin_site()
            if isinstance(obj, Resource):
                self.events.begin_node(obj.node)
        yield
        if not self.generated_once:
            if isinstance(obj, Resource):
                self.events.node_complete(obj.node)
            self.events.site_complete()
            self.generated_once = True

    def generate_node(self, node=None, incremental=False):
        """
        Generates the given node. If node is invalid, empty or
        non-existent, generates the entire website.
        """
        if not node or not self.generated_once and not incremental:
            return self.generate_all()

        self.load_template_if_needed()
        self.initialize()
        self.load_site_if_needed()

        try:
            with self.events_for(node):
                self.__generate_node__(node, incremental)
            self.finalize()
        except HydeException:
            self.generate_all()

    def generate_resource_at_path(self,
                    resource_path=None,
                    incremental=False):
        """
        Generates a single resource. If resource_path is non-existent or empty,
        generats the entire website.
        """
        if not self.generated_once and not incremental:
            return self.generate_all()

        self.load_template_if_needed()
        self.load_site_if_needed()
        resource = None
        if resource_path:
            resource = self.site.content.resource_from_path(resource_path)
        self.generate_resource(resource, incremental)

    def generate_resource(self, resource=None, incremental=False):
        """
        Generates the given resource. If resource is invalid, empty or
        non-existent, generates the entire website.
        """
        if not resource or not self.generated_once and not incremental:
            return self.generate_all()

        self.load_template_if_needed()
        self.initialize()
        self.load_site_if_needed()

        try:
            with self.events_for(resource):
                self.__generate_resource__(resource, incremental)
        except HydeException:
            self.generate_all()

    def refresh_config(self):
        if self.site.config.needs_refresh():
            logger.debug("Refreshing configuration and context")
            self.site.refresh_config()
            self.create_context()

    def __generate_node__(self, node, incremental=False):
        self.refresh_config()
        for node in node.walk():
            logger.debug("Generating Node [%s]", node)
            self.events.begin_node(node)
            for resource in node.resources:
                self.__generate_resource__(resource, incremental)
            self.events.node_complete(node)


    def __generate_resource__(self, resource, incremental=False):
        self.refresh_config()
        if not resource.is_processable:
            logger.debug("Skipping [%s]", resource)
            return
        if incremental and not self.has_resource_changed(resource):
            logger.debug("No changes found. Skipping resource [%s]", resource)
            return
        logger.debug("Processing [%s]", resource)
        with self.context_for_resource(resource) as context:
            if resource.source_file.is_text:
                self.update_deps(resource)
                if resource.uses_template:
                    logger.debug("Rendering [%s]", resource)
                    try:
                        text = self.template.render_resource(resource,
                                        context)
                    except Exception:
                        logger.error("Error occurred when"
                            " processing template: [%s]" % resource)
                        raise
                else:
                    text = resource.source_file.read_all()
                    text = self.events.begin_text_resource(resource, text) or text

                text = self.events.text_resource_complete(
                                        resource, text) or text
                target = File(self.site.config.deploy_root_path.child(
                                    resource.relative_deploy_path))
                target.parent.make()
                target.write(text)
                copymode(resource.source_file.path, target.path)
            else:
                logger.debug("Copying binary file [%s]", resource)
                self.events.begin_binary_resource(resource)
                target = File(self.site.config.deploy_root_path.child(
                                    resource.relative_deploy_path))
                target.parent.make()
                resource.source_file.copy_to(target)
                self.events.binary_resource_complete(resource)
