# -*- coding: utf-8 -*-
"""
API for administrating custom ticket fields in Trac.
Supports creating, getting, updating and deleting custom fields.

License: BSD

(c) 2005-2012 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
"""

import pkg_resources
import re

from trac.core import *
from trac.ticket.api import TicketSystem
from trac.util.translation import domain_functions

add_domain, _, tag_ = \
    domain_functions('customfieldadmin', ('add_domain', '_', 'tag_'))


class CustomFields(Component):
    """ These methods should be part of TicketSystem API/Data Model.
    Adds update_custom_field and delete_custom_field methods.
    (The get_custom_fields is already part of the API - just redirect here,
     and add option to only get one named field back.)

    Input to methods is a 'cfield' dict supporting these keys:
        name = name of field (ascii alphanumeric only)
        type = text|checkbox|select|radio|textarea|time
        label = label description
        value = default value for field content
        options = options for select and radio types
                  (list, leave first empty for optional)
        rows = number of rows for text area
        order = specify sort order for field
        format = text|wiki|reference|list (for text)
        format = text|wiki (for textarea)
        format = relative|date|datetime (for time)
    """

    config_options = ['label', 'value', 'options', 'rows', 'order', 'format']
    field_types = ['text', 'checkbox', 'select', 'radio', 'textarea', 'time']
    field_formats = {
        'text': ['plain', 'wiki', 'reference', 'list'],
        'textarea': ['plain', 'wiki'],
        'time': ['relative', 'date', 'datetime'],
    }

    def __init__(self):
        # bind the 'customfieldadmin' catalog to the specified locale directory
        try:
            locale_dir = pkg_resources.resource_filename(__name__, 'locale')
        except KeyError:
            # No 'locale', hence no compiled message catalogs. Ignore.
            pass
        else:
            add_domain(self.env.path, locale_dir)

    def get_custom_fields(self, cfield=None):
        """ Returns the custom fields from TicketSystem component.
        Use a cfdict with 'name' key set to find a specific custom field only.
        """
        items = TicketSystem(self.env).get_custom_fields()
        for item in items:
            if item['type'] == 'textarea':
                item['rows'] = item.pop('height')
            if cfield and item['name'] == cfield['name']:
                return item  # only return specific item with cfname
        if cfield:
            return None   # item not found
        else:
            return items  # return full list

    def verify_custom_field(self, cfield, create=True):
        """ Basic validation of the input for modifying or creating
        custom fields. """
        # Requires 'name' and 'type'
        if not (cfield.get('name') and cfield.get('type')):
            raise TracError(
                    _("Custom field requires attributes 'name' and 'type'."))
        # Use lowercase custom fieldnames only
        cfield['name'] = cfield['name'].lower()
        # Check field name is not reserved
        tktsys = TicketSystem(self.env)
        if cfield['name'] in tktsys.reserved_field_names:
            raise TracError(_("Field name '%(name)s' is reserved",
                              name=cfield['name']))
        # Only alphanumeric characters (and [-_]) allowed for custom fieldname
        if re.search('^[a-z][a-z0-9_]+$', cfield['name']) == None:
            raise TracError(_("Only alphanumeric characters allowed for " \
                             "custom field name ('a-z' or '0-9' or '_'), " \
                             "with 'a-z' as first character."))
        # Name must begin with a character - anything else not supported by Trac
        if not cfield['name'][0].isalpha():
            raise TracError(
                    _("Custom field name must begin with a character (a-z)."))
        # Check that it is a valid field type
        if not cfield['type'] in self.field_types:
            raise TracError(_("%(field_type)s is not a valid field type",
                              field_type=cfield['type']))
        # Check that field does not already exist
        # (if modify it should already be deleted)
        if create and self.config.get('ticket-custom', cfield['name']):
            raise TracError(_("Can not create as field already exists."))
        if create and [f for f in tktsys.fields
                         if f['name'] == cfield['name']]:
            raise TracError(_("Can't create a custom field with the "
                              "same name as a built-in field."))

    def create_custom_field(self, cfield):
        """ Create the new custom fields (that may just have been deleted as
        part of 'modify'). In `cfield`, 'name' and 'type' keys are required.
        Note: Caller is responsible for verifying input before create."""
        # Need count pre-create for correct order
        count_current_fields = len(self.get_custom_fields())
        # Set the mandatory items
        self.config.set('ticket-custom', cfield['name'], cfield['type'])
        # Label = capitalize fieldname if not present
        self.config.set('ticket-custom', cfield['name'] + '.label',
                        cfield.get('label') or cfield['name'].capitalize())
        # Optional items
        if 'value' in cfield:
            self.config.set('ticket-custom', cfield['name'] + '.value',
                                                        cfield['value'])
        if cfield['type'] in ('select', 'radio') and \
                'options' in cfield:
            if cfield.get('optional', False) and '' not in cfield['options']:
                self.config.set('ticket-custom', cfield['name'] + '.options',
                                '|' + '|'.join(cfield['options']))
            else:
                self.config.set('ticket-custom', cfield['name'] + '.options',
                               '|'.join(cfield['options']))
        if 'format' in cfield and \
                cfield['type'] in ('text', 'textarea', 'time') and \
                cfield['format'] in self.field_formats[cfield['type']]:
            self.config.set('ticket-custom', cfield['name'] + '.format',
                            cfield['format'])
        # Textarea
        if cfield['type'] == 'textarea':
            rows = cfield.get('rows', 0) and int(cfield.get('rows', 0)) > 0 \
                                                and cfield.get('rows') or 5
            self.config.set('ticket-custom', cfield['name'] + '.rows', rows)
        # Order
        order = cfield.get('order') or count_current_fields + 1
        self.config.set('ticket-custom', cfield['name'] + '.order', order)
        self._save(cfield)

    def update_custom_field(self, cfield, create=False):
        """ Updates a custom field. Option to 'create' is kept in order to keep
        the API backwards compatible. """
        if create:
            self.verify_custom_field(cfield)
            self.create_custom_field(cfield)
            return
        # Check input, then delete and save new
        if not self.get_custom_fields(cfield=cfield):
            raise TracError(_("Custom Field '%(name)s' does not exist. " \
                    "Cannot update.", name=cfield.get('name') or '(none)'))
        self.verify_custom_field(cfield, create=False)
        self.delete_custom_field(cfield, modify=True)
        self.create_custom_field(cfield)

    def delete_custom_field(self, cfield, modify=False):
        """ Deletes a custom field. Input is a dictionary
        (see update_custom_field), but only ['name'] is required.
        """
        if not self.config.get('ticket-custom', cfield['name']):
            return # Nothing to do here - cannot find field
        if not modify:
            # Permanent delete - reorder later fields to lower order
            order_to_delete = self.config.getint('ticket-custom',
                                                 cfield['name']+'.order')
            cfs = self.get_custom_fields()
            for field in cfs:
                if field['order'] > order_to_delete:
                    self.config.set('ticket-custom', field['name'] + '.order',
                                    field['order'] - 1 )
        supported_options = [cfield['name']] + \
            [cfield['name'] + '.' + opt for opt in self.config_options]
        for option, _value in self.config.options('ticket-custom'):
            if modify and not option in supported_options:
                # Only delete supported options when modifying
                # http://trac-hacks.org/ticket/8188
                continue
            if option == cfield['name'] \
                    or option.startswith(cfield['name'] + '.'):
                self.config.remove('ticket-custom', option)
        # Persist permanent deletes
        if not modify:
            self._save(cfield)

    def _save(self, cfield=None):
        """ Saves a value, clear caches if needed / supported. """
        self.config.save()
        del TicketSystem(self.env).custom_fields
        # Re-populate contents of cfield with new values and defaults
        if cfield:
            stored = self.get_custom_fields(cfield=cfield)
            if stored: # created or updated (None for deleted so just ignore)
                cfield.update(stored)
