# -*- encoding: utf-8 -*-
#   Copyright 2009 Agile42 GmbH, Berlin (Germany)
#   
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#   
#       http://www.apache.org/licenses/LICENSE-2.0
#   
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
#   Authors:
#        - Felix Schwarz <felix.schwarz__at__agile42.com>
#        - Andrea Tomasini <andrea.tomasini__at__agile42.com>

from trac.core import Component, ExtensionPoint, TracError

from agilo.charts.api import IAgiloWidgetGenerator
from agilo.utils import Key
from agilo.utils.config import AgiloConfig

__all__ = ['ChartGenerator']


class ChartGenerator(Component):
    """The ChartGenerator can be asked to generate a widget for one of the 
    implemented charts. Furthermore it can decide to cache charts."""
    
    widget_generators = ExtensionPoint(IAgiloWidgetGenerator)
    
    def __init__(self):
        self.cache = {}
        self.config = AgiloConfig(self.env).get_section(AgiloConfig.AGILO_CHARTS)
    
    @property
    def available_charts(self):
        charts = dict()
        for generator in self.widget_generators:
            if hasattr(generator, 'get_backlog_information'):
                available = generator.get_backlog_information()
                charts.update(available)
        return charts
    
    def _get_generator(self, name):
        generator = None
        for widget_generator in self.widget_generators:
            if widget_generator.can_generate_widget(name):
                generator = widget_generator
                break
        if generator == None:
            raise TracError(u'Unknown widget type %s' % name)
        return generator
    
    def _get_cache_key(self, generator, name, values):
        data = dict(name=name, **values)
        if hasattr(generator, 'get_cache_components'):
            cache_components = generator.get_cache_components(data.keys())
        else:
            cache_components = ('name', 'sprint_name')
        
        cache_values = []
        for item in cache_components:
            if item not in data:
                return None
            cache_values.append((item, data[item]))
        # We need a hashable (non-mutable) type here therefore a dict won't 
        # work. I did not want to resort to some url-like encoding because then
        # it is not 100% clear how to identify the separate items afterwards.
        # So a tuple of tuples (key, value) should work ok.
        cache_key = tuple(cache_values)
        return cache_key
    
    def _add_configured_colors(self, kwargs):
        if 'FORE_COLOR' not in kwargs:
            # The config key here is chosen because it was used in the constant
            # Option.FORE_COLOR until 0.7.3 and I did not want to break 
            # backwards compatibility without good reason (fs, 2009-01-23)
            color = self.config.get('sprintstats.foreground', 
                                    default='#4180b3')
            kwargs['FORE_COLOR'] = color
        if 'BACK_COLOR' not in kwargs:
            color = self.config.get('sprintstats.background', 
                                    default='#94d31a')
            kwargs['BACK_COLOR'] = color
    
    def _get_widget(self, name, kwargs, use_cache):
        # The idea behind this caching is that getting the widget from the 
        # generator is by far the most expensive operation (because it will 
        # populate the widget with all necessary data). On the other hand, 
        # creating the real chart is cheap (this is especially true for charts
        # generated by flot but holds true also for matplotlib).
        # Furthermore I assume that making a copy of dicts with some (<20) keys
        # is probably also cheap. The copy frees us from any thoughts about
        # threading and multiple instances.
        new_widget = None
        
        generator = self._get_generator(name)
        cache_key = None
        if use_cache:
            cache_key = self._get_cache_key(generator, name, kwargs)
        if cache_key is not None and cache_key in self.cache:
            cached_widget = self.cache[cache_key]
            new_widget = cached_widget.copy()
        else:
            self._add_configured_colors(kwargs)
            new_widget = generator.generate_widget(name, **kwargs)
            if cache_key is not None:
                # Maybe clean cache after a certain amount of time/certain size?
                self.cache[cache_key] = new_widget.copy()
            kwargs = {} # don't overwrite everything again with .update() below
        return new_widget
    
    def _set_widget_dimensions(self, widget, name, kwargs):
        """Sets the dimensions if they were specified by the user. If 
        width/height were in kwargs, these will be deleted."""
        width, height = kwargs.get(Key.WIDTH), kwargs.get(Key.HEIGHT)
        if width == None or height == None:
            config_width = self.config.get_int('%s.%s' % (name, Key.WIDTH))
            config_height = self.config.get_int('%s.%s' % (name, Key.HEIGHT))
            if width != None and height != None:
                (width, height) = (config_width, config_height)
        if width is not None and height is not None:
            widget.set_dimensions(width, height)
        # do not set these using update_data
        kwargs.pop(Key.HEIGHT, None)
        kwargs.pop(Key.WIDTH, None)
    
    def get_chartwidget(self, name, use_cache=True, **kwargs):
        """Return a widget instance which will generate the chart's HTML."""
        
        new_widget  = self._get_widget(name, kwargs, use_cache)
        self._set_widget_dimensions(new_widget, name, kwargs)
        new_widget.update_data(**kwargs)
        return new_widget
    
    def _add_names_for_persistent_objects(self, changed_items):
        """We don't want to care if a user passes a PersistentObject
        instance or its name so this method just grabs a name if it is
        available and adds it the changed_items (this is no problem
        because as long as *one* cache component is in the cache key,
        the cache item will be invalidated."""
        new_items = {}
        for key in changed_items:
            value = changed_items[key]
            if hasattr(value, 'name'):
                new_key = key + '_name'
                new_items[new_key] = value.name
        new_items.update(changed_items)
        return new_items
    
    def invalidate_cache(self, **changed_items):
        changed_items = self._add_names_for_persistent_objects(changed_items)
        if len(changed_items) == 0:
            self.cache = {}
        else:
            del_keys = []
            for cache_key in self.cache:
                for key, value in cache_key:
                    if (key in changed_items) and (value == changed_items[key]):
                        del_keys.append(cache_key)
                        break
            for key in del_keys:
                del self.cache[key]
