Source control/default.control.js

define(function(require){
/**
* @module default%control
* @description ## Default controls
*
* #### Primitives
*
* - {@link control:number}
* - {@link control:integer}
* - {@link control:boolean}
*
* #### Formats
* 
* - {@link control:email}
* - {@link control:payment-card}
* 
* #### Control types
* 
* - {@link control:checkbox}
* - {@link control:select}
* - {@link control:date}
* - {@link control:payment-date-start}
* - {@link control:payment-date-expiry}
*
* @see template:control%view
* @see module:bauplan%control%view
*
* @requires module:bauplan%control%view
* @requires jQuery
* @requires Lodash
* @requires Moment
* @requires datejs
* @requires Larynx
*/
    var ControlView = require("bauplan.control.view");
    var Larynx = require("larynx");
    var jQuery = require("jquery");
    var Moment = require("moment");
    var _ = require("underscore");
    var Bauplan = require("bauplan");
    require("datejs");
    var DateJS = Date;

    /**
     * @method normalizeNumber
     * @description Ensure a number is a number
     * @param  {*} value Value to be normalised
     * @return {number} Number or undefined
     */
    var normalizeNumber = function(value) {
        if (!value && value !== 0) {
            return undefined;
        }
        var newvalue = 1 * value;
        return isNaN(newvalue) ? value : newvalue;
    };

    /**
     * @member numberInit
     * Provides number validation methods and ensures instant validation
     * @property  {boolean} instantValidation see {@link module:default%control~instantValidation}
     * @property  {function} numberValidation see {@link module:default%control~numberValidation}
     * @return {object} NumberMethods
     */
    var numberInit = {
        /**
         * @member {boolean} instantValidation
         */
        instantValidation: true,
        /**
         * @method numberValidation
         * @description Checks a number to see that it validates against any of the following validation constraints set on the property’s schema:
         * 
         * - multipleOf
         * - maximum
         * - exclusiveMaximum
         * - minimum
         * - exclusiveMinimum
         * - maxLength
         * - minLength
         * - exactLength
         *
         * #### Error codes
         * 
         * - {{primitive}}.not-multiple-of
         * - {{primitive}}.more-than-maximum
         * - {{primitive}}.more-than-exclusive-maximum
         * - {{primitive}}.less-than-minimum
         * - {{primitive}}.less-than-exclusive-minimum
         * - {{primitive}}.maxlength
         * - {{primitive}}.minlength
         *
         * (where primitive is this.primitive)
         */
        numberValidation: function(value, options) {
            options = options || {};
            if (options.display === undefined && !this.model.get(this.valueAttribute)) {
                options.display = false;
            }
            var schema = this.controlschema;
            if (schema.multipleOf) {
                if (value/schema.multipleOf !== parseInt(value/schema.multipleOf, 10)) {
                    this.addError(this.primitive+".not-multiple-of");
                }
            }
            if (schema.maximum) {
                if (schema.exclusiveMaximum) {
                    if (value >= schema.maximum) {
                        this.addError(this.primitive+".more-than-exclusive-maximum");
                    }
                } else if (value < schema.maximum) {
                    this.addError(this.primitive+".more-than-maximum");
                }
            } else if (schema.minimum) {
                if (schema.exclusiveMinimum) {
                    if (value <= schema.minimum) {
                        this.addError(this.primitive+".less-than-exclusive-minimum");
                    }
                } else if (value < schema.minimum) {
                    this.addError(this.primitive+".less-than-minimum");
                }
            }
            var maxLength = schema.maxLength || schema.exactLength;
            if (maxLength) {
                if (("" + value).length > maxLength) {
                    this.addError(this.primitive+".maxlength", options);
                }
            }
            var minLength = schema.minLength || schema.exactLength;
            if (minLength) {
                if (("" + value).length < minLength) {
                    this.addError(this.primitive+".minlength", options);
                }
            }
        }
    };
    /**
    * @control number
    * @controlkind primitive
    * @description Checks that input is a number
    *
    * Output a control with the name "foo" that only accepts numbers
    * 
    *     {{control "foo" control-primitive="number"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the type property for the "foo" property in the model schema has been set to "number"
    *
    * #### Error codes
    * 
    * - number.not-a-number
    * - Plus those provided by {@link module:default%control~numberValidation}
    *
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addPrimitive({
        name: "number",
        /**
         * @method initialize
         * @memberof control:number
         * @description {@link module:default%control~numberInit}
         */
        initialize: numberInit,
        /**
         * @method normalize
         * @memberof control:number
         * @description {@link module:default%control~normalizeNumber}
         */
        normalize: normalizeNumber,
        /**
         * @method validate
         * @memberof control:number
         * @param {number} value
         * @description Checks:
         * 
         * - number
         * - {@link module:default%control~numberValidation}
         */
        validate: function(value, options) {
            // if (this.getError()) { return; }
            if (value && typeof value !== "number") {
                this.addError("number.not-a-number");
            }
            this.numberValidation(value, options);
        }
    });
    /**
    * @control integer
    * @controlkind primitive
    * @description Checks that input is an integer
    *
    * Output a control with the name "foo" that only accepts integers
    * 
    *     {{control "foo" control-primitive="integer"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the type property for the "foo" property in the model schema has been set to "integer"
    *
    * #### Error codes
    * 
    * - integer.not-a-number
    * - integer.not-an-integer
    *
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addPrimitive({
        name: "integer",
        /**
         * @method initialize
         * @memberof control:integer
         * @description {@link module:default%control~numberInit}
         */
        initialize: numberInit,
        /**
         * @method normalize
         * @memberof control:integer
         * @description {@link module:default%control~normalizeNumber}
         */
        normalize: normalizeNumber,
        /**
         * @method validate
         * @memberof control:integer
         * @param {number} value
         * @param {object} [options]
         * @description Checks:
         * 
         * - number
         * - integer
         * - {@link module:default%control~numberValidation}
         */
        validate: function(value, options) {
            if (typeof value !== "number") {
                this.addError("integer.not-a-number");
            } else if (parseInt(value, 10) !== value) {
                this.addError("integer.not-an-integer");
            }
            this.numberValidation(value, options);
        }
    });
    /**
    * @control boolean
    * @controlkind primitive
    * @description Checks that input is boolean via checkbox
    *
    * Output a control with the name "foo" that ensures the value returned is boolean
    * 
    *     {{control "foo" control-primitive="boolean"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the type property for the "foo" property in the model schema has been set to "boolean"
    *
    * Additionally makes the control type checkbox
    *
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addPrimitive({
        name: "boolean",
        /**
         * @method initialize
         * @memberof control:boolean
         * @description Sets controltype to checkbox
         */
        initialize: {
            controltype: "checkbox"
        },
        //normalize: normalizeNumber,
        //validate: function(value) {
        //}
    });


    /**
    * @control email
    * @controlkind format
    * @description Checks that input is a validly formatted email address
    *
    * Output a control with the name "foo" that only accepts input matching an email address
    * 
    *     {{control "foo" control-format="email"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the format property for the "foo" property in the model schema has been set to "email"
    *
    * #### Error codes
    * 
    * - email.no-at-symbol
    * - email.invalid
    * 
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    var emailpattern = /^(([A-Z0-9_\.\-])+\+){0,1}([A-Z0-9_\.\-])+\@(([A-Z0-9\-])+\.)+[A-Z0-9]{2,6}$/i;
    ControlView.addFormat({
        name: "email",
        /**
         * @method initialize
         * @memberof control:email
         * @description Sets instantValidation to true
         */
        initialize: {
            instantValidation: true
        },
        /**
         * @method validate
         * @memberof control:email
         * @param {number} value
         * @param {object} [options]
         * @description Checks:
         * 
         * - contains @
         * - matches email regex
         */
        validate: function(value, options) {
            var erroropts = {};
            if (options.instantValidate && !this.model.get("value")) {
                erroropts.display = false;
            }
            if (value.indexOf("@") === -1) {
                this.addError("email.no-at-symbol", erroropts);
            } else if (!emailpattern.test(value)) {
                this.addError("email.invalid", erroropts);
            }
        }
    });


    /**
    * @control checkbox
    * @controlkind controltype
    * @description Outputs a checkbox control
    *
    * Output a checkbox control with the name "foo"
    * 
    *     {{control "foo" control-type="checkbox"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the controltype property for the "foo" property in the model schema has been set to "checkbox"
    *
    * Succesful control sends value
    *
    * @todo  explain how value gets defined
    *
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addControlType({
        name: "checkbox",
        /**
         * @method initialize
         * @memberof control:checkbox
         * @description Sets valueAttribute to checked
         */
        initialize: {
            valueAttribute: "checked"
        },
        /**
         * @method prepare
         * @memberof control:checkbox
         * @description Uses valueAttribute to ensure correct value is set
         */
        prepare: function(control, phrasekey, options) {
            var value = (this.primitive === "boolean" ? true : Larynx.Phrase.getString(phrasekey, {_append: "value"})) || true;
            control.checked = control.value === value;
            control.value = value;
            if (!this.edit) {
                var dvalue = "" + (!!control.checked);
                var appends = {
                    _append: "value",
                    _appendix: dvalue
                };
                control.value = Larynx.Phrase.getString(phrasekey, appends) || Larynx.Phrase.getString("control.checkbox", appends) || dvalue;
            }
            return control;
        },
        /**
         * @method normalize
         * @memberof control:checkbox
         * @description Massages values of non-boolean controls
         */
        normalize: function(value) {
             if (this.primitive !== "boolean") {
                value = value ? this.model.get("value") : "";
            }
            return value;
        }
    });


    // jsonschema-types.txt for missing primitives, formats and generics
    // radio and checkbox groups, single checkbox

    /**
    * @control password-new
    * @controlkind controltype
    * @description Ensures conditions for submitting a new password have been met
    *
    * Output a new password control with the name "foo"
    * 
    *     {{control "foo" control-type="password-new"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the controltype property for the "foo" property in the model schema has been set to "password-new"
    *
    * #### Error codes
    * 
    * - password.old-password-required
    * - password.no-confirmation
    * - password.passwords-do-not-match
    *
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addControlType({
        name: "password-new",
        /**
         * @method validate
         * @memberof control:password-new
         * @param {string} value
         * @description Checks
         *
         * - old password has been provided (if not creating a brand new password)
         * - new password has been entered
         * - new password has been confirmed and matched
         */
        validate: function(value, options) {
            if (!this.controlparent) {
                return;
            }
            options = options || {};
            if (options.norevalidation) {
                return;
            }
            //console.log("parent view", this.controlparent);
            /*if (this.controlparent) {
                if (this.controlname === "password-new") {
                    var confirm = 
                }

                console.log("parent view kids", this.controlparent.children);
            }*/

            if (this.controlname === "password-old") {
                // if !value and password-new (or rather password) has a value
                if (!value) {
                    this.addError("password.old-password-required");
                }
                return;
            }

            // could pass 
            if (this.controlname === "password-new") {
                var confirmview = jQuery("[name=password-confirm]").view();
                confirmview.instantValidateControl(jQuery("[name=password-confirm]").val(), {prompted: true});
                // could spot this - but not doing so at the moment
                return;
            }

            var confirm = jQuery.trim(value); // nortygnorty
            var password = jQuery.trim(jQuery("[name=password-new]").val());

            if (password && !confirm) {
                var display = true; // won't display in any case, because only validating and not unpdating
                this.addError("password.no-confirmation", {display:display});
            } else if (confirm) {
                if (password !== confirm) {
                    this.addError("password.passwords-do-not-match");
                } else if (password) {
                    //console.log("We actually have a password and should be setting it");
                }
            }
        }
    });


    /**
    * @control select
    * @controlkind controltype
    * @description Select control
    *
    * Output a select control with the name "foo"
    * 
    *     {{control "foo" control-type="select"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the controltype property for the "foo" property in the model schema has been set to "select"
    *
    * @todo  Provide brief overview of options/values/selected
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addControlType({
        name: "select",
        /**
         * @method prepare
         * @memberof control:checkbox
         * @description Marshalls options and values
         * 
         * Checks for any values which may exist in the resource bundle properties files.
         * 
         * If so, the result is passed to Larynx.Phrase using the property’s phrase key appended with ".values"
         * 
         * Any resulting string (if any) is then split using commas as the delimiter.
         */
        prepare: function(control, phrasekey, options) {
            var valueStr = Larynx.Phrase.getString(phrasekey, {_append: "values"});

            control.options = options.options || [];
            control.values = options.values || [];
            if (valueStr) {
                var fvalues = valueStr.replace(/,\s+/g, ",").split(",");
                _.each(fvalues, function(a){
                    control.values.push(a);
                    var optionStr = Larynx.Phrase.getString(phrasekey, {_append: "option", _appendix:a});
                    control.options.push(optionStr);
                });
            }

            control.cue = Larynx.Phrase.getString(phrasekey, {_append: "cue"});

            return control;
        },
        /**
         * @method normalize
         * @memberof control:checkbox
         * @description Removes initial select cue option if present when an option with an actual value is selected
         */
        normalize: function(value) {
            // not really normalization - this should really be handled on the event
            if (value !== "" && value !== null && value !== undefined) {
                var $view = this.$el;
                $view.find(".select-cue").remove();
            }
            return value;
        }
    });

    /**
     * @method monthYearValue
     * @memberof control:payment-date-expiry
     * @inner
     * @param  {string} value       Value to be formatted
     * @param  {string} [phrasekey] Key to use for phrase lookup
     * @todo  phrasekey param looks wrong and pointless
     * @return {string}             Formatted month/year
     */
    var monthYearValue = function(value, phrasekey) {
        var format = "MMM YYYY";
        return moment(value).format(format);
    };
    /**
     * @method monthYearPrepare
     * @memberof control:payment-date-expiry
     * @inner
     * @param  {control} control
     * @param  {object} options
     * @return {control} Updated control
     */
    var monthYearPrepare = function(control, options) {
        var expiryValues = {};
        var dateValue;
        if (control.value) {
            dateValue = Moment(control.value);
        }
        if (options.edit) {
            if (dateValue) {
                expiryValues.month = dateValue.month();
                expiryValues.year = dateValue.year();
            }
        } else {
            control.value = dateValue ? dateValue.format("MMM YYYY") : "";
            return control;
        }
        // good to get this from options.options too?
        var currentDate;
        if (options.date) {
            currentDate = moment(options.date, options.dateformat);
        }
        if (!currentDate || !currentDate.isValid()) {
            currentDate = moment();
        }

        var currentMonth = currentDate.month() + 1;
        var currentYear = currentDate.year();
        var months = [];
        var monthcount = moment("Dec", "MMM");
        options.cc = true;
        for (var mon = 0; mon < 12; mon++) {
            var mval = mon + 1;
            var mvaldisplay = monthcount.add("M", 1).format("MMM");
            if (options.cc) {
                mvaldisplay = (mval < 10 ? "0" : "") + mval;
            }
            months.push({
                content: mvaldisplay,
                value: mval
            });
        }
        var datakey = "data-" + control.name;
        var month = {
            name: control.name + "-month",
            value: expiryValues.month + 1,
            options: months,
            "data-current-month": currentMonth
        };
        month[datakey] = "month";

        if (options.yearoffset) {
            currentYear -= options.yearoffset;
        }
        var years = [];
        var yearsBefore = options.yearsbefore || 0;
        var yearsAfter = options.years || 5;
        if (yearsAfter < 0) {
            yearsBefore = yearsAfter * -1;
        }
        if (yearsBefore) {
            yearsBefore *= -1;
            yearsAfter = options.yearsafter || 0;
            if (!yearsAfter) {
                options.exclude = "future";
            }
            yearsBefore += 1;
            yearsAfter += 1;
        } else {
            options.exclude = "past";
        }

        for (var i = yearsBefore; i < yearsAfter; i++) {
            var yeardata = currentYear + i;
            if (options.cc) {
                yeardata = {
                    value: yeardata,
                    content: yeardata.toString().substr(2)
                };
            }
            years.push(yeardata);
        }

        if (options.reverse) {
            years = years.reverse();
        }
        var year = {
            name: control.name + "-year",
            value: expiryValues.year,
            options: years,
            "data-current-year": currentYear
        };
        year[datakey] = "year";
        if (options.exclude) {
            year["data-exclude"] = options.exclude;
        }
        control.month = month;
        control.year = year;
        return control;
    };
    /**
     * @method monthYearNormalize
     * @memberof control:payment-date-expiry
     * @inner
     * @param  {string} value
     * @return {string} Normalised date
     */
    var monthYearNormalize = function(value) {
        var $view = this.$el;
        if (!$view) {
            return value;
        }
        var $monthControl = $view.find("[data-" + this.controlname + "=month]");
        var $yearControl = $view.find("[data-" + this.controlname + "=year]");
        var month = $monthControl.val();
        var year = $yearControl.val();
        /*var checks = {
            future: function(stored, value) {
                return stored < value;
            },
            past: function(stored, value) {
                return stored > value;
            }
        };
        function checkMonth(stored, value, exclude) {
            return checks[exclude](stored, value * 1);
        }
        var exclude = $yearControl.attr("data-exclude");
        if (exclude) {
            if (year === $yearControl.attr("data-current-year")) {
                var currentMonth = $monthControl.attr("data-current-month") * 1;
                $monthControl.find("option").each(function(){
                    if (checkMonth(currentMonth, this.value, exclude)) {
                        jQuery(this).attr("disabled", "disabled");
                    }
                });
                if (checkMonth(currentMonth, month, exclude)) {
                    month = currentMonth;
                    $monthControl.val(currentMonth);
                }
            } else {
                $monthControl.find("[disabled]").removeAttr("disabled");
            }
        }*/

        if (!month || !year) {
            return value;
        }
        value = Moment(month + " " + year, "M YYYY").format();
        return value;
    };
    /**
     * @method monthYearValidate
     * @memberof control:payment-date-expiry
     * @inner
     * @param  {string} value
     * @param  {object} [options]
     * @description
     *
     * #### Error codes
     * 
     * - month-year.invalid-{{exclude}}
     *
     * @return {boolean} Validation status
     */
    var monthYearValidate = function(value, options) {
        //if (options.display === undefined && !this.dirtymodel.get(this.valueAttribute)) {
        if (options.rendered) {
            options.display = false;
        }
        //var display = {};
        //if (!options.rendered && this.model.get(this.valueAttribute) === this.dirtymodel.get(this.valueAttribute)) {
        /*if (!options.rendered && this.model.get(this.valueAttribute)
        //if (options.initialValues) {
            display.display = false;
        }*/
        var $view = this.$el;
        var $monthControl = $view.find("[data-" + this.controlname + "=month]");
        var $yearControl = $view.find("[data-" + this.controlname + "=year]");
        var month = $monthControl.val();
        var year = $yearControl.val();
        var checks = {
            future: function(stored, value) {
                return stored < value;
            },
            past: function(stored, value) {
                return stored > value;
            }
        };
        function checkMonth(stored, value, exclude) {
            return checks[exclude](stored, value * 1);
        }
        var exclude = $yearControl.attr("data-exclude");
        if (exclude) {
            if (year === $yearControl.attr("data-current-year")) {
                var currentMonth = $monthControl.attr("data-current-month") * 1;
                if (checkMonth(currentMonth, month, exclude)) {
                    this.addError("month-year.invalid-"+exclude, options);
                }
            }
        }
    };

    /**
    * @control payment-date-expiry
    * @controlkind controltype
    * @description Using this format denotes a property as an expiry date and causes its control to allow the input of a month and year
    * 
    * Ensures expiry date is in the future
    *
    * Output a expiry date control with the name "foo"
    * 
    *     {{control "foo" control-type="payment-date-expiry"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the controltype property for the "foo" property in the model schema has been set to "payment-date-expiry"
    *
    * #### Error codes
    * 
    * - provided by {@link module:default%control~monthYearValidate}
    *
    * @see control:payment-date-start
    * @see control:payment-card
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addControlType({
        name: "payment-date-expiry",
        /**
         * @method display
         * @memberof control:payment-date-expiry
         * @description {@link control:payment-date-expiry~monthYearValue}
         */
        display: monthYearValue,
        /**
         * @method prepare
         * @memberof control:payment-date-expiry
         * @description Passes options to {@link control:payment-date-expiry~monthYearPrepare}
         */
        prepare: function(control, phrasekey, options) {
            var monthYearOptions = {
                edit: this.edit,
                phrasekey: phrasekey,
                options: options,
                years: 20
            };
            return monthYearPrepare(control, monthYearOptions);
        },
        /**
         * @method normalize
         * @memberof control:payment-date-expiry
         * @description {@link control:payment-date-expiry~monthYearNormalize}
         */
        normalize: function() {
            return monthYearNormalize.apply(this, arguments);
        },
        /**
         * @method validate
         * @memberof control:payment-date-expiry
         * @description {@link control:payment-date-expiry~monthYearValidate}
         */
        validate: monthYearValidate
    });



    /**
    * @control payment-date-start
    * @controlkind controltype
    * @description Using this format denotes a property as a start date and causes its control to allow the input of a month and year
    * 
    * Ensures start date is in the past
    *
    * Output a start date control with the name "foo"
    * 
    *     {{control "foo" control-type="payment-date-start"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the controltype property for the "foo" property in the model schema has been set to "payment-date-start"
    *
    * #### Error codes
    * 
    * - provided by {@link module:default%control~monthYearValidate}
    *
    * NB. shares methods with {@link control:payment-date-expiry} against which the methods are documented
    *
    * @see control:payment-date-expiry
    * @see control:payment-card
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addControlType({
        name: "payment-date-start",
        /**
         * @method display
         * @memberof control:payment-date-start
         * @description {@link control:payment-date-expiry~monthYearValue}
         */
        display: monthYearValue,
        /**
         * @method prepare
         * @memberof control:payment-date-start
         * @description Passes options to {@link control:payment-date-expiry~monthYearPrepare}
         */
        prepare: function(control, phrasekey, options) {
            var monthYearOptions = {
                edit: this.edit,
                phrasekey: phrasekey,
                options: options,
                //date: "1920 31 03",
                //dateformat: "YYYY DD MM",
                years: -10,
                //yearsbefore: 10,
                //yearsafter: 10,
                reverse: true
            };
            return monthYearPrepare(control, monthYearOptions);
        },
        /**
         * @method normalize
         * @memberof control:payment-date-start
         * @description {@link control:payment-date-expiry~monthYearNormalize}
         */
        normalize: function() {
            return monthYearNormalize.apply(this, arguments);
        },
        /**
         * @method validate
         * @memberof control:payment-date-start
         * @description {@link control:payment-date-expiry~monthYearValidate}
         */
        validate: monthYearValidate
    });



    /**
     * @method adjustMonth
     * @memberof control:date
     * @inner
     * @param  {number} m Month
     * @description  Prepends month with 0 if less than 10
     * @return {string}
     */
    function adjustMonth(m) {
        m = m * 1;
        if (m <  10) {
            m = "0" + m;
        }
        return m;
    }
    /**
     * @method adjustYear
     * @memberof control:date
     * @inner
     * @param  {number} m Year
     * @description Sets years entered as less
     *
     * eg.
     *
     *     adjustYear(15) -> 2015
     *     adjustYear(1986) -> 1986
     *  
     * @return {number}
     */
    function adjustYear(m) {
        m = m * 1;
        if (m <  100) {
            m = 2000 + m;
        }
        return m;
    }
    /**
     * @method dateValidateHelper
     * @memberof control:date
     * @inner
     * @param  {control} control Control object
     * @param  {string} type    Whether date is before or after
     * @param  {string} value   Date value to validate
     *
     * #### Error codes
     * 
     * - date.{{type}}
     * - {{control.options[type]}}.error
     * 
     * @return {boolean}
     */
    function dateValidateHelper (control, type, value) {
        var controloptions = control.controloptions;
        if (controloptions[type]) {
            var when = controloptions[type];
            if (typeof when === "string") {
                var d = new moment(when);
                if (!d.isValid()) {
                    var match = when.match(/(\d+)(\w+)/);
                    var matchmethod = type === "after" ? "add" : "subtract";
                    var mwhen = moment()[matchmethod](match[1], match[2]);
                    when = mwhen._d.toISOString();
                    //controloptions[type] = when;
                }
            }
            if (type === "after") {
                var x = when;
                when = value;
                value = x;
            }
            if (value > when) {
                var error = controloptions[type+".error"] || "date."+type;
                control.addError(error);
            }
        }
    }

    /**
    * @control date
    * @controlkind controltype
    * @todo  Provide better explanation for normalize
    * @todo  Provide som example expected input -> output
    * @description Allows freeform input of dates
    *
    * Output a date control with the name "foo"
    * 
    *     {{control "foo" control-type="date"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the controltype property for the "foo" property in the model schema has been set to "date"
    *
    * #### Error codes
    *
    * - date.invalid
    * - plus those provided by {@link control:date~dateValidateHelper}
    *
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addControlType({
        name: "date",
        /**
         * @method initialize
         * @memberof control:date
         * @description Sets instantValidation to false
         */
        initialize: {
            instantValidation: false
        },
        /**
         * @method prepare
         * @memberof control:date
         * @param {control} control Control object
         * @param {string} phrasekey Phrase key to provide format
         * @param {object} options
         * @description Sets correct format for date type
         * 
         * - is it just month/year?
         */
        prepare: function(control, phrasekey, options) {
            if (this.controloptions.datetype === "month-year") {
                this.monthyear = true;
                this.controloptions.format = "MMM YYYY";
                //delete options.datetype;
            }
            if (!this.controloptions.format) {
                this.controloptions.format = "D MMM YYYY";
            }
            //control.value = (new Date()).toISOString();
            return control;
        },
        /**
         * @method normalize
         * @memberof control:date
         * @param {string} value Date string to be normalized
         * @description Somewhat hardcore regexxing that requires much better explanation
         */
        normalize: function (value) {
            if (value) {
                value = value.trim();
            }
            if (value) {
                //|| this.controloptions.monthyear
                if (this.monthyear) {
                    value = value.replace(/^(\d{2})(\d{2}|\d{4})$/, function(m, m1, m2) {
                        if (m1 * 1 > 12) {
                            if (m2.length === 2) {
                                return m2  + "/" + m1 + "/01";
                            }
                            return m2.substr(2,2) + "/" + m1 + m2.substr(0,2) + "/01";
                        }
                        return m1 + "/01/" + m2;
                    });

                    value = value.replace(/^(\d+)\s*(?:\s|\.|\/)\s*(\d+)$/, function (m, m1, m2) {
                        if (m1 * 1 > 12) {
                            return adjustMonth(m2) + "/01/" + adjustYear(m1);
                        }
                        return adjustMonth(m1) + "/01/" + adjustYear(m2);
                    });
                    value = value.replace(/^(\d{1,2})\s*(?:\s|\.|\/)\s*(\w+)$/, function (m, m1, m2) {
                        return "01 " + m2 + " " + adjustYear(m1);
                    });
                    value = value.replace(/^(\w+)\s*(?:\s|\.|\/)\s*(\d{1,2})$/, function (m, m1, m2) {
                        return "01 " + m1 + " " + adjustYear(m2);
                    });
                } else {

                    value = value.replace(/^(\d{2})(\d{2})$/, function(m, m1, m2) {
                        return m1 + "/" + m2;
                    });
                    value = value.replace(/^(\d{2})(\d{2})(\d{2}|\d{4})$/, function(m, m1, m2, m3) {
                        return m1 + "/" + m2 + "/" + m3;
                    });

                    value = value.replace(/^(\d+)\s*(?:\s|\.|\/)\s*(\d+)$/, function (m, m1, m2) {
                        if (m1 * 1 > 31 || m2 * 1 > 12) {
                            return m;
                        }
                        return m2 + "/" + m1;
                    });
                    value = value.replace(/^(\d+)\s*(?:\s|\.|\/)\s*(\d+)\s*(?:\s|\.|\/)\s*(\d+)$/, function (m, m1, m2, m3) {
                        if (m1 * 1 > 31 || m2 * 1 > 12) {
                            return m;
                        }
                        return m2 + "/" + m1 + "/" + m3;
                    });
                }

                var m;
                if (value.match(/.*T(\d{2}:)*\d{2}\.\d+Z{0,1}$/)) {
                    m = Moment(value);
                }
                if (!m) {
                    var pvalue = DateJS.parse(value);
                    if (pvalue) {
                        m = Moment(pvalue);
                    }
                }
                if (m && m.isValid()) {
                    value = m.toISOString();
                    var displayval = m.format(this.controloptions.format);
                    var that = this;
                    setTimeout(function(){
                        that.$el.find("input").val(displayval);
                    }, 0);
                }
            }
            return value;
        },
        zformat: function (value) {
            if (value) {
                value = Moment(value).format("DD MMM YYYY");
            }
            //console.log("formatting date");
            //value = "12 Jan 1997";
            return value;
        },
        /**
         * @method validate
         * @memberof control:date
         * @param  {string} value   Date to be validated
         * @param  {object} options Validation options
         * @description Checks
         *
         * - invalid date string
         * - {@link control:date~dateValidateHelper}
         */
        validate: function(value, options) {
            if (!Moment(value).isValid()) {
                this.addError("date.invalid");
            }
            dateValidateHelper(this, "before", value);
            dateValidateHelper(this, "after", value);
        }
    });

/*
Test numbers
http://www.worldpay.com/support/kb/bg/testandgolive/tgl5103.html
http://support.virtuemart-solutions.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=88
http://www.ballyhoo.ltd.uk/Sage-Pay-Test-Credit-Card-Number.html
http://blog.rac.me.uk/2009/02/12/techy-test-credit-and-debit-card-numbers/
http://www.freeformatter.com/credit-card-number-generator-validator.html
http://www.getcreditcardnumbers.com/
http://en.wikipedia.org/wiki/Bank_card_number
http://en.wikipedia.org/wiki/List_of_Bank_Identification_Numbers
https://www.bindb.com/
credit v debit
view-source:https://web.archive.org/web/20130118122930/http://eflo.net/mod10.htm
*/

    /**
     * @member cardtypes
     * @memberof control:payment-card
     * @inner
     * @description Card types object
     *
     * Types defined
     *
     * - amex
     * - dinersclubcarteblanche
     * - dinersclubinternational
     * - jcb
     * - laser
     * - visaprepaid
     * - visaelectron
     * - visadebit
     * - visa
     * - mastercard
     * - maestro
     * - discover
     *
     * @property {regex} pattern a regex matching the card type
     * @property {number|array} length acceptable lengths of number for card type
     * @property {number} [ccv=3] How many digits the security code (CCV|CVV|SC) should be
     * 
     */
    var cardtypes = {
        amex: {
            pattern: /^3[47]/,
            length: 15,
            ccv: 4
        },
        dinersclubcarteblanche: {
            pattern: /^30[0-5]/,
            length: 14
        },
        dinersclubinternational: {
            pattern: /^36/,
            length: 14
        },
        jcb: {
            pattern: /^35(2[89]|[3-8][0-9])/,
            length: 16
        },
        laser: {
            pattern: /^(6304|670[69]|6771)/,
            length: [16, 17, 18, 19],
            type: "debit"
        },
        visaprepaid: {
            pattern: /^(405851|405856|410489|418122|420792|475743|476072|497766)/,
            length: 16,
            type: "debit"
        },
        visaelectron: {
            pattern: /^(4026|417500|4405|4508|4844|491(3|7)|491880)/,
            length: 16,
            type: "debit"
        },
        visadebit: {
            pattern:/^4(01106|01180|013|01795|02802|029|035|03675|03677|03897|04137|04645|05625|05670|05919|060|06632|07441|09908|10773|11636|117|124|1298[45]|14051|14588|15231|15461|15981|16724|18238|18370|19672|20719|20767|20792|20841|20984|213|21473|21494|216|2176[456]|22127|238|23953|23966|251|2543[56]|256|25914|26579|27342|27557|28208|28332|28418|28454|29475|29531|29805,429812|30552|30605|30763|312|3193[012]|31935|31940|31947|3262[456789]|32630|32732|32901|32937|329386|329387|32995|33991|34256|34257|34258|35225|356|35760|36618|36742|37737|382|41104|42729|42730|42742|42790|430|435|43420|43438|43469|451|45785|46053|46106|46157|46261|46268|46272|46274|46277|46278|46279|46291|470|47452|479|48027|48156|48360|490|492|49533|506|507|50875|51368|536|53978|53979|54202|543|54434|54742|549|551|55451|557|560|56351|56403|56406|56413|56414|56432|56445|56475|56726|56735|56738|568|569|58109|58440|585|60005|62239|62263|62288|64944|65345|65858|65859|65861|65901|65904|65921|65942|65943|65944|65946|68805|69568|70132|70758|72409|73099|73354|736|744|74480|75034|75055|75110|75114|75116|75117|75118|75126|75127|75128|75129|75130|75131|75132|75423|75637|75714|75747|760|76[1234]|76225|76559|77462|77548|77596|779|78200|78880|79056|79213|79293|79348|797|79884|800|80119|80686|815|828|82840|83512|83531|83564|83741|841|84823|85342|854|85751|86290|88839|892|90077|90292|903|905|909|911|91859|921|92181|92182|92183|92184|925|929|93414|936|94120|95055|97766|98022|98864|98857|99811)/,
            length: [16,19],
            type: "debit"
        },
        visa: {
            pattern: /^4\d{3}/,
            length: [13,16]
        },
        // can also have 13 digits
        // add \d{3} to prevent early match against visaelectron
        // or add min chars to match against
        // visa debit?
        mastercard: {
            pattern: /^5[1-5]/,
            length: 16
        },
        maestro: {
            pattern: /^(5018|5020|5038|5612|5893|6304|6759|676[1-3]|0604|6390)/,
            length: [12, 13, 14, 15, 16, 17, 18, 19],
            type: "debit"
            //additionalfields: ["startdate", "issuenumber"]
        },
        discover: {
            pattern: /^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)/,
            length: 16
        }
    };
    var ccvDefault = 3;

    /**
     * @member acceptable
     * @memberof control:payment-card
     * @inner
     * @description List of acceptable card types
     *
     * By default
     *
     * - visa
     * - mastercard
     *
     * To override, see {@link control:payment-card}
     */
    var acceptable = ["visa", "mastercard"];
    try {
        acceptable = Bauplan.Config.config.clientdefaults.cardtypes;
    } catch (e) {}


    /**
     * @member additionalfields
     * @memberof control:payment-card
     * @inner
     * @description List of card types which require additional fields (start date, issue number)
     *
     * - maestro
     */
    var additionalfields = ["maestro"];

    for (var ct in cardtypes) {
        if (!cardtypes.type) {
            cardtypes.type = [];
        }
        cardtypes.type.push(ct);
        if (!cardtypes.acceptable) {
            cardtypes.acceptable = [];
        }
        cardtypes.acceptable[ct] = true;
    }
    if (acceptable && acceptable.length) {
        cardtypes.acceptable = {};
        for (var ca = 0, accLength = acceptable.length; ca < accLength; ca++) {
            cardtypes.acceptable[acceptable[ca]] = true;
        }
    }

    /**
     * @method getCardType
     * @memberof control:payment-card
     * @inner
     * @param  {number} input Card long number
     * @return {string} Card type
     */
    var getCardType = function(input) {
        var matched;
        var shortest = 0;
        for (var i = 0, ctLength = cardtypes.type.length; i < ctLength; i++) {
            var cardtype = cardtypes.type[i];
            var matchee = input.match(cardtypes[cardtype].pattern);
            if (matchee && (matchee[0].length > shortest)) {
                shortest = matchee[0].length;
                matched = cardtype;
            }
        }
        return matched;
    };

    /**
     * @method getValidCardType
     * @memberof control:payment-card
     * @inner
     * @param  {number} input Card long number
     * @return {boolean} Whether rard type is acceptable
     */
    var getValidCardType = function(input) {
        var cardtype = getCardType(input);
        return cardtypes.acceptable[cardtype] || false;
    };

    /**
     * @method getValidLength
     * @memberof control:payment-card
     * @inner
     * @param  {number} input Card long number
     * @param  {string} cardtype Card type
     * @return {boolean} Whether input is of correct length
     */
    var getValidLength = function(input, cardtype) {
        var validLength = false;
        if (!cardtype) {
            cardtype = getCardType(input);
        }
        if (!cardtype) {
            return validLength;
        }
        var validLengths = cardtypes[cardtype].length;
        var length = input.length * 1;
        if (typeof validLengths === "number") {
            validLength = length === validLengths;
        } else {
            for (var i = 0; i < validLengths.length; i++) {
                if (length === validLengths[i]) {
                    validLength = true;
                    break;
                }
            }
        }
        return validLength;
    };

    /**
     * @method getValidLuhn
     * @memberof control:payment-card
     * @inner
     * @param  {number} input Card long number
     * @return {boolean} Whether input passes Luhn check
     */
    var getValidLuhn = function(input) {
        var digits = input.split("").reverse();
        var sum = 0;
        //for (var n = 0, _j = 0, dLength = digits.length; _j < dLength; n = ++_j) {
        for (var n = 0, dLength = digits.length; n < dLength; n++) {
            var digit = digits[n];
            digit = +digit;
            if (n % 2) {
                digit *= 2;
                if (digit < 10) {
                    sum += digit;
                } else {
                    sum += digit - 9;
                }
            } else {
                sum += digit;
            }
        }
        return sum % 10 === 0;
    };
    (function($){
        function zapClass($el, regex) {
            $el.removeClass(function(index, css) {
                return (css.match(regex) || []).join(" ");
            });
        }
        $.fn.removePrefixedClass = function(prefix) {
            var prefixregex = new RegExp("\\b" + prefix + "\\S+", "g");
            zapClass(this, prefixregex);
            return this;
        };
        $.fn.removeSuffixedClass = function(suffix) {
            var suffixregex = new RegExp("\\S+" + suffix + "\\b", "g");
            zapClass(this, suffixregex);
            return this;
        };
    })(jQuery);


    /**
    * @control payment-card
    * @controlkind format
    * @description Card number input control
    *
    * If set as format for a property, treats the property as a card’s long number (PAN).
    *
    * Output a date control with the name "foo"
    * 
    *     {{control "foo" control-format="payment-card"}}
    *
    * or just
    *
    *     {{control "foo"}}
    *
    * if the format property for the "foo" property in the model schema has been set to "payment-card"
    *
    * Allows input to contain both spaces and dashes.
    *
    * Validates number entered against acceptable card types, displaying relevant errors.
    *
    * Adds a class representing cardtype matched.
    * 
    * Ensures that a valid CCV (CVV/SCV) is set dependent on the card type.
    * 
    * Currently, the format expects the CCV property to be named *ccv*.
    *
    * To change the acceptable card types, send as part of BauplanData:
    * 
    *     var BauplanData = {
    *         …
    *         clientdefaults: {
    *             cardtypes: [
    *                 "visa",
    *                 "mastercard",
    *                 "amex"
    *             ]
    *         }
    *
    * #### Error codes
    * 
    * - payment-card.undefined-card-type
    * - payment-card.invalid-card-type
    * - payment-card.invalid-length
    * - payment-card.invalid-luhn
    * 
    * @see control:payment-date-expiry
    * @see control:payment-date-start
    * @see template:control%view
    * @see module:bauplan%control%view
    */
    ControlView.addFormat({
        name: "payment-card",
        initialize: {
            instantValidation: true,
            restrictinput: "payment-card",
            showAdditionalFields: function(on) {
                this.controlparent.$el.toggleClass("show-additional", on);
                if (!on) {
                    this.model.unset("issuenumber");
                    this.controlmodel.unset("issuenumber");
                    this.model.unset("startdate");
                    this.controlmodel.unset("startdate");
                }
            },
            showCCV: function(cardType) {
                if (this.controlparent) {
                    var $el = this.controlparent.$el;
                    var ccvLength = cardType ? cardtypes[cardType].ccv : ccvDefault;
                    var $ccvEl = $el.find("[name=ccv]");
                    $ccvEl.view().controlschema.exactLength = ccvLength;
                    if ($ccvEl.attr("maxlength") !== ccvLength) {
                        $ccvEl.attr("maxlength", ccvLength);
                        var ccvPH = "";
                        for (var i = 0; i < ccvLength; i++) {
                            ccvPH += "•";
                        }
                        $ccvEl.attr("placeholder", ccvPH);
                    }
                }
            },
            showCardType: function(cardType) {
                var $el = this.$el;
                var cardprefix = "card-type-";
                if (cardType) {
                    var cardTypeClass = cardprefix + cardType;
                    if (!$el.hasClass(cardTypeClass)) {
                        $el.removePrefixedClass(cardprefix);
                        $el.addClass(cardTypeClass);
                    }
                } else {
                    $el.removePrefixedClass(cardprefix);
                }
                this.showCCV(cardType);
            },
            hideCardType: function() {
                this.showCardType();
            }
        },
        normalize: function(value) {
            if (value) {
                value = "" + value;
                value = value.replace(/[ -]/g, "");
            }
            if (!value) {
                this.hideCardType();
            }
            return value;
        },
        validate: function(value, options) {
            var erroropts = {};
            if (options.instantValidate && !this.model.get("value")) {
                erroropts.display = false;
            }
            // this should really be in control.view
            if (!this.controlparent || !this.controlparent.$el) {
                return;
            }

            var cardType = getCardType(value);
            if (!cardType) {
                this.addError("payment-card.undefined-card-type", erroropts);
                this.hideCardType();
                this.showAdditionalFields(false);
                return;
            } else {
                this.showCardType(cardType);
                //this.addInfo("payment-card.card-type");
            }
            var validCardType = getValidCardType(value);
            if (!validCardType) {
                this.addError("payment-card.invalid-card-type", erroropts);
                this.showAdditionalFields(false);
                return;
            }
            var showAdditional = !!cardtypes[cardType].additionalfields; //_.contains(additionalfields, cardType);
            this.showAdditionalFields(showAdditional);

            var validLength = getValidLength(value);
            if (!validLength) {
                this.addError("payment-card.invalid-length", erroropts);
                return;
            }
            var validLuhn = getValidLuhn(value);
            if (!validLuhn) {
                this.addError("payment-card.invalid-luhn", erroropts);
            } else {
                this.controlmodel.set("cardtype", cardType);
            }
        }
    });

});