# -*- encoding: utf-8 -*-
#   Copyright 2008 Agile42 GmbH, Berlin (Germany)
#   Copyright 2007 Andrea Tomasini <andrea.tomasini_at_agile42.com>
#
#   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: 
#         - Andrea Tomasini <andrea.tomasini__at__agile42.com>

import os
import re
import threading
import time

from pkg_resources import resource_filename
from trac.config import BoolOption, Option
from trac.core import Component, ExtensionPoint, Interface
from trac.env import Environment
from trac.ticket import Type
from trac.util.compat import set
from trac.util.text import to_unicode

from agilo.utils import MANDATORY_FIELDS, Key
from agilo.utils.compat import exception_to_unicode
from agilo.utils.log import debug
from agilo.utils.version_check import VersionChecker

try:
    from trac.util.translation import _tbl
    _tbl.update({
        'Accepted':'受け入れ完了',
        'Actual Burndown':'実際のバーンダウン',
        'Actual':'実際',
        'Actions':'操作',
        'Add and edit':'追加して編集',
        'Add Backlog':'バックログ種別の追加',
        'Add Custom Field':'カスタムフィールド追加',
        'Add Link':'関連付けの追加',
        'Add Team':'チーム追加',
        'Add To Actual Time':'実時間に追加',
        'Add sprint':'スプリントを追加',
        'Add Sprint':'スプリントを追加',
        'Add new sprint':'スプリントを追加',
        'Add new contingent':'予備時間を追加',
        'Advanced UI':'拡張ユーザーインターフェイス',
        'ago':'前',
        'Alias':'別名',
        'Alternative':'代替',
        'Amount':'合計',
        'Associate New':'新しい関連の作成',
        'Available Actions':'操作メニュー',
        'Available Backlogs':'バックログメニュー',
        'Available Charts':'利用可能なチャート',
        'Backlogs':'バックログ',
        'Backlog':'バックログ',
        'Backlogs list':'バックログ一覧',
        'Bug':'バグ',
        'Business Value':'ビジネス価値',
        'business days':'営業日',
        'Calculate properties':'計算ロジック',
        'Cancel':'キャンセル',
        'cannot modify':'変更不可',
        'capacity':'キャパシティ',
        'Capacity':'キャパシティ',
        'Capacity / Commitment':'キャパシティ/コミットメント',
        'Changing the sprint name may require additional (manual) changes in the database!':'スプリント名の変更は手動でのデータベース変更が必要になることがあります',
        'Checkbox':'チェックボックス',
        'Choose your Scrum Team from the list':'リストからスクラムチームを選択してください',
        'Columns':'列',
        'Columns Preferences':'カラム設定',
        'Completed':'完了済',
        'Commitment':'コミットメント',
        'Contingent':'予備時間',
        'Contingents':'予備時間',
        'Contingents planned for Sprint':'スプリント予備時間の計画',
        'Copy these fields from source to target':'リンク元からリンク先にコピーするフィールドの選択',
        'Closed':'クローズ',
        'Close sprint':'スプリントの終了',
        'Closed (by End Date)':'完了済(終了日順)',
        'Create a new team':'新規チームを作成する',
        'Create a new':'を作成',
        'Create a new Requirement':'要求を作成',
        'Create a new referenced':'新しい関連の作成：',
        'Create a new Task':'タスクを作成',
        'Create a new User Story':'ユーザーストーリを作成',
        'Create a new Bug':'バグを作成',
        'create link':'関連付けの作成',
        'Create New':'新規作成：',
        'Create new user':'新規ユーザーを作成',
        'Dashboard':'ダッシュボード',
        'days':'日にち',
        'Default value':'初期値',
        'Defines which is the "scope" of this Backlog':'このバックログのスコープを決定してください',
        'Delete':'削除',
        'delete link':'関連付けの削除',
        'Delete sprint':'スプリントの削除',
        'Delete Tickets (from *.csv)':'csvファイルでチケットを削除',
        'Delete selected team members':'選択したチームメンバーを削除',
        'Description':'説明',
        'Done':'完了',
        'Due in':'以下の日数後に終了',
        'Duration':'期間',
        'The duration counts business days only':'期間は営業日ベースでカウントします',
        'Edit':'編集',
        'Editable?':'編集可能',
        'Edit Sprint':'スプリントの編集',
        'Edit sprint':'スプリントの編集',
        'Edit Sprint details':'スプリント詳細の編集',
        'End date':'終了日',
        'End':'終了',
        'Ended':'終了済',
        'Enable Agilo Advanced UI?':'拡張ユーザーインターフェイスを有効にしますか？',
        'Estimated Remaining Time':'推定残り時間',
        'Estimated Velocity / Velocity':'見積もりベロシティ/実ベロシティ',
        'Estimated Velocity':'見積もりベロシティ',
        'Existing teams':'既に存在するチーム',
        'False':'いいえ',
        'Fields':'フィールド',
        'For a given start date, set either the end date or the duration':'開始日を指定した場合は、終了日か期間を指定してください',
        'For a given end date, set either the start date or the duration':'終了日を指定した場合は、開始日か期間を指定してください',
        'for sprint':'',
        'for this sprint':'',
        'from':'',
        'Go to the':'',
        'hours per day':'一日あたり時間数',
        'hours':'時間',
        'Ideal Burndown':'理想のバーンダウン',
        'Importance':'重要度',
        'Import Tickets (from *.csv)':'csvファイルでチケットをインポート',
        'In Progress':'進行中',
        'Full Name':'フルネーム',
        'General':'全般',
        'General Options':'一般設定',
        'Label':'ラベル',
        'Legenda':'凡例',
        'Links':'関連付け',
        'Manage Custom Fields':'カスタムフィールドの管理',
        'Manage Backlogs':'バックログの管理',
        'Manage Allowed Links':'関連付けの管理',
        'Manage Teams':'チーム管理',
        'Manage Sprints':'スプリント管理',
        'Manage Types':'種別の管理',
        'metrics':'メトリクス',
        'members':'メンバー',
        'Members':'メンバー',
        'Modify':'変更',
        'Modify Tickets (from *.csv)':'csvファイルでチケットを更新',
        'Modify Custom Field':'カスタムフィールドの変更',
        'Modify Link':'関連付けの変更',
        'Name':'名前',
        'Name of the sprint':'スプリント名',
        'New user creation':'新規ユーザー作成',
        'New Sprint for milestone':'次のマイルストン用に新しいスプリントを作成',
        'No backlogs created yet':'バックログ種別は定義されていません',
        'No Custom Fields defined for this project':'カスタムフィールドは定義されていません',
        'No teams created yet':'まだチームが作成されていません。',
        'No backlogs available':'利用可能なバックログはありません',
        'No sprints created yet':'まだスプリントが作成されていません',
        'no':'いいえ',
        'Open':'オープン中',
        'Options':'オプション',
        'Options for Radio or Select (for Select, empty first line makes field optional)':'ラジオボタンおよびリストのオプション(リストの場合は先頭行を空白にすると必須選択でなくなります)',
        'OR':'もしくは',
        'or visit the':'もしくは',
        'Order':'並び順',
        'page for statistics and team details':'の統計とチーム詳細のページを参照してください',
        'Planning for Sprint':'スプリント計画',
        'Planned':'計画済み',
        'Please specify the time in timezone':'日時は次のタイムゾーン形式で指定してください：',
        'Please note that changing this setting may require restarting the server. Depending on the infrastructure you use (mod_python, fast_cgi, mod_wsgi...) some information may be remain cached in memory':'この設定変更によりWebサーバの再起動が必要になることがあります。ご利用の環境(mod_python, fast_cgi, mod_wsgi...)によってはメモリにキャッシュが残ってしまう可能性もあります',
        'Please note that this setting only changes how time is displayed, the database is not updated automatically. If you switch from \'hours\' to \'days\' (or vice versa), a remaining time of \'5h\' will simply show up as \'5d\'.':'この設定は時間をどの単位で表示するかを変更するだけで、データベースが自動で更新されるわけではありません。もし時間から日にちに(もしくはその逆に)変更した場合、5時間の残時間は単に5日として表示されます。',
        'Product Backlog':'プロダクトバックログ',
        'Property':'プロパティ',
        'Radio':'ラジオボタン',
        'References':'参照',
        'Reference':'参照',
        'Referenced By':'参照元',
        'Referenced by':'参照元',
        'Remaining Time':'残存時間',
        'Remove from team':'チームから除外する',
        'Remove selected Backlogs':'選択したバックログ種別を削除する',
        'Remove selected Sprints':'選択したスプリントを削除する',
        'Remove selected links':'選択した関連付けを削除する',
        'Remove selected teams':'選択したチームを削除する',
        'Requirement':'要求',
        'Reserved amount':'確保済合計時間',
        'Resources':'リソース',
        'Rows':'行',
        'Rt Usp Ratio':'Rt Usp Ratio',
        'Running (by Start Date)':'実施中(開始日順)',
        'Save':'保存',
        'Schedule':'スケジュール',
        'Scope':'スコープ',
        'Scrum Dashboard':'スクラムダッシュボード',
        'Select':'選択',
        'Set the team working on this sprint':'このスプリントで稼働するチームを設定',
        'Show?':'表示',
        'show all':'全て表示',
        'Show My Tickets':'自分のチケット',
        'Shows only the tickets that are not planned for a sprint or a milestone':'スプリントもしくはマイルストンに割り当てられていないチケットのみ表示する',
        'Show these fields in list overview':'リストに表示するフィールドの選択',
        'Size of Textarea for entry (Textarea only)':'テキストエリア入力サイズ(テキストエリアのみ)',
        'Size of Textarea':'テキストエリアサイズ',
        'Source':'リンク元',
        'Sorting':'ソート',
        'Sprint':'スプリント',
        'Sprints':'スプリント',
        'Sprint Backlog':'スプリントバックログ',
        'Sprint Burndown Chart':'スプリント バーンダウンチャート',
        'Sprint Contingents':'スプリント予備時間',
        'Status':'状況',
        'Start':'開始',
        'Starting in':'以下の日数後に開始',
        'Start date':'開始日',
        'Statistics':'統計',
        'Story':'ストーリー',
        'storie':'ストーリー',
        'Story Points':'ストーリーポイント',
        'Submit changes':'変更を送信',
        'Submit Changes':'変更を送信',
        'Summary':'概要',
        'Target':'リンク先',
        'Task':'タスク',
        'Teams':'チーム',
        'Team':'チーム',
        'teams':'チーム',
        'Team':'チーム',
        'Team Members':'チームメンバー',
        'Team members':'チームメンバー',
        'Team Member':'チームメンバー',
        'Team members\' capacity planning':'チームメンバーのキャパシティプランニング',
        'Team Planning':'チーム計画',
        'Text':'テキスト',
        'Textarea':'テキストエリア',
        'This Team has no members yet':'チームにはまだメンバーがいません',
        'Tickets Reported by Me':'自分が報告したチケット',
        'tickets are planned for this sprint':'個のチケットがこのスプリントで計画済',
        'tickets are currently open':'個のチケットが現在オープン',
        'tickets have already been closed':'個のチケットがクローズ済',
        'Ticket types':'チケット種別',
        'Tickets':'チケット',
        'Time Estimation Unit':'時間見積もりの単位',
        'Time Sheet':'タイムシート',
        'to':'　から　',
        'True':'はい',
        'Total':'合計',
        'Totals':'合計',
        'Total team Capacity for':'合計キャパシティ',
        'Total Remaining Time':'合計残り時間',
        'To Start (by Start Date)':'実施待ち(開始日順)',
        'Trend':'傾向',
        'Type':'種別',
        'Types':'種別',
        'Unassigned team members':'未割当のチームメンバー',
        'User Story':'ストーリー',
        'Using which units are you estimating?':'どちらの単位で見積りしますか？',
        'Velocity':'ベロシティ',
        'View':'表示',
        'View Sprint details':'スプリント詳細を見る',
        'Weekly capacity':'週次稼働可能量',
        'Weekly capacity in hours':'週次稼働可能時間',
        'working days':'稼働日',
        'yes':'はい',
        'You don\'t have the permission to edit tickets':'チケットを編集する権限がありません',
        '|Mandatory|Linear|Exciter':'|基本|性能|魅力',
        'All Project Timeline':'全てのプロジェクトのタイムライン',
        'All Project Source':'全てのプロジェクトのソース',
        'All Project Tickets':'全てのプロジェクトのチケット',
    })
except:
    {}


def get_label(attribute_name):
    """Return the label for the given ticket attribute name. Currently this just
    replaces '_' with spaces and capitalizes the first character but maybe we
    will have a more sophisticated mechanism later."""
    return attribute_name.replace('_', ' ').title()


def normalize_ticket_type(t_type):
    """Returns a string representing the normalized ticket type, that is
    the trac type, with lowercase and underscore in place of spaces, which
    are not suitable for configuration keys."""
    if t_type:
        return t_type.lower().replace(' ', '_')




class IAgiloConfigChangeListener(Interface):
    """Listeners are notified as soon as the configuration is 
    changed."""
    def config_reloaded(self):
        """This method is called after the configuration was 
        reloaded."""


class IAgiloConfigContributor(Interface):
    """Additional components can participate in the configuration
    process of Agilo. The instance of the current AgiloConfig will 
    be passed through."""
    def initialize(self):
        """This method is called after the initialization of agilo
        config system, and allows external components to enrich the
        configuration with additional parameters."""


class AgiloConfig(Component):
    """Component to keep in memory the whole agilo configuration,
    avoiding the reading of the trac.ini file multiple times"""
    
    config_change_listeners = \
        ExtensionPoint(IAgiloConfigChangeListener)
    
    config_contributors = ExtensionPoint(IAgiloConfigContributor)
    
    backlog_filter_attribute = Option('agilo-general', 
        'backlog_filter_attribute', None,
        """Use this attribute as an additional filter sprint 
        backlog""")
    
    sprints_can_start_or_end_on_weekends = BoolOption('agilo-general', 
        'sprints_can_start_or_end_on_weekends', False,
        """If sprints must not start/end on weekends the dates are 
        automatically moved to the next working day.""")
    
    # Constant definition for Agilo Configuration
    AGILO_BACKLOGS = 'agilo-backlogs'
    AGILO_CHARTS = 'agilo-charts'
    AGILO_GENERAL = 'agilo-general'
    AGILO_LINKS = 'agilo-links'
    AGILO_TYPES = 'agilo-types'
    # This is a standard trac section but we use it in Agilo too
    TICKET_CUSTOM = 'ticket-custom'
    
    class ConfigWrapper(object):
        """Class to encapsulate utility method to read, and write into the
        trac config file: trac.ini."""
        def __init__(self, agilo_config, section=None, auto_save=False):
            assert isinstance(agilo_config.env, Environment), \
                "env parameter is not a trac.Environment object!"
            self.env = agilo_config.env
            self.agilo_config = agilo_config
            self._section = section
            self.auto_save = auto_save
            self._types_are_changed = False
        
        def section(self, section):
            if not section:
                section = self._section
            assert section, "Either give a section as argument, or pre-configure it via get_section"
            return section
        
        def get_bool(self, propname, section=None, default=False):
            section = self.section(section)
            return self.env.config.getbool(section, propname, default)
        
        def get_int(self, propname, section=None, default=None):
            section = self.section(section)
            return self.env.config.getint(section, propname, default)
        
        def get_list(self, propname, section=None, default=None):
            """
            Returns the given propname value as a list, default
            to an empty list
            """
            section = self.section(section)
            return self.env.config.getlist(section, propname, default=default)
        
        def get(self, propname, section=None, default=None):
            """Returns a value from the config as string"""
            section = self.section(section)
            value = self.env.config.get(section, propname, default)
            #if len(value.strip()) == 0:
            #    return default
            if not value or len(value.strip()) == 0:
                value = default
            return value
        
        def has_option(self, propname, section=None):
            section = self.section(section)
            return self.env.config.has_option(section, propname)
        
        def get_options(self, section=None):
            """Returns a dictionary of all the options: value pairs contained 
            into the given section of the config file."""
            section = self.section(section)
            options = dict()
            for opt in self.env.config.options(section):
                value = [p.strip() for p in opt[1].split(',')]
                options[opt[0]] = value
            return options
        
        def get_options_by_prefix(self, prefix, chop_prefix=True, section=None):
            """Returns a dictionary (name/value pairs) of all options
            in the given section that start with the given prefix.
            
            By default the prefix will be removed from all options'
            names. If chop_prefix is set to False the options'
            names will be left untouched."""
            section = self.section(section)
            options = dict()
            for name, value in self.get_options(section).items():
                if name.startswith(prefix):
                    if chop_prefix:
                        # FIXME: I think this is wrong, lot of copy&paste with get_options_by_postfix
                        options[name[:-len(prefix)]] = value
                    else:
                        options[name] = value
            return options
        
        def get_options_by_postfix(self, postfix, chop_postfix=True, section=None):
            """Returns a dictionary (name/value pairs) of all options
            in the given section that end with the given postfix.
            
            By default the postfix will be removed from all options'
            names. If chop_postfix is set to False the options'
            names will be left untouched."""
            section = self.section(section)
            options = dict()
            for name, value in self.get_options(section).items():
                if name.endswith(postfix):
                    key = name
                    if chop_postfix:
                        key = name[:-len(postfix)]
                    options[key] = value
            return options
        
        def get_options_matching_re(self, regexp, section=None):
            """Returns a dictionary (name/value pairs) of all options
            in the given section that match as a whole the given regular 
            expression."""
            section = self.section(section)
            options = dict()
            compiled_regexp = re.compile(regexp)
            for name, value in self.get_options(section).items():
                match_object = compiled_regexp.match(name)
                if match_object and len(match_object.group(0)) == len(name):
                    # only if the complete name matches not only a part of it
                    options[name] = value
            return options
        
        def _should_save(self, save):
            """Return true if the config should be saved"""
            return (save is None and self.auto_save) or save
        
        def _check_changed_types(self, section):
            """Checks if in the transaction parts of the config which are
            affecting the ticket typing have been changed"""
            if section in (AgiloConfig.AGILO_TYPES, 
                           AgiloConfig.TICKET_CUSTOM):
                self._types_are_changed = True
        
        def _clean_key_and_value(self, key, value):
            """Cleans the key and the value to suite trac needs in the config
            API, if the key can't be converted to a str and the value can't be
            converted to unicode, they are not valid, so None will be returned
            for both, otherwise the cleaned values."""
            try:
                #value = to_unicode(value)
                #key = str(key)
                # make sure that None will not be 'None'
                if value is not None:
                    value = to_unicode(value)
                if key is not None:
                    key = str(key)
            except:
                key = value = None
            return key, value
        
        def set_option(self, propname, value, section=None, save=None):
            """Adds an option to trac.ini unless the option has
            been already defined."""
            section = self.section(section)
            propname, value = self._clean_key_and_value(propname, value)
            if not propname:
                return
            if not self.env.config.get(section, propname):
                self.env.config.set(section, propname, value)
                self._check_changed_types(section)
            self.save_if_wanted(save)
            
        def change_option(self, propname, value, section=None, save=None):
            """Set or change an option in trac.ini."""
            section = self.section(section)
            propname, value = self._clean_key_and_value(propname, value)
            if not propname:
                return
            if self.env.config.get(section, propname) != value:
                self.env.config.set(section, propname, value)
                self._check_changed_types(section)
            self.save_if_wanted(save)
            
        def remove_option(self, propname, section=None, save=None):
            """Removes the propname option from section, or the default section"""
            section = self.section(section)
            try:
                propname = str(propname)
            except:
                return
            agilo_entries = self.get_options_by_prefix(propname, 
                                                       section=section, 
                                                       chop_prefix=False)
            for prop in agilo_entries.keys():
                self.env.config.remove(section, prop)
            self._check_changed_types(section)
            self.save_if_wanted(save)
        
        def remove(self, section=None, save=None):
            section = self.section(section)
            for option_name in self.get_options(section=section):
                #self.remove_option(option_name, section=section, save=save)
                self.remove_option(option_name, section=section, save=False)
            self.save_if_wanted(save)
        
        def reload(self):
            """Reloads config from the disk, so use with care"""
            # We need to force trac's config to reparse the config without
            # touching to avoid endless reloads.
            self.env.config._lastmtime = 0
            self.env.config.parse_if_needed()
        
        def save_if_wanted(self, should_save):
            if self._should_save(should_save):
                self.save()
        
        def save(self):
            """Saves the config"""
            self.wait_until_writing_the_config_would_change_the_mtime()
            self.agilo_config._config_lock.acquire()
            try:
                self.env.config.save()
            finally:
                self.agilo_config._config_lock.release()
            
            # Fixme: we want to have some notification-like system
            if self._types_are_changed:
                # something in the type definition changed so we need to reset
                # also the ticket type system.
                from agilo.ticket.api import AgiloTicketSystem
                AgiloTicketSystem(self.env).clear_cached_information()
                self._types_are_changed = False
            
            # AT: We force a reload, if the type are changed the TicketSystem is
            # forcing a touch() on the config which will reload all of the
            # components.
            self.agilo_config.reload()
        
        def wait_until_writing_the_config_would_change_the_mtime(self):
            # only do this if we actually have a file backing
            if not self.env.config.filename:
                return
            
            # If we save the config two times in one second, trac will not notice it needs to reload it
            old_mtime = int(os.path.getmtime(self.env.config.filename))
            while old_mtime == int(time.time()):
                time.sleep(0.1)
        
    # AgiloConfig
    def __init__(self, *args, **kwargs):
        """initialization of AgiloConfig"""
        super(AgiloConfig, self).__init__(*args, **kwargs)
        # set a thread locking variable to avoid overlapping of read/write
        self._config_lock = threading.RLock()
        self._config = AgiloConfig.ConfigWrapper(self)
        self._version = self._is_agilo_enabled = None
        # Set variables
        self.backlog_columns = self.backlog_charts = \
            self.backlog_conf_by_name = None
        self.ALIASES = self.TYPES = self.LABELS = None
        # At the end of the reload process the contributors will be
        # called
        self.reload()
        # Checks if the currently stored template_dir is matching the current
        # agilo installation path
        # fs: This will write the configuration so we *must* reload first
        #  - otherwise we could write outdated values to the filesystem
        # at: I really don't get this, in this way it will be reloaded twice,
        # cause as soon as the config gets saved is also reloaded. Here we are
        # creating the component for the first time, so the config is what has
        # been read from the environment right now... cause trac is starting, so
        # what possibly can be written as "outdated"?
        if self._is_template_dir_outdated():
            self.enable_agilo_ui(save=True)
    
    # exposes the whole api of ConfigWrapper as if it where from this class
    def __getattr__(self, name):
        return getattr(self._config, name)
        
    def get_section(self, name):
        """Returns the config wrapper related to the given section to the caller
        that can use the object as a subset of the config to make changes"""
        return AgiloConfig.ConfigWrapper(self, section=name)
    
    @property
    def is_agilo_enabled(self):
        """Returns True if the given environment has agilo 
        configured"""
        # Actually looking at the configuration to detect if Agilo is 
        # enabled eats quite a lot of CPU time. This small hack of 
        # adding a special attribute to the environment after checking 
        # the configuration once saved us 10 seconds on a 30 second 
        # backlog load in total (~100 items).
        # Please note that the time savings mentioned above will 
        # probably much less after we fixed some other stuff but it 
        # seemed worthwile to optimize this nevertheless.
        # Now we use this in AgiloTicket and AgiloTicketSystem quite 
        # often to provide multi-environment compatibility. Therefore 
        # it is really important to make this method as fast as 
        #possible.
        if self._is_agilo_enabled is None:
#            from agilo.ticket import AgiloTicketSystem
#            result = self.env.is_component_enabled(AgiloTicketSystem)
            agilo_entries = self.get_options_by_prefix('agilo', 
                                                       section='components')
            result = False
            for value in agilo_entries.values():
                if 'enabled' in value:
                    result = True
                    break
            self._is_agilo_enabled = result
        return self._is_agilo_enabled
    
    @property
    def is_agilo_ui_enabled(self):
        """Return True if the Agilo UI is enabled"""
        templates_dir = self.get('templates_dir', 'inherit', None)
        return templates_dir is not None
    
    def enable_agilo_ui(self, save=False):
        """Enables the Agilo UI by setting the appropriate template_dir in the
        inherit option of the trac.ini"""
        templates_dir = self.calculate_template_path()
        self.change_option('templates_dir', templates_dir, 'inherit',
                           save=save)
        
    def disable_agilo_ui(self, save=False):
        """Disables the Agilo UI by setting the appropriate template_dir in the
        inherit option of the trac.ini"""
        self.change_option('templates_dir', '', 'inherit', save=save)
    
    def _is_template_dir_outdated(self, correct_templates_dir=None):
        """Returns True if the currently stored template_dir, explicitly points
        to an old egg or installation path.
        The correct_templates_dir is only used for testing."""
        # We can't cover cases in which the user explicitly re-mapped the path
        # with symlinks or other aliases.
        if not self.is_agilo_ui_enabled or not self.is_agilo_enabled:
            return False
        current_dir = self.get('templates_dir', 'inherit', None)
        if '.egg' not in current_dir:
            return False
        if correct_templates_dir is None:
            correct_templates_dir = self.calculate_template_path()
        if current_dir != correct_templates_dir:
            return True
        return False
    
    def enable_agilo(self):
        """Enables agilo for the current environment"""
        self.set_option('agilo.*', 'enabled', 'components')
        self.enable_agilo_ui(save=False)
        self._is_agilo_enabled = True
        self.save()
    
    def disable_agilo(self):
        """Disables agilo for the current environment"""
        self.remove_option('agilo', section='components')
        self.disable_agilo_ui(save=False)
        self._is_agilo_enabled = False
        self.save()
    
    def calculate_template_path(self):
        """Calculates the template path for the current running agilo"""
        template_path = ''
        try:
            template_path = resource_filename('agilo', 'templates')
        except Exception, e:
            self.env.log.error(exception_to_unicode(e))
        return template_path
    
    def get_available_types(self, strict=False, with_fields=False):
        """Returns a list containing all the defined ticket types for 
        the given environment. With the option strict is limiting the 
        type to the ones with specific type declaration as
        [agilo-types] and also configured into the trac database."""
        ret = None
        if with_fields:
            ret = dict()
        else:
            ret = list()
        
        agilo_types = self.get_options_matching_re(r'(^[^\.]+$)', 
                                                   section=AgiloConfig.AGILO_TYPES)
        agilo_type_names = agilo_types.keys()
        for t in Type.select(self.env):
            normalized_type = normalize_ticket_type(t.name)
            if strict and not normalized_type in agilo_type_names:
                continue
            if with_fields:
                ret[t.name] = agilo_types.get(normalized_type, [])
            else:
                ret.append(t.name)
        return ret

    def get_version(self):
        """Return the version number, including build, of the current agilo"""
        if not self._version:
            version = VersionChecker().installed_version('agilo')
            if version is None:
                version = VersionChecker().installed_version('binary-agilo')
            if version is None:
                version = 'unknown'
            self._version = version
        return self._version
    
    def get_fields_for_type(self, exclude_mandatory_and_copy=False):
        """Returns a dictionary {type: fields} for all the types 
        configured. If exclude_mandatory_and_copy is set, than the 
        mandatory fields are removed, and a copy is created, otherwise 
        only a reference is returned"""
        fields_for_type = None
        if exclude_mandatory_and_copy:
            fields_for_type = dict()
            for t_type, fields in self.TYPES.items():
                fields_for_type[t_type] = [f for f in fields if f \
                                           not in MANDATORY_FIELDS]
        else:
            fields_for_type = self.TYPES
        return fields_for_type
    
    def _reload_backlog_charts(self, backlog_key, backlog_name):
        """Reloads the charts definition from the backlog config"""
        self.backlog_conf_by_name[backlog_name] = backlog_key
        option_name = '%s.charts' % backlog_key
        chartnames = self.get_list(option_name, 
                                   section=AgiloConfig.AGILO_BACKLOGS, 
                                   default=[])
        for chartname in chartnames:
            if not backlog_name in self.backlog_charts:
                self.backlog_charts[backlog_name] = list()
            self.backlog_charts[backlog_name].append(chartname)
    
    def _reload_backlog_columns(self, backlog_key, backlog_name, fields):
        """Reloads the column definition from the backlog config"""
        from agilo.ticket.api import AgiloTicketSystem
        option_name = '%s.columns' % backlog_key
        configured_columns = \
            self.get_list(option_name, 
                          section=AgiloConfig.AGILO_BACKLOGS, 
                          default=[])
        
        order = 0
        for column in configured_columns:
            if not self.backlog_columns.has_key(backlog_name):
                self.backlog_columns[backlog_name] = list()
            # Checks if there are alternative fields to show
            real_columns = column.split('|')
            # Create a list of fields to be appended to this backlog
            # each column may have a list of alternative fields
            field = read_only = None
            editable = False
            rc = real_columns[0]
            if len(real_columns) > 1:
                read_only = real_columns[1]
            if rc.find('editable') != -1:
                rc = rc.split(':')[0]
                editable = True
            # Get the field type
            for f in fields:
                if f[Key.NAME] == Key.OWNER and self.restrict_owner:
                    # restrict owner to the pulldown list
                    AgiloTicketSystem(self.env).eventually_restrict_owner(f)
                if f[Key.NAME] == rc:
                    # TODO: may be we should think about this again
                    # it may make sense to cache the fields all 
                    # together with additional agilo infos, that will 
                    # be ignored anyway from trac.
                    field = f.copy() # We don't want the real field
                    field['order'] = order
                    field['show'] = True
                    field['editable'] = editable
                    if read_only is not None:
                        field['alternative'] = read_only
                    break
            # May be it is a calculated property, then is not listed 
            # in the ticket fields, and we should disable editing
            if field is None:
                field = {'name': rc,
                         'order': order,
                         'show': True,
                         'editable': False,
                         'type': None,
                         'label': self.LABELS.get(rc, get_label(rc))}
            # Append the field
            self.backlog_columns[backlog_name].append(field)
            order += 1
    
    def notify_other_components_of_config_change(self):
        from agilo.ticket.api import AgiloTicketSystem
        ticket_system = AgiloTicketSystem(self.env)
        ticket_system.clear_cached_information()
        fields = ticket_system.get_ticket_fields(new_aliases=self.ALIASES)
        backlog_names = \
            self.get_options_by_postfix('.name', 
                                        section=AgiloConfig.AGILO_BACKLOGS)
        for bo_1, name in backlog_names.items():
            self._reload_backlog_charts(bo_1, name[0])
            self._reload_backlog_columns(bo_1, name[0], fields)
        # AT: I moved it out of the other for loop, I don't 
        # understand why it should be called after every backlog
        # has been reloaded?
        for listener in self.config_change_listeners:
            listener.config_reloaded()
    
    def reload(self, locked=False):
        """Load or reload the agilo related configuration parameters
        and recall also the initialize on the IAgiloConfigContributors
        components"""
        def _get_real_type(t_type):
            """Return the real type description from trac db"""
            for real_type in self.TYPES.keys():
                # AT: Only in config the types are lowercase, all 
                # around agilo we should use Trac types
                if normalize_ticket_type(real_type) == t_type:
                    return real_type
            # FIXME: Should not exist?
            return t_type
        
        # reset also the agilo_enabled to make sure we reread the 
        # config
        self._is_agilo_enabled = None
        if self.is_agilo_enabled:
            if not locked:
                self._config_lock.acquire()
            try:
                from agilo.ticket.api import AgiloTicketSystem
                # patch the query module of trac to use our ticket system
                from trac.ticket import query
                query.TicketSystem = AgiloTicketSystem
                # We have to reload the config to check if something changed
                # FIXME: this will trigger an additional reload of all the
                # components at runtime. We need to separate this from the
                # config initialization
                self._config.reload()
        
                agilo_types = self.get_available_types(strict=True, 
                                                       with_fields=True)
                self.TYPES = dict(zip(agilo_types.keys(), 
                                      [set(val + MANDATORY_FIELDS) for \
                                       val in agilo_types.values()]))
                #print "***TYPES*** %s" % self.TYPES
                agilo_aliases = self.get_options_by_postfix(".alias", 
                                                            section=AgiloConfig.AGILO_TYPES)
                self.ALIASES = dict(zip([_get_real_type(k) for k in \
                                         agilo_aliases.keys()], 
                                        [l[0] for l in agilo_aliases.values()]))
                # Checks if the advanced UI is enabled
                self.is_agilo_ui = self.get('templates_dir', 
                                            'inherit', None) != None
                # checks if the time measurement unit is days or hours
                self.use_days = self.get_bool(Key.USE_DAYS, 
                                              'agilo-general', False)
                # check the restrict_owner option
                self.restrict_owner = self.get_bool('restrict_owner', 
                                                    'ticket')
                # Build a dictionary containing the name: label values for 
                # each ticket field
                fields = AgiloTicketSystem(self.env).get_ticket_fields(new_aliases=self.ALIASES)
                self.LABELS = dict([(f[Key.NAME], 
                                     f.get('label', get_label(f['name']))) \
                                     for f in fields])
                # Read from config the configured columns for the backlogs
                self.backlog_columns = dict()
                self.backlog_charts = dict()
                self.backlog_conf_by_name = dict()
                
                
                # Now loads the configuration contributors
                for contributor in self.config_contributors:
                    contributor.initialize()
                    
            finally:
                if not locked:
                    self._config_lock.release()
            
            self.notify_other_components_of_config_change()
            debug(self, "Reloaded AgiloConfig: %s" % agilo_types)
        else:
            # Reset everything in case agilo is not enabled.
            self._version = None
            # Set variables
            self.backlog_columns = self.backlog_charts = \
                self.backlog_conf_by_name = None
            self.ALIASES = self.TYPES = self.LABELS = None


def populate_section(env, section_name, values, config):
    config_section = config.get_section(section_name)
    
    for option, value in values.items():
        if value.startswith('+'):
            # value starts with "+", we're prepending to existing
            # values
            env.log.debug("[Utils]: Inserting config value: " + \
                          "[%s] => %s += %s" % \
                          (section_name, option, value))
            val = config_section.get(option)
            # strip leading +
            value = value[1:]
            if val and val.strip():
                # the current value is set...
                if val.find(value) == -1:
                    # ... and does not already contain the
                    # value we're trying to prepend
                    val = "%s, %s" % (value, val)
            else:
                # option is currently not set, simply set the new 
                # value
                val = value
            config_section.change_option(option, val)
        else:
            env.log.debug("[Utils]: Setting config: " + \
                          "[%s] => %s = %s" % \
                          (section_name, option, value))
            config_section.set_option(option, value)

def initialize_config(env, config_properties):
    """Initialize the trac.ini with the values given in the
    config_properties dictionary, which contains itself
    dictionaries where as keys are set the sections, and as
    values dictionaries composed by the couples option:value."""
    assert isinstance(env, Environment), \
        "env should be an instance of trac.env.Environment, " + \
        "not a %s" % str(env)
    assert type(config_properties) == dict, \
        "config_properties should be of type dict, got a %s" % \
        type(config_properties)
    env.log.debug("[Utils]: Initializing config...")
    
    config = AgiloConfig(env)
    for section_name, values in config_properties.items():
        populate_section(env, section_name, values, config)
    # Make sure to enable Agilo, this also saves the config
    config.enable_agilo()
    debug(env, "[Utils]: Config successfully updated!")
