/**
 * @abstract
 * Provides extensive utilities for working with ELO-forms.
 *
  *
   * Using FormWrapper's simple state-manager, you can define
   * application states consisting of fields and their respective:
   *
   *  - values
   *  - labels
   *  - tooltips
   *  - attributes (readonly / hidden / optional)
   *  - responders:
   *    a responder is a function which will get called when the field's value is changed
   *  - validators:
   *    a validator is a function which will get called when a field is clicked. Afterwards
   *    the validation massage is shown in the client.
   *
   *  There is also support for behaviour templates which provide common behaviours to a group of fields.
   *  You can use templates defined in FormWrapper.Templates (end of this file)
   *
   * # Setup
   *
   *  The Wrapper must be initialized in a form's `Header.txt`.
   *  e.g.:
   *
   *     <script type="text/javascript" src="lib_sol.common.forms.FormWrapper"></script>
   *     <script type="text/javascript" src="lib_sol.hr.forms.PersonnelFile"></script>
   *
   *  `lib_sol.hr.forms.PersonnelFile` will be a separate class extending
   *  the form-wrapper by the customer specific logic. Therefore, also add these entries to your `Header.txt`:
   *
   *     // ELO < 10.1 does not support onInit here. Just add these lines to inputChanged source == null instead.
   *     function onInit() {
   *       this.form = sol.create("sol.hr.forms.PersonnelFile");
   *       this.form.OnInitAndTabChange();
   *     }
   *
   *     function tabChanged(id) {
   *       this.form.TabChanged(id);
   *     }
   *
   *     function inputChanged(source) {
   *       source == null ?
   *         window.setTimeout(function() { sol.common.forms.Utils.initializeForm(); }, 100)
   *         : this.form.InputChanged(source);
   *     }
   *
   *     function nextClicked(id) {
   *       return (
   *         sol.common.forms.Utils.disableCancelButtonValidation(id, ["sol.common.wf.node.cancel"])
   *         || this.form.OnSave()
   *       );
   *     }
   *
   *     function saveClicked() {
   *       return this.form.OnSave();
   *     }
   *
   *     function addLineClicked(addLineId, groupIndex) {
   *       this.form.OnLineChange(true, addLineId, groupIndex);
   *     }
   *
   *     function removeLineClicked(addLineId, groupIndex) {
   *       return this.form.OnLineChange(true, addLineId, groupIndex);
   *     }
   *
   *     function onDynListItemSelected(item, inputName) {
   *       this.form.InputChanged(item, inputName);
   *     }
   *
   *  By defining all customer specific logic in the extending class, your `Header.txt` will be much cleaner.
   *  It is recommended not to call any additional customer specific util-functions in `Header.txt`. Instead, you
   *  should `include` your customer-specific logic solely in the extending class.
   *
   *  You can define and use multiple extending classes in one Header.txt.
   *
   *  Minimal example for an extending class `PersonnelFile`:
   *
   *     sol.define("sol.hr.forms.PersonnelFile", {
   *       extend: "sol.common.forms.FormWrapper",
   *       prefix: "HR_PERSONNEL"  // usually all customer-specific fields have some kind of prefix. You can define it here.
   *     }
   *
   * # Usage
   *  The wrapper functions are now available in the form. These are some common use-cases:
   *
   * ### Field functions (this.form.fields.FIELDNAME)
   * Every field on the form (MAP,GRP) is represented as an object.
   *    .value(opts)-   returns the field's value.
   *                    (isodate for date-fields and the key for localized Kwls).
   *                    pass `{full: true}` to receive the localized value of an lKwl instead of key only
   *                    pass `{asNumber: true}` to receive the value as a number. (e.g. valuable for localized number strings)
   *
   *    .set(val)   -   replaces the field's value by `val`
   *                    (for val, pass an isodate date-fields and for localizedKwls pass the key)
   *
   *    .element()  -   returns the field's HTML element
   *
   *    .show()     -   displays the field in the form
   *                    (will also show the field's label and selector if called with .(true, true))
   *
   *    .hide()     -   hides the field
   *                    (will also hide the field's label and selector if called with .(true, true))
   *
   *    .setAttribute("readonly", true)
   *                -   renders a field readonly (pass false to make it writeable again)
   *
   *    .setAttribute("optional", true)
   *                -   makes a field optional. Pass false to make a field required
   *
   *    .tooltip()
   *                -   if a tooltip is defined, returns the tooltip as a String
   *
   *    .writeTooltip(tooltip)
   *                -   Adds a tooltip to the field
   *                -   removes tooltip if falsy value is passed
   *
   *    .setImage(guid)   -   only works on image-fields! Downloads an image specified by guid and sets the image-fields src-attribute
   *                         if no guid is passed, but the sord contains a guid as the fields value, it will be used as a fallback
   *
   *  show and hide of labels will only work, if you assign the label's "variable" field a value of "LBL_" + `Fieldname without IX_MAP/GRP`
   *  and the label's respective input-field's variable field contains the prefix defined in the extending class definition (e.g. HR_PERSONNEL)
   *
   * ### Form functions (this.form)
   *
   *    .setState(statename)
   *                -   transforms the form according to the state-definition
   *
   *    .activeState
   *                -   stores the currently active state
   *
   *    .getActiveTabId()
   *                -   returns the id of the currently active tab (e.g. "_501_time_phases")
   *
   *    .today()    -   returns the current client date as an isodate
   *
   *    .logFieldValues(b) -   logs all fieldnames and their values to the console (as a table if implemented). pass true as parameter to return fields and values as an array instead of a console-log
   *
   *
   * ### Tabs functions (this.form.tabs)
   *    ._tabName  -   returns the tab object of the tab "tabName"
   *
   *    .activeTab  -   returns the tab object of the active tab
   *
   *    .all  - holds an object containing all templates which are available on the form
   *
   * ### Tab functions (this.form.tabs._tabName)
   *    .parts      -   holds an object containing the tab's templates (called "parts" in this framework)
   *
   *    .parts._partName - holds the part-object having the name "partName"
   *
   *    .containsCover  - boolean. does the tab contain a business solution coversheet? (__cover)
   *
   *    .hidePartsContainingOnlyEmptyFields() - hides all parts of a tab which where none of each part's fields has a value.
   *
   *    .hideUnnecessaryParts(array) - hides all parts which do not contain any of the fields listed in the passed array
   *
   *
   * ### Template/Part functions (this.form.tabs_tabName.parts._partName)
   *    .show()     -   shows the template/part
   *
   *    .hide()     -   hides the template/part
   *
   *    .isCover    -   is the part a business solution coversheet? (__cover)
   *
   *
   * # States:
   *  The class extending sol.common.forms.FormWrapper can define multiple states, which can be
   *  set e.g. on input-changed events (responders). Usually, states are changed in "responder"-functions or
   *  the "BeforeOnInitAndTabChange" function, which will be called once during initialization
   *  of the wrapper and again on every tab-change. "AfterOnInitAndTabChange" is called
   *  after all states' "OnInitAndTabChange"-functions were called. (see e.g. templates, which
   *  define their own OnInitAndTabChange function).
   *  There is also "BeforeInputChanged" and "AfterInputChanged", which will be called
   *  before/after the changed field's responder has been executed.
   *
   *
   *
   * ### Order of computation
   *     (# means, you can define this function in the extending class)
   *     initialize: called during sol.create
   *       #OnInit
   *     OnInitAndTabChange: called when initializing form and on a tab change
   *       #BeforeOnInitAndTabChange
   *       Calls all OnInitAndTabChange functions which are defined in templates or states. (#template.OnInitAndTabChange and #state.OnInitAndTabChange)
   *       #AfterOnInitAndTabChange
   *     InputChanged: called when an input field´s value changes
   *       #BeforeInputChanged
   *       Calls the changed field´s #responder function
   *       #AfterInputChanged
   *     OnSave: called when "save" is clicked
   *       #BeforeOnSave
   *       Calls all rules defined in #onSaveRules
   *       #AfterOnSave
   *
   *
   *
   * ### Exemplary Formwrapper extending class:
   *     sol.define("sol.hr.forms.PersonnelFile", {
   *       extend: "sol.common.forms.FormWrapper",
   *       prefix: "HR_PERSONNEL",   // IX_MAP_HR_PERSONNEL_DATEOFJOINING -> Prefix = HR_PERSONNEL
   *       defaultState: "myDefaultState",
   *       //states begin
   *       states: {
   *         stateInit: {  //stateInit is called automatically when the form is initially loaded
   *           //declare your desired initial state here (usually responders and validators)
   *           fieldProperties: {
   *             IX_MAP_DURATION_TYPE: {
   *               responder: function(form, state, field, value) {
   *                 if (value == "") { form.setState("myDefaultState")
   *                 } else { form.setState("durationTypeSelected") };
   *               }
   *             }
   *           }
   *         },
   *         myDefaultState: {
   *           fieldProperties: {
   *             IX_MAP_HR_PERSONNEL_DEPARTMENT: {
   *               hidden: true
   *             }
   *           }
   *         },
   *         durationTypeSelected: {
   *           fieldProperties: {
   *             IX_MAP_HR_PERSONNEL_DEPARTMENT: {
   *               value: "Sales"
   *               hidden: false
   *             }
   *           }
   *         },
   *         departmentHasAValue: {
   *           fieldProperties: { IX_MAP_HR_PERSONNEL_DEPARTMENT: { hidden: false } }
   *         },
   *       }
   *     }
   *
   * ### Explanation
   *
   *  First, during initialization, the stateInit state is set. Here, we assign a responder
   *  function to the field "IX_MAP_DURATION_TYPE", which will set the state "durationTypeSelected"
   *  as soon as the field has any value.
   *
   *  The "myDefaultState" is set directly after "stateInit", because we defined it as me.defaultState
   *  In "myDefaultState", the field IX_MAP_HR_PERSONNEL_DEPARTMENT will be hidden.
   *
   *  If someone enters a value into the "IX_MAP_DURATION_TYPE" field, its responder will be executed.
   *  The responder sets the state "durationTypeSelected", which sets the DEPARTMENT-field's value to
   *  "Sales" and unhides/shows the field in the form.
   *
   *  Validators:
   *  You can define Validators in the fieldProperties of a field by assigning a "validator:" property.
   *  Tip: You don't have to define the validation function ("JS_VAL_...") in the form designer anymore!
   *
   *  BodyClasses:
   *  You can define CSS-Bodyclasses in every state:
   *  myDefaultState: { bodyClasses: ["greatDesign", "flexibleTable"] }
   *  Attention: body classes will be removed, as soon as you execute "setState" for another state.
   *  To keep body-classes during state-change, you must define the property e.g.
   *  "myDefaultState.removePreviousBodyClasses: false" in subsequent states
   *
   *  Template:
   *  To use a template, you have to define it instead of (or alongside) a field's properties.
   *  e.g.
   *
   *     ...
   *     fieldProperties: {
   *       IX_MAP_HR_PERSONNEL_CASSATIONTOGGLE: {
   *         template: "toggle"
   *         config: { ... } //see documentation at the end of this file for template configs.
   *       }
   *     }
   *     ...
   *
   *  Batchprocessing:
   *  It is tedious to define field:{hidden: true} if you want to hide many fields at once.
   *  You can use all field properties (hidden, readonly, optional, value, responder, ...) in batch
   *  processing. Example:
   *
   *     fieldProperties: {
   *       _batch: {
   *         hidden: {
   *           val: true,
   *           fields: [
   *             "IX_GRP_HR_PERSONNEL_DATEOFJOINING", "IX_MAP_HR_PERSONNEL_PROBATIONARYPERIODDURATION"
   *           ]
   *         }
   *       }
   *     }
   *
   *  Of course you can combine batch-processing and ´normal´ attribute setting.
   *
   *  If you want to define your own templates, you can do so in the "OnInit" function.
   *  Just add your own templates to form.templates. E.g.:
   *
   *     form.templates.mygreattemplate = function (config) { ...  return configuredTemplate; };
   *
   * ### Conclusion
   *
   *  OnInitAndTabChange should be used to set up the environment for the user.
   *  (what happens if the user switches tabs,... )
   *
   *  Any other state should be defined as ´state´.
   *  If you need more control than the standard field properties (value, hide,...) can offer,
   *  you can and should implement the logic directly in a field's responder instead of creating
   *  a state which won't have sufficient facilities to represent the desired state anyways.
   *
   *
   * # Additional features
   * ### Business Solution Coversheet
   *  The Business Solution Coversheet provides an easy way to display information in a form as readonly-labels
   *  instead of input boxes.
   *
   *  To display any MAP/GRP field in the coversheet, prepend "VIEW_" to its name when defining it in the coversheet
   *  template. e.g. IX_MAP_TEST -> VIEW_IX_MAP_TEST
   *
   *  The template containing the coversheet must be named ending with "__cover" (two underscores)
   *
   *  The required CSS rules will then be applied automatically.
   *
 * @author ESt, ELO Digital Office GmbH
 *
 * @elowf
 * @requires sol.common.DateUtils
 */
sol.define("sol.common.forms.FormWrapper", {

  /**
   * will contain all fields of the form after initializeFields has been called
   */
  fields: {},

  /**
   * will contain all tabs of the form after initializeTabs has been called
   */
  tabs: {
    all: {
      parts: {}
    }
  },

  /**
   * will contain rules, which are applied when a user presses the save button
   */
  onSaveRules: {},

  /**
   * will contain all template OnInitAndTabChange functions after setState has been called
   */
  onInitAndTabChangeFunctions: [],

  /**
   * holds the active state's name as a string
   */
  activeState: undefined,

  /**
   * true if the form is running in workflow mode. (set in initialize)
   */
  workflowActive: false,

  /**
   * Version of ELO-wf parsed to an Integer. (10.04.032 = 1004032)
   */
  wfVersion: 0,

  /**
   * Name of ELO-wf (e.g. wf-Solutions)
   */
  wfName: "",

  /**
   *  logs all fieldnames and their values to the console (as a table if implemented in the JS-Engine).
   *  @param {Boolean} nolog returns fields and their values as an array instead of doing a console-log
   *  @returns {[{fieldName: String, fieldValue: String}] || undefined}
   */
  logFieldValues: function (nolog) {
    var me = this;
    return (function (keys) {
      return (nolog && keys) || (console.table ? console.table(keys) : console.log(keys));
    })(
      Object.keys(me.fields)
      .sort(function (a, b) {
        return (a).localeCompare(b);
      })
      .map(function (field) {
        return {
          fieldName: field,
          fieldValue: me.fields[field].value()
        };
      })
    );
  },

  /**
   * returns true if the passed string `str` ends with passed string `needle`, otherwise false
   */
  endsWith: function(str, needle) {
    return (typeof str === "string" && typeof needle === "string")
      && (str.lastIndexOf(needle) + needle.length === str.length);
  },

  /**
   * returns true if the running WF's version is >= the passed `requiredVersion`
   * requiredVersion (String)
   *  e.g. "10.02.000"
   */
  wfMeetsVersionRequirement: function (requiredVersion) {
    var me = this;
    return (typeof requiredVersion === "string" ? +(requiredVersion.replace(/\./g, "", true)) : 0) <= me.wfVersion;
  },

  /**
   * determines the active tab's id e.g. "_510_time_details" and returns it
   * @returns {String}
   */
  getActiveTabId: function () {
    var activeTab = document.getElementsByClassName("selected");
    if (activeTab.length > 0) {
      return activeTab[0].hash && activeTab[0].hash.replace(/^#/, "_") || "none";
    } else {
      return "none";
    }
  },

  /**
   * sets a state from the "states"-object by name
   * @param {String} stateName name of the state as defined in the "states"-object
   */
  setState: function (stateName) {
    var me = this,
        state = me.states[stateName],
        delOldClasses;

    if (state === undefined) {
      return;
    }
    delOldClasses = state.removePreviousBodyClasses;
    // default is to delete old classes
    (delOldClasses === undefined || delOldClasses) && (me.activeState != undefined) && me.removeOldBodyClasses();

    me.setNewBodyClasses(state.bodyClasses);
    me.setFieldProperties(state.fieldProperties);
    me.setTabProperties(state.tabProperties);
    state.reassignlkwls && me.reassignLocalizedKwlValues({
      force: true
    });
    me.assignValidators(state);
    me.assignValidationFunctionsToWindow();
    me.assignResponders(state);

    if (state.OnInitAndTabChange && !(state.OnInitAndTabChange in me.onInitAndTabChangeFunctions)) {
      me.onInitAndTabChangeFunctions.push(state.OnInitAndTabChange);
    }

    me.activeState = stateName;
  },

  /**
   * sets localizedKwl keys again (e.g. to receive translated text)
   * this is usually run once during initialization, but can be called manually at any time
   * @param {force: Boolean} opts if defined, forces a reassignment of all localizedKwl values.
   *                              Usually, only incomplete localizedKwl values are reassigned
   */
  reassignLocalizedKwlValues: function (opts) {
    var me = this;
    for (var fieldname in me.fields) {
      var field = me.fields[fieldname],
          fValue = field.value({
            full: true
          });
      if (field.isLocalized) {
        field.localizedDynKwlName
        && (
            fValue
            && (
              (opts && opts.force) ||
              (fValue.indexOf(" - ") === -1) ||
              (fValue.search(/\w+ - ?$/g) === 0)
            )
        ) && field.set(field.value());
      }
    }
  },


  /**
   * removes all bodyClasses which were set by the previous state
   */
  removeOldBodyClasses: function () {
    var me = this;
    me.removeBodyClasses(me.states[me.activeState].bodyClasses);
  },

  /**
   * removes bodyclasses
   * @private
   */
  removeBodyClasses: function (bodyClasses) {
    var me = this;
    bodyClasses && bodyClasses.forEach(function (bc) {
      me.removeBodyClass(bc);
    });
  },

  /**
   * @private
   */
  setNewBodyClasses: function (bodyClasses) {
    var me = this;
    me.setBodyClasses(bodyClasses);
  },

  /**
   * @private
   */
  setBodyClasses: function (bodyClasses) {
    var me = this;
    bodyClasses && bodyClasses.forEach(function (bc) {
      me.addBodyClass(bc);
    });
  },

  /**
   * determines the current day as isoDate using the beginning of the day as time and returns it
   * @returns {Integer (isodate)}
   */
  today: function () {
    return sol.common.DateUtils.dateToIso(new Date(), {
      startOfDay: true
    });
  },

  /**
   * iterates over the fieldProperties defined in a state and assigns these properties
   * to the respective field
   * @private
   */
  setFieldProperties: function (fieldProperties) {
    var me = this;
    for (var fieldName in fieldProperties) {
      if (fieldName == "_batch") {
        me.batchProcessProperties(fieldProperties[fieldName]);
      } else {
        me.processAFieldsProperties(fieldName, fieldProperties[fieldName]);
      }
    }
  },

  /**
   * iterates over the batchProperties defined in a state and performs the batch-tasks accordingly
   * @private
   */
  batchProcessProperties: function (batchProperties) {
    var me = this,
        propsAsArray = [];
    if (!(batchProperties instanceof Array)) {
      for (var key in batchProperties) {
        propsAsArray.push(batchProperties[key]);
        batchProperties = propsAsArray;
      }
    }
    try {
      batchProperties.forEach(function (o) {
        for (var prop in o) {
          var fields = o[prop].fields,
            value = o[prop].val,
            property = {},
            noLog;
          property[prop] = value;

          if (fields === "all") {
            noLog = true;
            fields = Object.keys(me.fields);
          }
          fields.forEach(function (name) {
            me.processAFieldsProperties(name, property, noLog);
          });
        }
      });
    } catch (e) {
      console.log("_batch property not defined correctly:", e);
    }
  },

  /**
   * used in setFieldProperties to map the defined properties to functions
   * @private
   */
  processAFieldsProperties: function (fieldName, aFieldsProperties, noLog) {
    var me = this, workflowOnlyModes = Array.isArray(aFieldsProperties["workflowOnly"]) && aFieldsProperties["workflowOnly"];
    for (var option in aFieldsProperties) {
      var optionValue = aFieldsProperties[option],
          overwrite = aFieldsProperties["overwrite"],
          field = me.fields[fieldName];
      if (!field || !field.element()) {
        noLog || console.log("Field '" + fieldName + "' does not exist! Skipping ...");
        return;
      }

      if (!me.workflowActive && (workflowOnlyModes && (workflowOnlyModes.indexOf(option) > -1))) {
        noLog || console.log("Rule '" + option + "' for '" + fieldName + "' only active in workflows! Skipping ...");
        continue;
      }

      switch (option) {
        case "value":
          (overwrite && field.set(optionValue)) || (!field.value() && field.set(optionValue));
          break;
        case "readonly":
          field.setAttribute(option, optionValue);
          break;
        case "optional":
          field.setAttribute(option, optionValue);
          break;
        case "hidden":
          field.setAttribute(option, optionValue);
          break;
        case "tooltip":
          field.writeTooltip(optionValue);
          break;
        case "template":
          me.applyTemplate(optionValue);
          field.template = optionValue; // it's nice to have access to the assigned template later...
          break;
        default:
          break;
      }
    }
  },

  /**
   * iterates over the tabProperties defined in a state and assigns these properties
   * to the respective tab. However, you should not try to assign properties to tabs
   * but to parts. (tab names can change too easily which winds up to orphaned rules)
   * @private
   */
  setTabProperties: function (tabProperties) {
    var me = this;
    for (var tabId in tabProperties) {
      //if (tabId == "_batch") {
      //me.batchProcessProperties(tabProperties[fieldName]); // not implemented yet
      //} else { no more else, see description }
      for (var partId in tabProperties[tabId]) {
        me.processAPartsProperties(partId, tabId, tabProperties[tabId][partId]);
      }
    }
  },

  /**
   * used in setTabProperties to map the defined properties to functions
   * @private
   */
  processAPartsProperties: function (partId, tabId, aPartsProperties) {
    var me = this;
    for (var option in aPartsProperties) {
      var optionValue = aPartsProperties[option],
          tab = me.tabs[tabId],
          part;

      if (!tab) {
        console.log("Tab '" + tabId + "' does not exist! Skipping ...");
        return;
      }

      part = tab.parts[partId];
      if (!part) {
        console.log("Formpart '" + partId + "' does not exist! Skipping ...");
        return;
      }
      switch (option) {
        case "hidden":
          part.setAttribute(option, optionValue);
          break;
        case "template":
          me.applyTemplate(optionValue);
          part.template = optionValue; // it's nice to have access to the assigned template later...
          break;
        default:
          break;
      }
    }
  },

  /**
   * renders a template using the passed config and adds it to the statemanager
   * @private
   */
  applyTemplate: function (templateObject) {
    var me = this,
        tName = templateObject.name,
        tConfig = templateObject.config,
        template = me.templates[tName],
        result;

    result = template.call(me, tConfig);
    me.addTemplateStatesToStates(result.states);

    if (result.OnInitAndTabChange && !(result.OnInitAndTabChange in me.onInitAndTabChangeFunctions)) {
      me.onInitAndTabChangeFunctions.push(result.OnInitAndTabChange);
    }

    if (result.OnSaveRule) {
      me.onSaveRules[result.OnSaveRule.name] = result.OnSaveRule.rule;
    }

    if (result.GlobalFunctions) {
      for (var functionName in result.GlobalFunctions) {
        window[functionName] = result.GlobalFunctions[functionName];
      }
    }
  },

  /**
   * adds states to the statemanager
   * @private
   */
  addTemplateStatesToStates: function (templateStates) {
    var me = this;
    for (var stateName in templateStates) {
      var state = templateStates[stateName];
      if (!state) {
        continue;
      }
      if (me.states[stateName] === undefined) {
        me.states[stateName] = {
          fieldProperties: {}
        };
      }
      for (var fieldName in state.fieldProperties) {
        var field = state.fieldProperties[fieldName];
        if (me.states[stateName].fieldProperties[fieldName] === undefined) {
          me.states[stateName].fieldProperties[fieldName] = {};
        }
        if (me.states[stateName].fieldProperties === undefined) {
          me.states[stateName].fieldProperties = {};
        }
        for (var propName in field) {
          var fieldInState = me.states[stateName].fieldProperties[fieldName];
          fieldInState[propName] = field[propName];
        }
      }
      for (var tabId in state.tabProperties) {
        var tab = state.tabProperties[tabId];
        if (me.states[stateName].tabProperties === undefined) {
          me.states[stateName].tabProperties = {};
        }
        if (me.states[stateName].tabProperties[tabId] === undefined) {
          me.states[stateName].tabProperties[tabId] = {};
        }
        for (var propName in tab) {
          var tabInState = me.states[stateName].tabProperties[tabId];
          tabInState[propName] = tab[propName];
        }
      }
    }
  },

  removeBodyClass: sol.common.forms.Utils.removeBodyClass,
  addBodyClass: sol.common.forms.Utils.addBodyClass,

  /**
   * determines a short name of a field using the prefix e.g. HR_PERSONNEL.
   *
   * IX_MAP_HR_PERSONNEL_DEPARTMENT -> department
   * @param {String} fieldName e.g. IX_MAP_HR_PERSONNEL_DEPARTMENT
   * @param {String} prefix e.g. HR_PERSONNEL
   * @returns {String}
   */
  getShortName: function (fieldName, prefix) {
    if (fieldName.indexOf(prefix) != -1) {
      return fieldName.slice(fieldName.indexOf(prefix) + prefix.length + 1).toLowerCase();
    } else {
      var match = /IX_\w{2,5}_(.+)/g.exec(fieldName);
      if (match && match[1]) {
        return match[1].toLowerCase();
      }
    }
    return fieldName;
  },

  determineUndefinedFields: function (params) {
    var me = this;
    return Object.keys(params || {})
      .filter(function (param) {
        return me.propIsField(param) && (!$var(param));
      });
  },

  /**
   * builds the "fields" object, which is accessible via me.fields
   * using the FormWrapper.Field Class
   * @private
   */
  initializeFields: function () {
    var me = this;

    me.getNamesOfAllFieldsOnForm().forEach(function (name) {
      var field = sol.create("sol.common.forms.FormWrapper.Field", {
        fName: name,
        shortName: me.getShortName(name, me.prefix),
        validator: undefined,
        responder: undefined,
        prefix: me.prefix,
        form: me
      });
      if (field) {
        me.fields[field.fName] = field; // makes field accessible by long name
        field.viewSource = field.fName.indexOf("VIEW_") === 0 ? field.fName.replace(/^VIEW_/, "") : undefined;
      }
    });
    // if fields are views, fill them with values from their source
    for (var fieldName in me.fields) {
      var field = me.fields[fieldName];
      if (field.viewSource) {
        field.viewSource = me.fields[field.viewSource];
        field.element().classList.add("sol-coversheetfield");
        field.viewSource && field.viewSource.value() && (
          field.viewSource.isDate ?
          field.set(elo.wf.date.format(field.viewSource.value())) :
          field.element().innerHTML = (field.viewSource.value({
            localizedStringOnly: true
          }).replace(/(?:\r\n|\r|\n)/g, "<br />"))
        );
      }
    }
  },

  /**
   * determines if the passed string is a field which can be handled by the FormWrapper.
   * @returns {Boolean}
   */
  propIsField: function (prop) {
    return (
      prop.indexOf("IX_MAP_") === 0
      || prop.indexOf("WF_MAP_") === 0
      || prop.indexOf("IX_GRP_") === 0
      || prop.indexOf("VIEW_") === 0
    );
  },

  /**
   * collects all fields that may be used in the form
   * @private
   */
  getNamesOfAllFieldsOnForm: function () {
    var me = this,
        tables = document.getElementsByTagName("tbody"),
        fields = [],
        tableNo = -1;
    while (tables[++tableNo]) {
      fields = fields.concat(tables[tableNo].getAttribute("eloelems").split(","));
    }

    // find viewfields
    for (var tabName in me.tabs) {
      var tab = me.tabs[tabName];
      if (tab && tab.containsCover) {
        Array.prototype.slice.call(tab.element.querySelectorAll("[elonodename]")).filter(function (element) {
          return element.id.indexOf("VIEW_") === 0;
        }).forEach(function (field) {
          fields.push(field.id.trim());
        });
      }
    }

    for (var prop in ELO_PARAMS) {
      var me = this,
          value = ELO_PARAMS[prop];
      typeof value === "string" &&
        me.propIsField(prop) &&
        fields.push(prop);
    }

    return fields.filter(function (item, index, a) {
      return a.indexOf(item) == index;
    });
  },

  removeFromUndefinedFields: function (name) {
    var me = this, foundAt;
    ((foundAt = me.undefinedFields.indexOf(name)) > -1)
      && me.undefinedFields.splice(foundAt, 1);
  },

  /**
   * adds a new field to the "fields" object, which is accessible via me.fields
   * using the FormWrapper.Field Class
   * @private
   */
  initializeNewField: function (name) {
    var me = this,
        field;
    field = sol.create("sol.common.forms.FormWrapper.Field", {
      fName: name,
      shortName: me.getShortName(name, me.prefix),
      validator: undefined,
      responder: undefined,
      prefix: me.prefix,
      form: me
    });
    if (field) {
      me.fields[field.fName] = field; // makes field accessible by long name
      me.removeFromUndefinedFields(name);  // field has been defined on the form, so remove it from undefined fields
    }
  },

  /**
   * adds new fields (which were added to the form since initialize) to the "fields" object, which is accessible via me.fields
   * using the FormWrapper.Field Class
   */
  initializeNewFields: function () {
    var me = this;
    me.getNewFieldsOnForm().forEach(function (name) {
      var field = sol.create("sol.common.forms.FormWrapper.Field", {
        fName: name,
        shortName: me.getShortName(name, me.prefix),
        validator: undefined,
        responder: undefined,
        prefix: me.prefix,
        form: me
      });
      if (field) {
        me.fields[field.fName] = field; // makes field accessible by long name
      }
    });
  },

  /**
   * collects all fields that were added to the form since initialize
   * @private
   */
  getNewFieldsOnForm: function () {
    var me = this,
        oldFields = Object.keys(me.fields),
        newFields = [];
    for (var prop in ELO_PARAMS) {
      var value = ELO_PARAMS[prop];
      typeof value === "string" &&
        me.propIsField(prop) &&
        oldFields.indexOf(prop) === -1 && newFields.push(prop);
    }
    return newFields;
  },

  /**
   * builds the "tabs" object, which is accessible via me.tabs
   * using the FormWrapper.Tab Class
   * @private
   */
  initializeTabs: function () {
    var me = this;
    me.tabs.all = sol.create("sol.common.forms.FormWrapper.Tab", { parentForm: me, parts: {} });
    me.getElementsOfAllTabsOnForm().forEach(function (tabDiv) {
      var tab = sol.create("sol.common.forms.FormWrapper.Tab", {
        name: tabDiv.id,
        element: tabDiv,
        parentForm: me
      });
      if (tab && tab.name && tab.element) {
        tab.id = tab.name.replace(/^/, "_");
        me.tabs[tab.id] = tab; // makes tab accessible
        me.tabs[tab.id].parts = {};
        me.getPartsOfTab(tab).forEach(function (partDiv) {
          var children, part;

          children = partDiv && partDiv.id && partDiv.getAttribute && partDiv.getAttribute("eloelems");
          part = sol.create("sol.common.forms.FormWrapper.Part", {
            name: partDiv && partDiv.id,
            element: partDiv,
            childrenFields: children ? children.split(",") : []
          });
          if (part && part.name && part.element) {
            part.id = part.name.replace(/^part/, "");
            part.isCover = me.endsWith(part.id, "__cover") ? true : false;
            if (me.endsWith(part.id, "__cover")) {
              tab.element.classList.add("sol-coversheet-tab");
              tab.containsCover = true;
            }
            part.isCover && part.element.classList.add("sol-coversheet");
            if (part.id) {
              me.tabs[tab.id].parts[part.id] = part;
              me.tabs["all"].parts[part.id] = part;
            }
          }
        });
      }
    });
  },

  /**
   * determines all tab Elements
   * @returns [{} (HTML-div)]
   * @private
   */
  getElementsOfAllTabsOnForm: function () {
    var tabDivs = [],
        tabElements = document.getElementsByClassName("tabContent");

    for (var n in tabElements) {
      var tabDiv = tabElements.namedItem ? (tabElements.namedItem((!isNaN(parseFloat(n)) && isFinite(n)) ? tabElements[n].id : n)) : tabElements[n];
      tabDiv && tabDivs.push(tabDiv);
    }
    return tabDivs;
  },

  /**
   * determines all template/part elements inside tabs
   * @returns [{} (HTML-div)]
   * @private
   */
  getPartsOfTab: function (tab) {
    var partDivs = [],
      partElements = tab.element.getElementsByTagName("tbody");
    for (var n in partElements) {
      var partDiv = partElements.namedItem ? partElements.namedItem((!isNaN(parseFloat(n)) && isFinite(n)) ? partElements[n].id : n) : partElements[n];
      partDiv && partDivs.push(partDiv);
    }
    return partDivs;
  },

  /**
   * assigns the validator function to the window object, just like ELO WF does it.
   * @private
   */
  assignValidationFunctionsToWindow: function () {
    var me = this;
    for (var field in me.fields) {
      if (me.fields[field].validator || (me.fields[field].validator === false)) {
        window["JS_VAL_" + field.toUpperCase()] = me.fields[field].validator;
        me.reassignEloVerify(me.fields[field].element(), "JS_VAL_" + field.toUpperCase());
      }
    }
  },

  /**
   * used by assignValidationFunctionToWindow
   * @private
   */
  reassignEloVerify: function (element, newJsVal) {
    var eloVerify = element.getAttribute("eloverify"),
        match, newVerify = newJsVal;
    if (eloVerify) {
      if (eloVerify.indexOf("JS_VAL_") == -1) {
        newVerify = eloVerify + " " + newJsVal;

      } else {
        match = /(.*) ?JS_VAL_[^ ](.*)/g.exec(eloVerify);
        newVerify = match && match[1] + " " + newJsVal + " " + match[2];
      }
    }
    element.setAttribute("eloverify", newVerify);
  },

  /**
   * assigns props defined in a state object to the respective fields
   */
  assignPropToFields: function (state, propname) {
    var me = this;
    for (var fieldName in state.fieldProperties) {
      var field = state.fieldProperties[fieldName];
      if (me.fields[fieldName] && (field[propname] || field[propname] === false)) {
        me.fields[fieldName][propname] = field[propname];
      }
    }
  },

  /**
   * calculates a date in the future using input parameters
   * @param {Integer }isoDate         isoDate as starting point
   * @param {Integer }durationNumber  number representing e.g. weeks, days (any momentJS unit)
   * @param {String} durationUnit    momentJS unit descriptor (y, Q, M, w, d)
   * @param {String} terminationPoint    momentJS unit descriptor (y, Q, M, w, d)
   * @returns {Integer} the new date as an isoDate
   */
  calculateDate: function (isoDate, durationNumber, durationUnit, terminationPoint, offsetNumber, offsetUnit) {

    var srcDate, dstDate;

    srcDate = sol.common.DateUtils.isoToDate(String(isoDate));
    durationNumber = Number(durationNumber);
    dstDate = sol.common.DateUtils.shift(srcDate, durationNumber, {
      unit: durationUnit
    });

    dstDate = (terminationPoint && (moment(dstDate.getTime())).endOf(terminationPoint).toDate()) || dstDate;

    if (offsetNumber) {
      dstDate = sol.common.DateUtils.shift(dstDate, offsetNumber, {
        unit: offsetUnit || "d"
      });
    }

    return sol.common.DateUtils.dateToIso(dstDate, {
      startOfDay: true
    });
  },

  /**
   * see setCalculatedDate
   */
  returnCalculatedDate: function (srcDateFieldName, durationFieldName, terminationPointFieldName, dstDateFieldName, offsetNumber, offsetUnit) {
    return this.setCalculatedDate(srcDateFieldName, durationFieldName, terminationPointFieldName, dstDateFieldName, offsetNumber, offsetUnit, true);
  },

  /**
   * uses calculateDate internally to simplify setting the value to a target field
   * @param {String} srcDateFieldName    start date field name or isoDate
   * @param {String} durationFieldName   field name of a field which holds a durationNumber
   * @param {String} terminationPointFieldName    field name of a field which holds a termination point (at the end of the month, year, quarter,...)
   * @param {String} dstDateFieldName    this field will receive the return value of calculateDate
   * @param {String} offsetNumber    adjusts the calculated date by value (+-x)
   * @param {String} offsetUnit    unit for adjustment
   * @param {Boolean} onlyReturn    if true, returns the calculated value instead of setting the value
   * @returns {undefined || Integer (isoDate)}
   */
  setCalculatedDate: function (srcDateFieldName, durationFieldName, terminationPointFieldName, dstDateFieldName, offsetNumber, offsetUnit, onlyreturn) {
    var me = this,
        srcIsoDate, durationNumber, durationUnit, terminationPoint, dstIsoDate;

    if (srcDateFieldName === undefined) {
      srcIsoDate = sol.common.DateUtils.dateToIso(new Date(), {
        startOfDay: true
      }); //today
    } else if (String(srcDateFieldName) !== "" && (String(srcDateFieldName).indexOf("IX_") == -1)) {
      srcIsoDate = +(srcDateFieldName); // isoDate from parameter
    } else {
      srcIsoDate = me.fields[srcDateFieldName] && me.fields[srcDateFieldName].value(); // isoDate from Field
    }

    if (srcIsoDate && me.fields[durationFieldName]) {
      durationNumber = me.fields[durationFieldName].value() || 0;
      terminationPoint = (terminationPointFieldName && me.fields[terminationPointFieldName].value()) || "";
      durationUnit = me.fields[me.fields[durationFieldName].selector.name].value(); // B-)
      dstIsoDate = me.calculateDate(srcIsoDate, durationNumber, durationUnit, terminationPoint, offsetNumber, offsetUnit);
      if (onlyreturn) {
        return dstIsoDate;
      }
      me.fields[dstDateFieldName].set(dstIsoDate);
    }
  },

  /**
   * assigns validators of a state to the respective field
   * @private
   */
  assignValidators: function (state) {
    var me = this;
    me.assignPropToFields(state, "validator");
  },

  /**
   * assings responders of a state to the respective field
   * @private
   */
  assignResponders: function (state) {
    var me = this;
    me.assignPropToFields(state, "responder");
  },

  OnLineChange: function (lineAdded, name, index) {
    return true;
  },


  /**
   * wrapper around OnInitAndTabChange which also unhides all fields on the form
   * @private
   */
  TabChanged: function () {
    var me = this;
    for (var fieldName in me.fields) {
      if (fieldName !== "") {
        var field = me.fields[fieldName];
        field.show(true, true);
      }
    }
    for (var tabId in me.tabs) {
      if (tabId !== "") {
        var tab = me.tabs[tabId];
        for (var partId in tab.parts) {
          if (partId !== "") {
            var part = tab.parts[partId];
            part.show();
          }
        }
      }
    }

    me.OnInitAndTabChange();
  },

  /**
   * Executes registered callbacks when the tab is changed or the form is initialized.
   * @private
   */
  OnInitAndTabChange: function () {
    var me = this;
    me.tabs.activeTab = me.tabs[me.getActiveTabId()];
    if (me.tabs.activeTab === undefined) {
      me.tabs.activeTab = {};
      me.tabs.activeTab.name = "";
    }

    // can be defined in extending class
    me.BeforeOnInitAndTabChange && me.BeforeOnInitAndTabChange();

    me.onInitAndTabChangeFunctions.forEach(function (f) {
      f.call(me);
    });

    // can be defined in extending class
    me.AfterOnInitAndTabChange && me.AfterOnInitAndTabChange();
  },

  getNameOfKwlSource: function (source) {
    if (typeof source === "object" && source.$KEY && source.$VALUE) {
      return Object.keys(source).filter(function (prop) {
        return (prop !== "$KEY") && (prop !== "$VALUE");
      })[0];
    }
  },

  initializeRowFieldsOfMapTableKwl: function (kwlFields) {
    var me = this;
    kwlFields
      .forEach(function (field) {
        me.propIsField(field) && !me.fields[field] && me.initializeNewField(field);
      });
  },

  /**
   * Basically executes the responder of a field which was changed by user input.
   * It is also possible to define a BeforeInputChanged and a AfterInputChanged
   * function in the extending class to make changes before/after any field-change
   * without specifically depending on one field.
   * @private
   */
  InputChanged: function (source, name) {
    var me = this,
        fieldname, field, val;

    fieldname = source.name || name || me.getNameOfKwlSource(source);
    if (!me.fields[fieldname] && source.$KEY && source.$VALUE && ((fieldname.indexOf("IX_") !== 0) && (fieldname.indexOf("WF_") !== 0))) { //kwl
      fieldname = "IX_GRP_" + fieldname;
    }
    !me.fields[fieldname] && me.initializeNewField(fieldname);
    (!source.name && name) && me.initializeRowFieldsOfMapTableKwl(Object.keys(source || {}));
    field = me.fields[fieldname];
    val = field.value();

    // can be defined in extending class
    me.BeforeInputChanged && me.BeforeInputChanged(field);

    field.responder && field.responder(me, me.activeState, field, val);

    // can be defined in extending class
    me.AfterInputChanged && me.AfterInputChanged(field);
  },

  /**
   * @private
   * Only returns false if the rule matches and saveValues is false.
   * Also executes "registerUpdate", if defined.
   * @param {Object} rule
   * @return {Boolean}
   */
  shouldISave: function (rule) {
    var mask = rule.maskName,
        soltype = rule.solType,
        ruleActive, saveValues;
    ruleActive =
      (mask && soltype && mask === ELO_PARAMS.IX_MASKNAME && soltype === ELO_PARAMS.IX_GRP_SOL_TYPE) ||
      (mask && !soltype && mask === ELO_PARAMS.IX_MASKNAME) ||
      (!mask && soltype && soltype === ELO_PARAMS.IX_GRP_SOL_TYPE);

    if (ruleActive) {
      rule.registerUpdate && sol.common.forms.Utils.registerUpdate((typeof rule.registerUpdate === "string") ? rule.registerUpdate : null);
      saveValues = ((typeof rule.saveValues) === "function") ? rule.saveValues.call(this) : rule.saveValues;
      return (saveValues !== undefined) ? saveValues : true;
    } else {
      return true;
    }
  },

  /**
   * executes OnSaveRules and consolidates their results
   * @private
   */
  executeOnSaveRules: function () {
    var me = this,
        combinedResults = true,
        result;
    for (var ruleName in me.onSaveRules) {
      var rule = me.onSaveRules[ruleName];
      if (rule) {
        result = me.shouldISave(rule);
        if (combinedResults) {
          combinedResults = result;
        }
      }
    }
    return combinedResults;
  },

  /**
   * OnSave callback
   * @private
   */
  OnSave: function () {
    var me = this,
        result = true;
    me.BeforeOnSave && me.BeforeOnSave();
    result = me.executeOnSaveRules();
    me.AfterOnSave && me.AfterOnSave(result);
    return result;
  },

  /**
   * initializes the class.
   */
  initialize: function () {
    var me = this;

    me.workflowActive = ELO_PARAMS.ELO_FLOWID !== "-1";
    me.wfVersion = typeof ELOWF_VERSION === "string" ? +(ELOWF_VERSION.replace(/\./g, "", true)) : 0;
    if (window.location && typeof window.location.pathname === "string") {
      if (window.location.pathname.indexOf("plugin/de.elo.ix.plugin.proxy/wf")) {
        me.wfName = window.location.pathname.split("/");
        me.wfName = me.wfName.slice(1, me.wfName.length - 2).join("/");
      } else {
        me.wfName = window.location.pathname.split("/");
        me.wfName = me.wfName.length > 1 ? me.wfName[1] : "";
      }
    }

    me.initializeTabs();
    me.tabs.activeTab = me.tabs[me.getActiveTabId()];
    me.templates = sol.create("sol.common.forms.FormWrapper.Templates");
    me.initializeFields();
    me.undefinedFields = me.determineUndefinedFields(ELO_PARAMS); // fields which are not on the form, but in ELO_PARAMS
    me.OnInit && me.OnInit();
    me.setState("stateInit");
    me.defaultState && me.setState(me.defaultState);
    me.reassignLocalizedKwlValues();
  }
});

sol.define("sol.common.forms.FormWrapper.Tab", {
  initialize: function (config) {
    var me = this;
    me.$super("sol.Base", "initialize", [config]);
  },

  /**
  * hides all parts of a tab which where none of each part's fields has a value.
  */
  hidePartsContainingOnlyEmptyFields: function () {
    var me = this;
    Object.keys(me.parts)
    .filter(function (partName) {
      return (
        me.parts[partName].childrenFields
        .filter(function (fieldName) {
          return me.parentForm.fields[fieldName].value() != "";
        }).length === 0
      );
    })
    .forEach(function (partName) {
      me.parts[partName].hide();
    });
  },

  /**
  * hides all parts which do not contain any of the fields listed in the passed array
  */
  hideUnnecessaryParts: function (necessaryFields) {
    var me = this;
    Object.keys(me.parts)
    .filter(function (partName) {
      return (
        me.parts[partName].childrenFields
        .filter(function (fieldName) {
          return necessaryFields.indexOf(fieldName) > -1;
        }).length === 0
      );
    })
    .forEach(function (partName) {
      me.parts[partName].hide();
    });
  }
});

/**
 * Represents a form-tab's template/part
 *
 * @author ESt, ELO Digital Office GmbH
 *
   */
sol.define("sol.common.forms.FormWrapper.Part", {
  initialize: function (config) {
    var me = this;
    me.$super("sol.Base", "initialize", [config]);
  },

  /**
   * Hides the template/part
   */
  hide: function () {
    var me = this;
    me.changeVisibility(false);
  },

  /**
   * Unhides/shows the template/part
   */
  show: function () {
    var me = this;
    me.changeVisibility(true);
  },

  /**
   * sets the style attribute accordingly
   * @private
   */
  changeVisibility: function (visible) {
    var me = this;
    me.element.style.display = visible ? "" : "none";
  },

  /**
   * used by FormWrapper to map state properties to the respective functions
   * @private
   */
  setAttribute: function (attribute, value) {
    var me = this;
    switch (attribute) {
      case "hidden":
        value ? me.hide() : me.show();
        break;
      default:
        break;
    }
  }
});


/**
 * Represents a form-template's field.
 *
 * @author ESt, ELO Digital Office GmbH
 *
 * Fields are automatically linked to their label, if they are called
 *
 *     "LBL_" + me.prefix + me.getShortName();
 *
 * To make things easier, localizedKwls will automatically be linked to
 * a selector:
 *
 *     me.fName + "_UNIT"
 *
 * e.g.
 *
 *     IX_MAP_HR_PERSONNEL_DEPARTMENT
 *     label: LBL_HR_PERSONNEL_DEPARTMENT
 *     selector: IX_MAP_HR_PERSONNEL_DEPARTMENT_UNIT
 *
 * This is important for the ´smart´ "set" and "value" functions to work!
 *
 * Setting a localizedKwl Value the old way:
 *
 *     //insert complicated, localizedKwl specific code here
 *
 * Setting a localizedKwl Value the new way:
 *
 *     form.fields.IX_MAP_HR_PERSONNEL_DEPARTMENT.set("DEV")
 *
 * The same also works for dates and of course "normal fields".
 *
 * field.show() and field.hide() take two parameters:
 * includingLabel     Boolean
 * includingSelector  Boolean
 * If both parameters are set to true, the field, its label and its unit-selector will be hidden/shown.
 */
sol.define("sol.common.forms.FormWrapper.Field", {
  initialize: function (config) {
    var me = this,
        parent, selector, unitSelectorName;

    me.$super("sol.Base", "initialize", [config]);
    me.name = me.fName;
    if (!me.name) {
      return;
    }

    parent = $var(me.fName);
    parent = parent && parent.parentElement;

    me.isDate = (parent && parent.getAttribute("elonodename") == "DATE") || (me.element() && me.element().getAttribute("eloverify") == "date");
    me.datePicker = me.isDate && me.element().parentElement.children[1];
    unitSelectorName = me.prefix + "_" + me.shortName.toUpperCase() + "_UNIT";


    me.label = $var("LBL_" + me.prefix + "_" + me.shortName.toUpperCase());

    if (me.isDate) {
      selector = $var(me.fName); // yep
    } else {
      selector = $var("IX_MAP_" + unitSelectorName) || $var("IX_GRP_" + unitSelectorName);
    }
    me.selector = selector;
    me.localizedDynKwlName = me.getDynKwlName();
    me.localizedDynKwl = me.localizedDynKwlName && me.element().nextSibling;
  },

  /**
   * determines the dynamic keyword list's name, if the field is linked to one and returns it
   * @returns {String}
   */
  getDynKwlName: function () {
    var me = this,
        kwl = me.element() && me.element().nextSibling,
        name, result, elocompl;
    if (!me.element()) {
      return;
    }
    name = (me.element().getAttribute("swlname") || (kwl && kwl.getAttribute("swlname"))) || elocompl;

    elocompl = (me.element().getAttribute("elocompl") || (kwl && kwl.getAttribute("elocompl")));

    name = (name === "#DATE#" ? undefined : name);

    result = name && ((name.indexOf("DYNSWL_") == 0 && name.replace("DYNSWL_", "")) || name) || undefined;
    me.isLocalized = result && (!elocompl); //localized kwls do not have elocompl attribute
    return result;
  },

  /**
   * removes selector from a kwl-field (e.g. for making it readonly)
   * @private
   */
  kwlStyle: function (mode) {
    var me = this;
    mode === "remove" ?
      me.localizedDynKwlName && me.element().removeAttribute("elocompl") :
      me.localizedDynKwlName && me.element().setAttribute("elocompl", me.localizedDynKwlName);
  },

  /**
   * wrapper around $var, can also work with images (which $var can't)
   * @returns {HTML-DIV}
   */
  element: function () {
    var el = $var(this.fName);
    if (!el) {
      el = document.getElementsByName(this.fName);
      el = el.length > 0 && el[0]; // var does not seem to find images, therefore try this
    }
    return el;
  },

  valueFromEloParams: function () {
    var me = this;
    return (me.form.undefinedFields.indexOf(me.fName) === -1)
      ? ""  // only lookup in ELO_PARAMS if field was never on form
      : (ELO_PARAMS[me.fName] === "undefined" ? "" : ELO_PARAMS[me.fName]);
  },

  /**
   * determines the field's value.
   *
   * returns an isoDate if the field is a date.
   * returns its key, if the field is a localizedKwl.
   *  @returns {String}
   */
  value: function (opts) {
    var me = this, value;

    value = (
      (me.isDate && sol.common.forms.Utils.getIsoDate(me.fName, {
        startOfDay: true
      }))
      || (me.localizedDynKwlName && me.getSelectedLocalizedDynKwlValue(opts))
      || ($var(me.fName) ? $val(me.fName) : ($val(me.fName) || me.valueFromEloParams()))
      || ""
    );

    return (opts && opts.asNumber)
      ? toNum(value) // ELO WF global function
      : value;
  },

  /**
   * returns current tooltip as a String
   */
  tooltip: function () {
    var me = this;
    return (me.element() && me.element().getAttribute("savedtitle")) || "";
  },

  /**
   * writes tip as the new tooltip
   */
  writeTooltip: function (tip) {
    var me = this, el = me.element();
    if (!el) {
      return;
    }
    if (typeof tip === "string" && tip) {
      if (el.getAttribute("savedtitle")) {
        el.setAttribute("savedtitle", tip);
      } else {
        el.setAttribute("title", tip);
        ELOF.assignTooltips(el.parentElement);
      }
    } else {
      el.getAttribute("savedtitle")
      ? el.onmouseover = undefined
      : console.log(me.fName + ": No tooltip created for value '" + tip + "' (type:" + typeof tip + ")");
    }
  },

 /**
  * @private please use value() for the same effect
  */
  getSelectedLocalizedDynKwlValue: function (opts) {
    var me = this;
    if (opts) {
      if (opts.full) {
        return $val(me.fName) || "";
      }
      if (opts.localizedStringOnly) {
        return me.getSelectedLocalizedDynKwlString() || "";
      }
    }
    return me.getSelectedLocalizedDynKwlKey() || "";
  },

 /**
  * @private please use getSelectedLocalizedDynKwlValue() without a parameter for the same effect
  */
  getSelectedLocalizedDynKwlKey: function () {
    var me = this,
        val = $val(me.name) || "";
    if (val.search(/\w+ - ?.*/g) === -1) {
      return val;
    } else {
      return val.slice(0, val.indexOf(" -"));
    }
  },

 /**
  * @private please use getSelectedLocalizedDynKwlValue() with { localizedStringOnly:true } for the same effect
  */
  getSelectedLocalizedDynKwlString: function () {
    var me = this,
        val = $val(me.name);
    if (val.search(/\w+ - ?.*/g) === -1) {
      return val;
    } else {
      return val.slice(val.indexOf(" -") + 3);
    }
  },

  /**
   * set a field's value to `value`.
   *
   * pass an isoDate if the field is a date.
   *
   * pass a key, if it's a localizedKwl
   *
   * @param {String || Integer(isoDate)} value
   */
  set: function (value) {
    var me = this, children;
    (me.isDate && value === "" && ($update(me.fName, value) || true))
    || (me.isDate && (sol.common.forms.Utils.setIsoDate(me.fName, value) || true))
    || (!me.localizedDynKwl && me.localizedDynKwlName && (me.selectDynKwlEntry(value) || true))
    || (me.localizedDynKwlName && (me.setLocalizedDynKwlKey(value) || true))
    || $update(me.fName, value);

    children = me.element().childNodes;
    value && me.viewSource && children.length === 1 && children[0].tagName === "ACRONYM" && me.element().replaceChild(document.createTextNode(value), children[0]);
  },

  selectDynKwlEntry: function (value) {
    var me = this, obj = {};
    obj[me.fName] = value;
    scatterDynamicField(obj, me.fName); //wf function
  },

  convertToDiv: function (url) {
    function replace(a, b) {
      a.parentElement.appendChild(b).parentElement.removeChild(a);
    }
    var img = this.element(), div = img.tagName === "DIV" ? img : document.createElement("div");
    div.setAttribute("name", img.getAttribute("name"));
    div.style = "background-image:url(\'" + url + "\');";
    div.value || (div.value = "");
    (img.tagName !== "DIV") && replace(img.parentElement, div);
  },

  /**
   * sets an Image divs Image to the image having the passed guid. If no guid is passed, and the field's value contains a guid, it will be used instead
   * @param {String} guid
   */
  setImage: function (guid, elementType) {
    var me = this, url, el = me.element();
    guid = guid || me.value();
    !guid && me.hide();
    if (me.form.wfMeetsVersionRequirement("10.02.000") && me.form.wfName) {
      url = "/" + me.form.wfName + "/apps/rest/api/download/" + guid + "?ticket=" + ELO_PARAMS.ELO_TICKET;
      if (elementType === "div") {
        me.convertToDiv(url);
      } else {
        el.src = url;
      }
    } else {
      guid && sol.common.forms.Utils.initializeIxSession(function () {
        elo.IX.ix().checkoutDoc(guid, null, elo.CONST.EDIT_INFO.mbDocument, elo.CONST.LOCK.NO, new de.elo.ix.client.AsyncCallback(
          function (doc) {
            url = doc.document.docs[0].url;
            if (elementType === "div") {
              me.convertToDiv(url);
              (url === "./images/") ? me.hide() : me.show();
            } else {
              el.src = url;
              (url === "./images/") ? me.hide() : me.show();
            }
          },
          function () {
            null;
          }));
      });
    }
  },

  /**
   * Sets an image by an URL
   * @param {String} url URL
   */
  setImageUrl: function (url, elementType) {
    var me = this, el = me.element();
    if (url) {
      if (elementType === "div") {
        me.convertToDiv(url);
        me.show();
        el.style = "background-image:url('" + url + "');";
      } else {
        me.show();
        el.src = url;
      }
    }
  },

  /**
   * @private please use set() for the same effect
   * @param {String} key
   */
  setLocalizedDynKwlKey: function (key) {
    var me = this;
    if (key) {
      $update(me.fName, me.getSelectedLocalizedDynKwlKey());
      $listDyn(me.localizedDynKwlName, me.fName, undefined, function (data) {
        var result = data.table.find(function (entry) {
          return entry[0] === key;
        });
        result && $update(me.fName, result[2]);
      });
    } else {
      $update(me.fName, "");
    }
  },

  /**
   * used by FormWrapper to map state properties to the respective functions
   * @private
   */
  setAttribute: function (attribute, value) {
    var me = this,
        field = me.element(), parentFun;
    switch (attribute) {
      case "readonly":
        if (field.type === "checkbox") {
          field[value ? "setAttribute" : "removeAttribute"]("disabled", value);
        } else {
          parentFun = value && field.parentElement.setAttribute.bind(field.parentElement) ||
          field.parentElement.removeAttribute.bind(field.parentElement);
          field.readOnly = value;
          parentFun("isreadonly", value);
          $setReadOnly(me.fName, value);
          me.kwlStyle(value && "remove");
        }
        break;
      case "optional":
        value ? me.makeOptional() : me.makeMandatory();
        break;
      case "hidden":
        value ? me.hide(true, true) : me.show(true, true);
        break;
      default:
        break;
    }
  },

  /**
   * @private please use setAttribute("optional", true)
   */
  makeOptional: function () {
    var me = this;
    me.modifyEloVerify(me.element(), "remove");
    me.label && me.label.parentElement && me.label.parentElement.classList.remove("required");
  },

  /**
   * used by assignValidationFunctionToWindow
   * @private
   */
  modifyEloVerify: function (element, action) {
    var me = this, eloVerify = element.getAttribute("eloverify"),
        match;
    if (eloVerify || (eloVerify.trim() === "")) {
      switch (action) {
        case "remove":
          element.setAttribute("eloverify", me.removeKeyFromEloVerify(eloVerify));
          break;
        case "add":
          element.setAttribute("eloverify", me.addKeyToEloVerify(eloVerify));
          break;
        default:
          break;
      }
    }
  },
  removeKeyFromEloVerify: function (eloVerify) {
    var match = /(.* ?)notemptyforward( ?.*)/g.exec(eloVerify);
    if (match) {
      eloVerify = match[1] + match[2];
    }
    return eloVerify.trim();
  },


  /**
   * @private please use setAttribute("optional", false)
   */
  makeMandatory: function () {
    var me = this;
    me.modifyEloVerify(me.element(), "add");
    me.label && me.label.parentElement && me.label.parentElement.classList.add("required");
  },
  addKeyToEloVerify: function (eloVerify) {
    var match = /(.* ?)notemptyforward( ?.*)/g.exec(eloVerify);
    if (!match) {
      eloVerify += " notemptyforward";
    }
    return eloVerify.trim();
  },

  /**
   * can be used to set an HTML Attribute
   * @param {String} attribute  name of the style attribute
   * @param {String} value  value for the attribute
   * @param {Boolean} includingLabel also set attribute for field's label
   * @param {Boolean} includingSelector also set attribute for field's selector
   */
  setStyleAttribute: function (attribute, value, includingLabel, includingSelector) {
    var me = this,
        field = me.element(),
        label = me.label,
        selector = me.selector,
        selectorButton = selector && selector.parentElement.children[1];
    field.style[attribute] = value;
    if (includingLabel && label) {
      label.style[attribute] = value;
    }
    if (includingSelector && selector) {
      selector.style[attribute] = value;
      selectorButton.style[attribute] = value;
    }
  },

 /**
  * applies a manipulator function to the field
  * @private
  */
  applyStandardManipulator: function (manipulator, includingLabel, includingSelector) {
    var me = this,
        field = me.fName,
        label = me.label && me.label.id,
        selector = me.selector && me.selector.name;
    manipulator(field);

    includingLabel && manipulator(label);
    includingSelector && manipulator(selector);
  },

 /**
  * @private please use show() or hide() for the same effect
  */
  changeVisibility: function (visible, includingLabel, includingSelector) {
    var me = this;
    // 9.3
    if (visible) {
      if (me.localizedDynKwl) {
        me.localizedDynKwl.style.visibility = "visible";
      }
      if (me.datePicker) {
        me.datePicker.children[0].classList.add("calbutton");
      }
    } else {
      if (me.localizedDynKwl) {
        me.localizedDynKwl.style.visibility = "visible";
      }
      if (me.datePicker) {
        me.datePicker.children[0].classList.remove("calbutton");
      }
    }
    // 9.3 end
    me.applyStandardManipulator(visible ? $show.bind(me) : $hide.bind(me), includingLabel, includingSelector);
  },

  /**
   * unhides/shows the field on the form
   * @param {Boolean} includingLabel also unhide/show the field's label
   * @param {Boolean} includingSelector also unhide/show the field's selector
   */
  show: function (includingLabel, includingSelector) {
    this.changeVisibility(true, includingLabel, includingSelector);
  },

  /**
   * hides the field on the form
   * @param {Boolean} includingLabel also hides the field's label
   * @param {Boolean} includingSelector also hides the field's selector
   */
  hide: function (includingLabel, includingSelector) {
    this.changeVisibility(false, includingLabel, includingSelector);
  }
});

/**
 * FormWrapper Templates
 *
 * @author ESt, ELO Digital Office GmbH
 *
 * @elowf
 *
 * The FormWrapper provides a simple templating mechanism.
 * To use a template, define a field in any state and set its template.name property
 * to the name of the template:
 *
 *     IX_MAP_HR_CASSATIONTOGGLE: {
 *       template: { name: "toggle", config: { ... } }
 *     }
 *
 * A template is simply a function which creates a state-object using the config passed via template.config
 * and returns the created state-object in the end.
 *
 * Simplest template:
 *
 *     myReadOnlyTemplate: function (config) {
 *       var configuredTemplate;
 *
 *       configuredTemplate = {
 *         states: {
 *           stateInit: {
 *             fieldProperties: {},
 *             tabProperties: {}
 *           }
 *         }
 *       }
 *
 *       configuredTemplate.stateInit.fieldProperties[config.name] = {readonly: config.value};
 *       return configuredTemplate;
 *     }
 *
 * This template `myReadOnlyTemplate` would do only one thing: set a field readonly during initialization (stateInit).
 *
 * If you define state properties in a template, they will overwrite existing states, be careful. Never overwrite the stateInit state itself, but only a field's properties in its `fieldProperties`!
 *
 * You can define a template like the one above in the OnInit function, if you define it in the extending class.
 *
 *     OnInit: function {
 *       var me = this;
 *       me.templates.myReadOnlyTemplate = me.myReadOnlyTemplate;
 *     }
 *
 * Now you can use the template:
 *
 *     IX_GRP_HR_PERSONNEL_FIRSTNAME: {
 *       template: { name: "myReadOnlyTemplate", config: { name: "IX_GRP_HR_PERSONNEL_FIRSTNAME", value: true } };
 *     }
 *
 * Of course, more complex templates make more sense, since a fieldProperty can only be assigned one single template.
 *
 * You can not use a template and properties (e.g. "hidden", "readonly", ...) in a fieldProperty at the same time!
 */
sol.define("sol.common.forms.FormWrapper.Templates", {
  /**
   * Toggle Template.
   *
   * ### Example config
   * You have a form containing a toggle. Depending on which option is toggled, you want to hide, or respectively unhide fields.
   *
   * A toggle always has an "on-state", an "off-state" and an internal "nothing toggled" state
   *
   *     {
   *        toggleOptionFieldName: "IX_MAP_HR_CASSATIONTOGGLE",
   *        onValue: "sol.hr.form.personnelfiledocument.cassationactive",
   *        offValue: "sol.hr.form.personnelfiledocument.nocassation",
   *        onState: {
   *          fieldProperties: {
   *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD: {
   *              hidden: false
   *            },
   *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD_UNIT: {
   *              hidden: false
   *            },
   *            IX_GRP_HR_PERSONNEL_CASSATIONDATE: {
   *              hidden: false
   *            }
   *          }
   *        },
   *        offState: {
   *          fieldProperties: {
   *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD: {
   *              hidden: true
   *            },
   *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD_UNIT: {
   *              hidden: true
   *            },
   *            IX_GRP_HR_PERSONNEL_CASSATIONDATE: {
   *              hidden: true,
   *              value: ""
   *            }
   *          }
   *        }
   *      }
   */
  toggle: function (config) {
    var configuredTemplate,
      triggerLogic = function (toggledOption) {
        var me = this;
        if (toggledOption == config.onValue) {
          me.setState("toggle_" + config.toggleOptionFieldName + "_anyOptionSelected");
          me.setState("toggle_" + config.toggleOptionFieldName + "_OnOptionSelected");
        } else if (toggledOption == config.offValue) {
          me.setState("toggle_" + config.toggleOptionFieldName + "_anyOptionSelected");
          me.setState("toggle_" + config.toggleOptionFieldName + "_OffOptionSelected");
        }
      };

    configuredTemplate = {
      states: {
        stateInit: {
          fieldProperties: {},
          tabProperties: {}
        }
      },
      OnInitAndTabChange: function () {
        var me = this,
          toggledOption = me.fields[config.toggleOptionFieldName].value();

        if (!config.tabs || config.tabs.length === 0 || config.tabs.indexOf(me.tabs.activeTab.name) > -1) {
          toggledOption == "" ?
            me.setState("toggle_" + config.toggleOptionFieldName + "_defaultState") :
            triggerLogic.call(me, toggledOption);
        }
      }
    };

    configuredTemplate.states.stateInit.fieldProperties[config.toggleOptionFieldName] = {
      responder: function (form, state, field, value) {
        triggerLogic.call(form, value);
      }
    };

    configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_defaultState"] = config.defaultState;
    configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_OnOptionSelected"] = config.onState;
    configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_OffOptionSelected"] = config.offState;
    configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_anyOptionSelected"] = config.anyState;

    return configuredTemplate;
  },

  /**
   * Filechooser Variants (drag&drop, filechooser, webcam picture capturing).
   * This template has various dependencies (jar-file, libs, ...)
   *
   * In some forms, you'd maybe like to let the user select a picture. And save it for later use.
   * If so, please use the 113_capturepic_webcam "HR"-Form as an inspiration.
   *
   * As soon as you defined your own form having all required fields, you can use the following template
   * ### Example config
   *
   *     {
   *       name: "personnelphotopicker",
   *       webcamName: "personnelphotocam",
   *       webcamConfig: {
   *         javaStartupButton: "WEBCAM_JAVA",
   *         varNameBtnReset: "JS_WEBCAM_RESET",
   *         varNameBtnSnap: "JS_WEBCAM_SNAP",
   *         varNameContainer: "WEBCAM_INIT",
   *         width: 540,
   *         height: 390,
   *         dest_width: 720,
   *         dest_height: 520,
   *         crop_width: 400,
   *         crop_height: 520,
   *         image_format: "jpeg",
   *         jpeg_quality: 90,
   *         swfURL: "lib_webcam.swf",
   *         fps: 45,
   *         showIfNoCam: true
   *       },
   *       dropZoneId: "dropZone",
   *       filePickerId: "filePicker",
   *       accept: "image/jpeg, image/jpg, image/png",
   *       maxSize: "3", //Megabyte (float values possible)
   *       maskNameForRule: "Personnel file",
   *       solTypeForRule: "PERSONNELFILE",
   *       photoReferenceField: "HR_PERSONNEL_PHOTO_GUID",
   *       photoReferenceFieldObjId: "HR_PERSONNEL_PHOTO_OBJID",
   *       clearPreviewField: "JS_PICTURE_CLEAR",
   *       filePickerField: "JS_FILEPICKER",
   *       photoConfig: {
   *         maskName: "Personnel file document",
   *         pictureName: "Mitarbeiterfoto"
   *       }
   *     }
   */
  fileChooserVariants: function (config) {
    var configuredTemplate;

    configuredTemplate = {
      states: {
        stateInit: {
          fieldProperties: {},
          tabProperties: {}
        }
      },
      OnInitAndTabChange: function () {
        var me = this;
        me.fcv = sol.create("sol.common.forms.FileChooserVariants", config);
      },
      OnSaveRule: {
        name: "FileDragDropAndDialog" + config.name,
        rule: {
          maskName: config.maskNameForRule,
          solType: config.solTypeForRule,
          saveValues: function () {
            var me = this;
            me.fcv.uploadFile();
            return true;
          }
        }
      }
    };

    return configuredTemplate;
  },

  /**
   * Template for a date which will receive its value from a unit field and its selector.
   *
   * Validation messages will be displayed, if the user tries to change a field which would falsify the calculation
   *
   * e.g.
   * Start date "20170101"
   *
   * unit "4"
   *
   * unitselector value "days"
   *
   * target date = 20170105
   *
   * Please always name your selector unit field "FIELDNAME_UNIT"
   * Other names are not supported yet.
   *
   * ### Example config
   *
   *     {
   *       startDateFnOrValue: "IX_GRP_HR_PERSONNEL_DATEOFJOINING",
   *       unitValueFieldName: "IX_MAP_HR_PERSONNEL_PROBATIONARYPERIODDURATION",
   *       unitSelectorFieldName: "IX_MAP_HR_PERSONNEL_PROBATIONARYPERIODDURATION_UNIT",
   *       targetDateFieldName: "IX_MAP_HR_PERSONNEL_ENDOFPROBATIONARY",
   *       validationMessage: "Da im Feld `Ende der Probezeit` ein von der Berechnung abweichender Wert eingegeben wurde, ist dieses Feld jetzt gesperrt. Leeren Sie das Feld, wenn Sie die Berechnungsfunktion verwenden möchten!"
   *     }
   *
   */
  dateFromUnitSelectorRestrictive: function (config) {
    var configuredTemplate;
    configuredTemplate = {
      states: {
        stateInit: {
          fieldProperties: {},
          tabProperties: {}
        }
      }
    };

    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasNoValue"] = {
      fieldProperties: {}
    };
    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasNoValue"].fieldProperties[config.unitValueFieldName] = {
      readonly: false,
      validator: false
    };
    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasNoValue"].fieldProperties[config.unitSelectorFieldName] = {
      readonly: false,
      validator: false
    };

    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"] = {
      fieldProperties: {}
    };
    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitValueFieldName] = {
      readonly: true,
      validator: function () {
        return config.validationMessage;
      }
    };
    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitSelectorFieldName] = {
      readonly: true,
      validator: function () {
        return config.validationMessage;
      }
    };

    configuredTemplate.states.stateInit.fieldProperties[config.targetDateFieldName] = {
      responder: function (form, state, field, value) {
        if (value === "") {
          form.setState("dfus_" + config.targetDateFieldName + "_HasNoValue");
        } else if (value !== form.returnCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.targetDateFieldName, config.offsetNumber, config.offsetUnit)) {
          form.setState("dfus_" + config.targetDateFieldName + "_HasCustomValue");
        }
      }
    };
    configuredTemplate.states.stateInit.fieldProperties[config.unitValueFieldName] = {
      responder: function (form, state, field, value) {
        if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
          form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
        }
      }
    };

    configuredTemplate.states.stateInit.fieldProperties[config.unitSelectorFieldName] = {
      responder: function (form, state, field, value) {
        if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
          form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
        }
      }
    };

    return configuredTemplate;
  },

   /**
   * Template for a date which will receive its value from a unit field, its duration-selector and an optional "termination-Point" selector.
   *
   * Please always name your selector unit field "FIELDNAME_UNIT"
   * Other names are not supported yet.
   *
   * Also, it is recommended, to name your termination point field "FIELDNAME_TP"
   *
   * ### Example config
   *
   *     IX_MAP_HR_PERSONNEL_NEXTPOSSIBLEDISMISSAL: {
   *       template: {
   *         name: "dateFromUnitSelector",
   *         config: {
   *           recalculate: true,
   *           startDateFnOrValue: undefined,  // == today
   *           unitValueFieldName: "IX_MAP_HR_PERSONNEL_PERIODOFNOTICE",
   *           unitSelectorFieldName: "IX_MAP_HR_PERSONNEL_PERIODOFNOTICE_UNIT",
   *           terminationPointFieldName: "IX_MAP_HR_PERSONNEL_PERIODOFNOTICE_TP",
   *           targetDateFieldName: "IX_MAP_HR_PERSONNEL_NEXTPOSSIBLEDISMISSAL",
   *         }
   *       }
   *     }
   *
   */
  dateFromUnitSelector: function (config) {
    var configuredTemplate;
    configuredTemplate = {
      states: {
        stateInit: {
          fieldProperties: {},
          tabProperties: {}
        }
      }
    };

    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"] = {
      fieldProperties: {}
    };
    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitValueFieldName] = {
      value: "",
      overwrite: true
    };
    configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitSelectorFieldName] = {
      value: "",
      overwrite: true
    };

    configuredTemplate.states.stateInit.fieldProperties[config.targetDateFieldName] = {
      responder: function (form, state, field, value) {
        if (value !== form.returnCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit)) {
          form.setState("dfus_" + config.targetDateFieldName + "_HasCustomValue");
        }
      }
    };

    if (config.terminationPointFieldName) {
      configuredTemplate.states.stateInit.fieldProperties[config.terminationPointFieldName] = {
        responder: function (form, state, field, value) {
          if (field.value() && form.fields[config.terminationPointFieldName].value()) {
            form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
          }
        }
      };
    }

    configuredTemplate.states.stateInit.fieldProperties[config.unitValueFieldName] = {
      responder: function (form, state, field, value) {
        if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
          form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
        }
      }
    };

    configuredTemplate.states.stateInit.fieldProperties[config.unitSelectorFieldName] = {
      responder: function (form, state, field, value) {
        if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
          form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
        }
      }
    };

    if (config.recalculate) {
      configuredTemplate.OnInitAndTabChange = function () {
        var me = this,
            unitValueField = me.fields[config.unitValueFieldName];
        if (unitValueField && unitValueField.value()) {
          me.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
        }
      };
    }

    return configuredTemplate;
  }
});