/* 
 * jquery.substitutions.js
 * Author: Vincent Pretre for Zest Sfotware (www.zestsoftware.nl)
 * Licence: GPL
 *
 * This plug-in allows to link some fields (input, select ...) with a TinyMCE
 * editor. When a value is changed in the fields, the TinyMCE editor is 
 * automatically updated. When the value is changed in TinyMCE, the fields
 * are updated.
 * This system is built for TinyMCE but shall work with other systems.
 *
 * This plug-in adds 3 news methods to jquery:
 * - $.substitutions(mapping, options) that makes the content of
 *   the editor change when the fields are updated and vice-versa.
 * - $.substitutions_fields_to_editor(mapping, options) that makes
 *   the content of the editor change accordingly to the change
 *   done in the fields.
 * - $.substitutions_editor_to_options(maping, options) that makes
 *   the fields change when the text in the editor is updated.
 * 
 * Parameters for each method is described before the function. The common
 * options for each method are:
 * - iframe_id: the selector used to find the editor (despite the name,
 *   it can be something else than an id).
 * - sub_class: the class used to find the text substituted in the editor.
 * - wrong_substitution_class: a class applied when the content of a field
 *   or a substituted text is incorrect.
 * - default_error_msg: text which is shown when a user typed an incorrect
 *   substitution for a select (by default: 'Possible values for this field
 *   are: ')
 * - add_error: function to customize the way an field/substitution with an
 *   incorrect hour is displayed.
 * - remove_error: function to undo effect of 'add_error'.
 * - debug_mode: turn it to true to have some debugging information in the
 *   console.
 * - animation_color: the color applied to the fields/substituted elements
 *   when they are automatically updated.
 * - animation_speed: the speed of the animations.
 * - animate_field: function to customize the animation when the fields
 *   are updated.
 * - animate_substitution: function to customize the animation when a 
 *   substituted element is updated.
 */

(function($) {
    /**********************************************************************
     Configuration variables - can be updated using the options
     **********************************************************************/

    /* Default selector for the iframe containing the editor*/
    var iframe_id = '#text_ifr';

    /* Default class used on substituted elements */
    var sub_class = 'autogenerated_text';

    /* Default class applied when a substitution or a field is incorrect */
    var wrong_substitution_class = 'wrong_substitution';

    var default_error_msg = 'Possible values for this field are: ';

    /* Default color applied when animating fields and substitutions */
    var animation_color = '#bdb130';

    /* Default animation speed */
    var animation_speed = 10;

    /* Variable used to know if the debug mode is turned on*/
    var debug_mode = false;

    /**********************************************************************
     Internal variables - should not be modified.
     **********************************************************************/
    
    /* Variable used to check if the user is currently in the editor */
    var SUBSTITUTIONS_IN_EDITOR = false;

    /* Variable used to know if the editor is ready
       (in practice, just check that the iframe is there)*/
    var SUBSTITUTIONS_EDITOR_READY = false;

    /* We store the original background of objects for animations.
       It's just a dictionnary, so you if you decide to use custom
       animations you can store anithing there.
     */
    var backgrounds = {};

    /* Dictionnary storing which element is currently animated to
     avoid animating a same element two times */
    var animated = {};


    /**********************************************************************
     Here starts the code :)
     **********************************************************************/

    /* Simple debug function to log information.
     */
    debug = function(msg) {
	if (debug_mode) {
	    console.log('[jquery.substitutions] ' + msg);
	}
    }

    make_uid = function(el) {
	uid = '';
	for (i = 0; i < el.length; i++) {
	    uid += $(el[i]).attr('id') + $(el[i]).attr('rel');
	}
	return uid;
    }

    /* Adds the wrong_substitution_class to 'el' plus adds the error message
       as a title attribute if provided.
     */
    add_error = function(el, msg) {
	el.addClass(wrong_substitution_class);
	if (typeof(msg) == 'string') {
	    el.attr('title', msg);
	}
    }

    /* Removes title attribute of the element 'el' and removes the class
       wrong_substitution_class.
     */
    remove_error = function(el) {
	el.removeAttr('title')
	el.removeClass(wrong_substitution_class);	
    }

    /* Makes a short animation on the field to show it changed.
     */
    animate_field = function(field) {
	uid = make_uid(field);
	if (typeof(animated[uid]) == 'undefined') {
	    animated[uid] = true;
	}
	else {
	    if (animated[uid]) {
		return;
	    }
	}

	field.wrap('<div style=""/>');
	p = field.parent();

	p.css({'backgroundColor': animation_color});
	p.animate({'backgroundColor': '#FFFFFF'},
		  animation_speed,
		  function() {
		      /* Could use unwrap but it only appeared on jquery 1.4 which is
		         not in Plone 3. */
		      $(this).replaceWith($(this).contents());
		      animated[uid] = false;
		  });
    }

    /* Makes a short animation on the substituted text to show it changed.
     */
    animate_substitution = function(sub) {
	uid = make_uid(sub);
	if (typeof(backgrounds[uid]) == 'undefined') {
	    backgrounds[uid] = sub.css('backgroundColor');
	}

	if (typeof(animated[uid]) == 'undefined') {
	    animated[uid] = true;
	}
	else {
	    if (animated[uid]) {
		return;
	    }
	}

	sub.removeClass(sub_class);

	sub.css('backgroundColor', animation_color);
	sub.animate({'backgroundColor': backgrounds[uid]},
		    animation_speed,
		    function() {
			sub.css('backgroundColor', '');
			sub.addClass(sub_class);
			animated[uid] = false;
		    });
    }

    /* Gets the value displayed by a field. It's mainly useful for
       'select', 'radio' and 'checkbox' as the value and what is displayed
       can differ.
     */
    displayed_value = function(field) {	
	if (field.type == 'text' || field.type == 'hidden') {
	    return $(field).attr('value');
	}

	if (field.type == 'select-one') {
	    options = $(field).find('option');
	    value = $(field).val();
	    displayed = ''

	    options.each(function() {
		    if ($(this).attr('value') == value) {
			displayed = $(this).html();
		    }
		});
	    return displayed;
	}
    }

    /* Finds all possible values for a field.
       Return an empty list for text, hidden or textarea.
     */
    find_displayed_values = function(field) {
	values = [];

	if (field.type == 'select-one') {
	    options = $(field).find('option');
	    options.each(function() {
		    values[values.length] = $(this).html();
		});
	}	

	return values;
    }

    /* Given a field, return a message:
       'Possible values for this field are: opt1, opt2 ...'
     */
    make_error_msg_from_field = function(field) {
	possible_values = find_displayed_values(field);
	if (possible_values.length == 0) {
	    return null;
	}
	msg = default_error_msg;
	    
	for (k = 0; k < possible_values.length; k++) {
	    msg += possible_values[k];
	    
	    if (k == possible_values.length - 1) {
		msg += '.';
	    }
	    else {
		msg += ', ';
	    }
	}
	return msg;
    }

    /* Makes the opposite of the 'displayed_value'. Takes a value, looks
       if it's displayed in the field and set's the value of the field.

       Example: We have the corresponding select field:
       <select>
         <option value="1">One</option>
	 <option value="2">Two</option>
       </select>

       If you call set_value(my_select, 'Two'), the option
       'Two' will be selected.

       This method returns 'false' if the provided value is not displayed
       by the field or 'null' if the value did not change.
     */
    set_value = function(field, value) {
	if ($(field).val() == value) {
	    return;
	}

	if (field.type == 'text' || field.type == 'hidden') {
	    $(field).attr('value', value);
	}

	if (field.type == 'select-one') {
	    options = $(field).find('option');
	    new_value = ''

	    options.each(function() {
		    if ($(this).attr('value').toLowerCase () == value.toLowerCase()) {
			new_value = $(this).attr('value');
		    }
		});

	    if (new_value == '') {
		debug('Value ' + value + ' not found for field ' + $(field).attr('id'));
		return false;
	    } else {
		$(field).attr('value', new_value);
	    }
	}

	return true;
    }
    

    /* Finds the element where the substituted value is stored.
     */
    find_substitution = function(rel) {
	subs = $(iframe_id).contents().find('\'.' + sub_class + '[rel=' + rel + ']\'');
	if (subs.length == 0) {
	    debug('No substitution found with rel ' + rel);
	}
	return subs;
    }

    /* Binds a function when the field change with a function
       that updates the editor content.
     */
    bind_field_change = function(f) {
	/* This avoid loops */
	if ($(f.field_id).length == 0) {
	    debug('Field not found: ' + f.field_id);
	    return;
	}
	$(f.field_id).pingchange('value', function() {
		if (SUBSTITUTIONS_IN_EDITOR) {
		    return;
		}

		value = displayed_value(this[0]);
		sub = find_substitution(f.sub_rel);
		
		if (typeof(f.regexp) != 'undefined') {
		    reg = new RegExp(f.regexp, "g");
		    
		    if (reg.exec(value) == null) {
			debug('No match found for ' + f.regexp + ' on string ' + value);
			add_error($(f.field_id), f.validation_msg);
			return;
		    }
		    
		    if (typeof(f.regexp_replace) != 'undefined') {
			new_val = value.replace(reg, f.regexp_replace);
		    }
		    else {
			new_val = value;
		    }
		}
		else {
		    new_val = value;
		}
		
		sub.html(new_val);
		/* We remove potential error on the field and the substituted value. */
		remove_error(sub);
		animate_substitution(sub);
		remove_error($(f.field_id));
	    });
    }

    /* Updates the default variables with the options given.
       See this file's header to view the possible options.
     */
    sub_treat_options = function(options) {
	if (typeof(options) == 'undefined') {
	    options = {};
	}

	keys = ['iframe_id',
		'sub_class',
		'wrong_substitution_class',
		'default_error_msg',
		'add_error',
		'remove_error',
		'debug_mode',
		'animation_color',
		'animation_speed',
		'animate_field',
		'animate_substitution'];

	for (i = 0; i < keys.length; i++) {
	    key = keys[i];
	    if (typeof(options[key]) != 'undefined') {
		eval(key + ' =  options[\'' + key + '\']');
	    }
	}
	return options;
    }

    /* $.substitutions_field_to_editor(mapping. options)
       mapping is a dictionnary, where the keys are the ids of the fields
       and the associated values are the 'rel' attribute of the element to change.

       option defines the options for the function. The only option added to the default
       ones is 'custom_fields', which is a list of custom fields.

       Custom fields:
       This is mainly intended for calendar or other fields that are not
       directly manually updated.
       A custom field as the following properties:
       - field_id: the id of the field containing the final value.
       - sub_rel: the value of the 'rel' attribute on the element that
         will be updated.
       - regexp: the regular expression that the value should matc
       - validation_msg: a message displayed when the text entered do not
         match the regexp.
       - regexp_replace: used when replacing the value in the element.

       Example:
       cf = {field_id: '#my_calendar',
             sub_rel: 'my_date',
             regexp: '(\d{4})/(\d{2})/(\d{2})'
             regexp_replace: '$3-$2-$1'}

       When the value in the field #my_calendar is changed, the system will
       first check its value correspond to a date'aaaa/mm/dd'.
       If it does so, then the value of the element having the rel 'my_date'
       will see it's value updated with 'mm-dd-aaaa'.
     */
    $.substitutions_fields_to_editor = function(mapping, options) {
	options = sub_treat_options(options);

	$(iframe_id).live('mouseover', function() {SUBSTITUTIONS_IN_EDITOR = true;})
	$(iframe_id).live('mouseout', function() {SUBSTITUTIONS_IN_EDITOR = false;})	

	/* We transform every fields in the mapping into a 
	   custom field.
	 */
	for (f_id in mapping) {
	    bind_field_change({field_id: f_id, sub_rel: mapping[f_id]})
	}

	/* Bindings for custom fields */
	if (typeof(options.custom_fields) != 'undefined') {
	    for (i = 0; i < options.custom_fields.length; i++) {
		bind_field_change(options.custom_fields[i]);
	    }
	}
    }

    /* $.substitutions_editor_to_field(mapping, options)
       This method makes changes in the editor appear in the fields.

       mapping is a dictionnary, where the keys are the 'rel' defined for the element and
       the associated values are the id of the field to update.

       option defines the options for the function. The only option added to the default
       ones is 'custom_substitutions'.
       The custom_substitutions is a list of fields. Each field is now defined as follow:
       - sub_rel: the rel attribute value of the element that contains the value to change
         in the fields.
       - regexp: a regular expression to validate the field.
       - validation_msg: a message displayed when the text entered do not match the regexp.
       - fields: a list containing dictionnaries with two key:
         - field_id: the id of the field that gets the value
	 - regexp_replace: a regular expression for replacing the values in the field.

       An example (the backward example given for the previous method)
       cf = {sub_rel: 'my_date',
	     regexp: '(\d{2})-(\d{2})-(\d{4})(.*)',
	     validation_msg: 'This is not a valid date',
	     fields: [{field_id: '#my_calendar',
	               regexp_replace: '$5/$3/$1'},
		      {field_id: '#my_calendar_day',
		       regexp_replace: '$1'},
		      {field_id: '#my_calendar_month',
		       regexp_replace: '$3'},
		      {field_id: '#my_calendar_year',
		       regexp_replace: '$5'}]}

       With this example, when the user set the value '03-08-1981'
       in the element having the rel 'my_date', four fields are updated:
        - #my_calendar receives the value 1981/08/03
	- #my_calendar_day receives the value 03
	- #my_calendar_month receives the value 08
	- #my_calendar_year receives the value 1981

     */
    $.substitutions_editor_to_fields = function(mapping, options) {
	options = sub_treat_options(options);

	/* We translate the mapping into custom fields */
	if (typeof(options.custom_substitutions) == 'undefined') {
	    options.custom_substitutions = [];
	}

	for (rel in mapping) {
	    cf = {sub_rel: rel,
	          regexp: '(.*)',
	          fields: [{field_id: mapping[rel],
			    regexp_replace: '$1'}]}
	    options.custom_substitutions[options.custom_substitutions.length] = cf;
	}

	/* When the content of the editor is changed, we update the fields. */	
	$(iframe_id).contents().find('body').pingchange('innerHTML', function() {
		for (i = 0; i < options.custom_substitutions.length; i++) {
		    sub = options.custom_substitutions[i];

		    substitution = find_substitution(sub.sub_rel);
		    value = substitution.html();
		    reg = new RegExp(sub.regexp, "g");

		    if (reg.exec(value) == null) {
			debug('No match found for ' + sub.regexp + ' on string ' + value);
			add_error(substitution, sub.validation_msg);
			continue;
		    }

		    if (value == null) {
			debug('Value is null for substitution ' + sub.sub_rel);
			continue;
		    }

		    remove_error(substitution);
		    for (j = 0; j < sub.fields.length; j++) {
			field = sub.fields[j];


			/* Yes this switch looks weird for a boolean result, but we can
			   also obtain 'null' as a result when the value set is the original
			   one.
			*/
			set_result = set_value($(field.field_id)[0], 
					       value.replace(reg, field.regexp_replace));

			switch (set_result) {
			case false:
			    add_error(substitution,
				      make_error_msg_from_field($(field.field_id)[0]));
			    break;
			case true:
			    remove_error($(field.field_id));
			    animate_field($(field.field_id));
			    break;
			}
		    }
		}
	    });
    }

    /* $.substitutions(mapping, options)
       This method is the one you should call if you want to have substitutions from
       and to the editor (changing the fields change the text in the editor, changing 
       the text in the editor updates the fields).

       Mapping is a dictionnary: the keys are the ids of the fields and corresponding
       'rel' values given to the substitution elements.

       Options is also a dictionnary, here's the list of keys you can define (extending
       the ones defined in the header of this file):
	- custom_fields: a list of custom fields (see substitution_fields_to_editor).
	- custom_substitutions: a list of custom_substitutions (see substitutions_editor_to_fields).
     */
    $.substitutions = function(mapping, options) {
	options = sub_treat_options(options);

	/* We do not use mappings here, we'll directly translate it into
	   custom fields/substitutions */
	/* We translate the mapping into custom fields */
	if (typeof(options.custom_substitutions) == 'undefined') {
	    options.custom_substitutions = [];
	}

	if (typeof(options.custom_fields) == 'undefined') {
	    options.custom_fields = [];
	}

	for (f_id in mapping) {
	    /* We create the custom field */
	    cf = {field_id: f_id,
		  sub_rel: mapping[f_id]}

	    options.custom_fields[options.custom_fields.length] = cf;

	    /* And the custom substitution */
	    cs = {sub_rel: mapping[f_id],
	          regexp: '(.*)',
	          fields: [{field_id: f_id,
			    regexp_replace: '$1'}]}

	    options.custom_substitutions[options.custom_substitutions.length] = cs;
	}

	/* Now we'll do the bindings when the editor is ready */
	$('body').pingchange('innerHTML', function() {
		/* Checks if the substitution has already been done. */
		if (SUBSTITUTIONS_EDITOR_READY) {
		    return;
		}

		/* Checks if the iframe is there. */
		if ($(iframe_id).length == 0) {
		    return;
		}

		SUBSTITUTIONS_EDITOR_READY = true;

		$.substitutions_fields_to_editor({}, options);
		$.substitutions_editor_to_fields({}, options);
	    });
    }


    })(jQuery);


jq(document).ready(function() {
	/* Remove all KSS stuff */
	jq('.field').each(function (){
		cl = jq(this).attr('class');
		reg = new RegExp('(.*) (kssattr-atfieldname-[a-z]*)( (.*))?');
		if (reg.exec(cl) == null) {
		    return;
		}
		jq(this).removeClass(cl.replace(reg, '$2'));
	    });

	find_field = function(el) {
	    field_div = null;
	    if (el.parent().hasClass('field')) {
		field_div = el.parent();
	    }
	    else if (el.parent().parent().hasClass('field')) {
		/* Calendar fields are in a sub div.*/
		field_div = el.parent().parent();
	    }
	    else if (el.parent().parent().parent().hasClass('field')) {
		/* Calendar fields are in a sub div.*/
		field_div = el.parent().parent().parent();
	    }

	    return field_div;
	}

	my_add_error = function(el, msg) {
	    field_div = find_field(el);
	    
	    if (field_div == null) {
		el.addClass('wrong_substitution');
		if (typeof(msg) == 'string') {
		    el.attr('title', msg);
		}
	    }
	    else {
		field_div.addClass('error');
		if (typeof(msg) == 'string') {
		    field_div.find('.fieldErrorBox').html('<p class="error_msg">' + msg + '</p>');
		}			
	    }
	}

	my_remove_error = function(el) {
	    field_div = find_field(el);

	    if (field_div == null) {
		el.removeClass('wrong_substitution');
		el.removeAttr('title');
	    }
	    else {
		field_div.removeClass('error');
		field_div.find('.fieldErrorBox').empty();
	    }
	}


	/* We do not use those function as animating has no sens for contracts, fields and
	   editor are on two different tabs */
										 
// 	animated = {}

// 	my_animate_field = function(f) {
// 	    field_div = find_field(f);
// 	    if (field_div == null) {
// 		/* Should not happen */
// 		return;
// 	    }

// 	    uid = make_uid(field_div);
// 	    if (typeof(animated[uid]) == 'undefined') {
// 		animated[uid] = true;
// 	    }
// 	    else {
// 		if (animated[uid]) {
// 		    return;
// 		}
// 	    }

// 	    old_style = field_div.attr('style');
// 	    if (typeof(old_style) == 'undefined') {
// 		old_style = '';
// 	    }
// 	    field_div.css({'backgroundColor': '#e0cf17',
// 		       'border': '1px solid #9f941d'})
	    
// 	    field_div.animate({'backgroundColor': '#FFFFFF',
// 			       'borderTopColor': '#FFFFFF',
// 			       'borderRightColor': '#FFFFFF',
// 			       'borderBottomColor': '#FFFFFF',
// 			       'borderLeftColor': '#FFFFFF'},
// 			      10,
// 			      function() {
// 				  animated[uid] = false;
// 				  console.log('Reseting style with ' + old_style);
// 				  jq(this).attr('style', old_style);
// 			      })
// 	}

	int_regexp = '^([1-9]\\\d*)';
	number_regexp = '^(0(\\\.|,)\\\d+|([1-9]\\\d*((\\\.|,)\\\d+)?))';

	int_error_msg = ajax_translate('label_int_error_msg');
	number_error_msg = ajax_translate('label_number_error_msg');
	date_error_msg = ajax_translate('label_date_error_msg');
	default_error_msg = ajax_translate('label_default_error_msg');

	jq.substitutions({'#trialPeriod': '[contract_trial_period]',
		    '#employmentType': '[contract_part_full_time]',
		    '#function': '[contract_function]'
		    },
	    {debug_mode: false,
		    default_error_msg: default_error_msg,
		    add_error: my_add_error,
		    remove_error: my_remove_error,
		    animate_field: function(f) {},
		    animate_substitution: function(sub) {},
		    animation_color: '#e0cf17',
		    custom_fields: [
				    {field_id: '#wage',
					    sub_rel: '[contract_wage]',
					    regexp: number_regexp + '$',
					    validation_msg: int_error_msg,
					    regexp_replace: '$1'},
				    {field_id: '#duration',
					    sub_rel: '[contract_duration]',
					    regexp: int_regexp + '$',
					    validation_msg: int_error_msg,
					    regexp_replace: '$1'},
				    {field_id: '#daysPerWeek',
					    sub_rel: '[contract_days_per_week]',
					    regexp: number_regexp + '$',
					    validation_msg: number_error_msg,
					    regexp_replace: '$1'},
				    {field_id: '#hours',
					    sub_rel: '[contract_hours_per_week]',
					    regexp: number_regexp + '$',
					    validation_msg: number_error_msg,
					    regexp_replace: '$1'},
				    {field_id: '#edit_form_startdate_0',
					    sub_rel: '[contract_startdate]',
					    regexp: '^(\\\d{4})(/|-)(\\\d{2})(/|-)(\\\d{2})(.*)',
					    validation_msg: date_error_msg,
					    regexp_replace: '$5-$3-$1'}],
		    custom_substitutions: [
					   {sub_rel: '[contract_wage]',
						   regexp: number_regexp + '(.*)',
						   validation_msg: number_error_msg,
						   fields: [{field_id: '#wage', regexp_replace: '$1'}]},
					   {sub_rel: '[contract_duration]',
						   regexp: int_regexp + '(.*)',
						   validation_msg: int_error_msg,
						   fields: [{field_id: '#duration', regexp_replace: '$1'}]},
					   {sub_rel: '[contract_days_per_week]',
						   regexp: number_regexp + '(.*)',
						   validation_msg: number_error_msg,
						   fields: [{field_id: '#daysPerWeek', regexp_replace: '$1'}]},
					   {sub_rel: '[contract_hours_per_week]',
						   regexp: number_regexp + '(.*)',
						   validation_msg: number_error_msg,
						   fields: [{field_id: '#hours', regexp_replace: '$1'}]},
					   {sub_rel: '[contract_startdate]',
						   regexp: '^(\\\d{2})-(\\\d{2})-(\\\d{4})(.*)',
						   validation_msg: date_error_msg,
						   fields: [{field_id: '#edit_form_startdate_0', regexp_replace: '$3/$2/$1'},
							    {field_id: '#edit_form_startdate_0_day', regexp_replace: '$1'},
							    {field_id: '#edit_form_startdate_0_month', regexp_replace: '$2'},
							    {field_id: '#edit_form_startdate_0_year', regexp_replace: '$3'}]}
					   ]
		    }
	    );
    })

