{# Renders field for bootstrap 3 standards.

    Params:
        field - WTForm field
        kwargs - pass any arguments you want in order to put them into the html attributes.
        There are few exceptions: for - for_, class - class_, class__ - class_

    Example usage:
        {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
#}
{% macro render_field(field, label_visible=true) -%}

    <div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
        {% if (field.id != 'csrf_token' or field.type != 'HiddenField') %}
            <label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
        {% endif %}
        {{ field(class_='form-control', **kwargs) }}
        {% if field.errors %}
            {% for e in field.errors %}
                <p class="help-block">{{ e }}</p>
            {% endfor %}
        {% endif %}
    </div>
{%- endmacro %}

{# Renders checkbox fields since they are represented differently in bootstrap
    Params:
        field - WTForm field (there are no check, but you should put here only BooleanField.
        kwargs - pass any arguments you want in order to put them into the html attributes.
        There are few exceptions: for - for_, class - class_, class__ - class_

    Example usage:
        {{ macros.render_checkbox_field(form.remember_me) }}
 #}
{% macro render_checkbox_field(field) -%}
    <div class="checkbox">
        <label>
            {{ field(type='checkbox', **kwargs) }} {{ field.label }}
        </label>
    </div>
{%- endmacro %}

{# Renders radio field
    Params:
        field - WTForm field (there are no check, but you should put here only BooleanField.
        kwargs - pass any arguments you want in order to put them into the html attributes.
        There are few exceptions: for - for_, class - class_, class__ - class_

    Example usage:
        {{ macros.render_radio_field(form.answers) }}
 #}
{% macro render_radio_field(field) -%}
    {% for value, label, _ in field.iter_choices() %}
        <div class="radio">
            <label>
                <input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}">{{ label }}
            </label>
        </div>
    {% endfor %}
{%- endmacro %}

{# Renders WTForm in bootstrap way. There are two ways to call function:
     - as macros: it will render all field forms using cycle to iterate over them
     - as call: it will insert form fields as you specify:
     e.g. {% call macros.render_form(form, action_url=url_for('view.login_view'), action_text='Login',
                                        class_='login-form') %}
                {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
                {{ macros.render_field(form.password, placeholder='Input password', type='password') }}
                {{ macros.render_checkbox_field(form.remember_me, type='checkbox') }}
            {% endcall %}

     Params:
        form - WTForm class
        action_url - url where to submit this form
        action_text - text of submit button
        class_ - sets a class for form
    #}
{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-success') -%}

    <form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}">
        {{ form.hidden_tag() if form.hidden_tag }}
        {% if caller %}
            {{ caller() }}
        {% else %}
            {% for f in form %}
                {% if f.type == 'BooleanField' %}
                    {{ render_checkbox_field(f) }}
                {% elif f.type == 'RadioField' %}
                    {{ render_radio_field(f) }}
                {% elif f.type == 'SelectField' %}
                    {{ render_select_field(f) }}
                {% else %}
                    {{ render_field(f) }}
                {% endif %}
            {% endfor %}
        {% endif %}
        <button type="submit" class="{{ btn_class }}">{{ action_text }} </button>
    </form>
{%- endmacro %}

{% macro smooth_scrolling() -%}
// Add a smooth scrolling to anchor
$(document).ready(function() {
	$('a.scroll').click(function() {
		var target = $(this.hash);
		if (target.length == 0) target = $('a[name="' + this.hash.substr(1) + '"]');
		if (target.length == 0) target = $('html');
		$('html, body').animate({ scrollTop: target.offset().top }, 500);
		return false;
	});
	$(".dropdown-menu label, .dropdown-menu input, .dropdown-menu li").click(function(e) {
		e.stopPropagation();
	});
});
{%- endmacro %}

{# Loads the datatable translation if needed #}
{% macro translate_datatable() -%}
  language: {
  {% if g.locale and g.locale != 'en' -%}
    url: '{{ url_for("static", filename="extra/i18n/datatable-{}.json".format(g.locale)) }}',
  {% endif -%}
    select: {
      rows: {
        _: '{{ _("You have selected %%d rows") }}',
        0: '<br />{{ _("Click a row to select it (hold ctrl to select several)") }}',
        1: '{{ _("You have selected 1 row") }}'
      },
    },
    buttons: {
      'selectAll': '{{ _("Select all") }}&nbsp;<i class="fa fa-check-square-o" aria-hidden="true"></i>',
      'selectNone': '{{ _("Deselect all") }}&nbsp;<i class="fa fa-square-o" aria-hidden="true"></i>',
    }
  },
{%- endmacro %}

{# Enables the calendar translation if needed #}
{% macro translate_calendar() -%}
  {% if g.locale and g.locale != 'en' -%}
  locale: {{ g.locale|tojson }},
  {% endif -%}
{%- endmacro %}

{# Record number of elements to display #}
{% macro page_length(selector) -%}
$('{{ selector }}').on('length.dt', function(e, settings, len) {
  $.post( '{{ url_for("api.prefs_ui") }}', {pageLength: len} );
});
{%- endmacro %}

{# Set number of elements to display #}
{% macro get_page_length() -%}
  {% if session.pageLength -%}
  pageLength: {{ session.pageLength }},
  {% endif -%}
{%- endmacro %}

{# Register a new filter for datatables dates #}
{% macro timestamp_filter() -%}
// extends DataTables sorting
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
  "timestamp-pre": function ( a ) {
    // legacy code
    var $obj = $(a);
    var title = $obj.attr('title');
    if (typeof title !== typeof undefined && title !== false)
      return moment(title, moment.ISO_8601).valueOf();

    // we are using the filter the "right" way here
    if (moment(a, moment.ISO_8601).isValid())
      return ''+moment(a, moment.ISO_8601).valueOf();
    // return some string that should be "last"
    if (a === '{{ _("now") }}') {
      return 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'+a;
    }
    return 'zzzzzzzzzzzzzzzzzzzz'+a;
  },
  "timestamp-asc": function ( a, b ) {
    return ((a < b) ? -1 : ((a > b) ? 1 : 0));
  },
  "timestamp-desc": function ( a, b ) {
    return ((a < b) ? 1 : ((a > b) ? -1 : 0));
  }
} );
{%- endmacro %}

{# create a new uiAce angular directove #}
{% macro angular_ui_ace() -%}
app.directive('uiAce', function () {

	if (angular.isUndefined(window.ace)) {
		throw new Error('ui-ace need ace to work... (o rly?)');
	}

	/**
	 * Sets editor options such as the wrapping mode or the syntax checker.
	 *
	 * The supported options are:
	 *
	 *   <ul>
	 *     <li>showGutter</li>
	 *     <li>useWrapMode</li>
	 *     <li>onLoad</li>
	 *     <li>theme</li>
	 *     <li>mode</li>
	 *   </ul>
	 *
	 * @param acee
	 * @param session ACE editor session
	 * @param {object} opts Options to be set
	 */
	var setOptions = function(scope, acee, session, opts) {

		// sets the ace worker path, if running from concatenated
		// or minified source
		if (angular.isDefined(opts.aceWorkerPath)) {
			var config = window.ace.require('ace/config');
			config.set('workerPath', opts.aceWorkerPath);
		}
		// ace requires loading
		if (angular.isDefined(opts.aceRequire)) {
			opts.aceRequire.forEach(function (n) {
					window.ace.require(n);
			});
		}
		// Boolean options
		if (angular.isDefined(opts.aceShowGutter)) {
			acee.renderer.setShowGutter(opts.aceShowGutter);
		}
		if (angular.isDefined(opts.aceUseWrapMode)) {
			session.setUseWrapMode(opts.aceUseWrapMode);
		}
		if (angular.isDefined(opts.aceShowInvisibles)) {
			acee.renderer.setShowInvisibles(opts.aceShowInvisibles);
		}
		if (angular.isDefined(opts.aceShowIndentGuides)) {
			acee.renderer.setDisplayIndentGuides(opts.aceShowIndentGuides);
		}
		if (angular.isDefined(opts.aceUseSoftTabs)) {
			session.setUseSoftTabs(opts.aceUseSoftTabs);
		}
		if (angular.isDefined(opts.aceShowPrintMargin)) {
			acee.setShowPrintMargin(opts.aceShowPrintMargin);
		}
		if (angular.isDefined(opts.readOnly)) {
			acee.setReadOnly(scope.$eval(opts.readOnly) === true);
		}

		// commands
		if (angular.isDefined(opts.aceDisableSearch) && opts.aceDisableSearch) {
			acee.commands.addCommands([
				{
					name: 'unfind',
					bindKey: {
						win: 'Ctrl-F',
						mac: 'Command-F'
					},
					exec: function () {
						return false;
					},
					readOnly: true
				}
			]);
		}

		// Basic options
		if (angular.isString(opts.aceTheme)) {
			acee.setTheme('ace/theme/' + opts.aceTheme);
		}
		if (angular.isString(opts.aceMode)) {
			session.setMode('ace/mode/' + opts.aceMode);
		}
		// Advanced options
		if (angular.isDefined(opts.aceFirstLineNumber)) {
			if (angular.isNumber(opts.aceFirstLineNumber)) {
				session.setOption('firstLineNumber', opts.aceFirstLineNumber);
			} else if (angular.isFunction(opts.aceFirstLineNumber)) {
				session.setOption('firstLineNumber', opts.aceFirstLineNumber());
			}
		}

		// advanced options
		var key, obj;
		if (angular.isDefined(opts.aceAdvanced)) {
				for (key in opts.aceAdvanced) {
						// create a javascript object with the key and value
						obj = { name: key, value: opts.aceAdvanced[key] };
						// try to assign the option to the ace editor
						acee.setOption(obj.name, obj.value);
				}
		}

		// advanced options for the renderer
		if (angular.isDefined(opts.aceRendererOptions)) {
				for (key in opts.aceRendererOptions) {
						// create a javascript object with the key and value
						obj = { name: key, value: opts.aceRendererOptions[key] };
						// try to assign the option to the ace editor
						acee.renderer.setOption(obj.name, obj.value);
				}
		}

		// onLoad callbacks
		angular.forEach(opts.callbacks, function (cb) {
			if (angular.isFunction(cb)) {
				cb(acee);
			}
		});
	};

	return {
		restrict: 'EA',
		require: '?ngModel',
		link: function (scope, elm, attrs, ngModel) {

			/**
			 * uiAceConfig merged with user options via json in attribute or data binding
			 * @type object
			 */
			var opts = attrs;

			/**
			 * ACE editor
			 * @type object
			 */
			var acee = window.ace.edit(elm[0]);

			/**
			 * ACE editor session.
			 * @type object
			 * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
			 */
			var session = acee.getSession();

			/**
			 * Reference to a change listener created by the listener factory.
			 * @function
			 * @see listenerFactory.onChange
			 */
			var onChangeListener;

			/**
			 * Reference to a blur listener created by the listener factory.
			 * @function
			 * @see listenerFactory.onBlur
			 */
			var onBlurListener;

			/**
			 * Calls a callback by checking its existing. The argument list
			 * is variable and thus this function is relying on the arguments
			 * object.
			 * @throws {Error} If the callback isn't a function
			 */
			var executeUserCallback = function () {

				/**
				 * The callback function grabbed from the array-like arguments
				 * object. The first argument should always be the callback.
				 *
				 * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
				 * @type {*}
				 */
				var callback = arguments[0];

				/**
				 * Arguments to be passed to the callback. These are taken
				 * from the array-like arguments object. The first argument
				 * is stripped because that should be the callback function.
				 *
				 * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
				 * @type {Array}
				 */
				var args = Array.prototype.slice.call(arguments, 1);

				if (angular.isDefined(callback)) {
					scope.$evalAsync(function () {
						if (angular.isFunction(callback)) {
							callback(args);
						} else {
							throw new Error('ui-ace use a function as callback.');
						}
					});
				}
			};

			/**
			 * Listener factory. Until now only change listeners can be created.
			 * @type object
			 */
			var listenerFactory = {
				/**
				 * Creates a change listener which propagates the change event
				 * and the editor session to the callback from the user option
				 * onChange. It might be exchanged during runtime, if this
				 * happens the old listener will be unbound.
				 *
				 * @param callback callback function defined in the user options
				 * @see onChangeListener
				 */
				onChange: function (callback) {
					return function (e) {
						var newValue = session.getValue();

						if (ngModel && newValue !== ngModel.$viewValue &&
								// HACK make sure to only trigger the apply outside of the
								// digest loop 'cause ACE is actually using this callback
								// for any text transformation !
								!scope.$$phase && !scope.$root.$$phase) {
							scope.$evalAsync(function () {
								ngModel.$setViewValue(newValue);
							});
						}

						executeUserCallback(callback, e, acee);
					};
				},
				/**
				 * Creates a blur listener which propagates the editor session
				 * to the callback from the user option onBlur. It might be
				 * exchanged during runtime, if this happens the old listener
				 * will be unbound.
				 *
				 * @param callback callback function defined in the user options
				 * @see onBlurListener
				 */
				onBlur: function (callback) {
					return function () {
						executeUserCallback(callback, acee);
					};
				}
			};

			session.on('changeMode', function() {
				session.$worker.on('annotate', function(errors) {
					scope.validGrantInput = errors.data.length <= 0;
					scope.$apply();
				});
			});

			// Value Blind
			if (ngModel) {
				ngModel.$formatters.push(function (value) {
					if (angular.isUndefined(value) || value === null) {
						return '';
					}
					else if (angular.isObject(value) || angular.isArray(value)) {
						throw new Error('ui-ace cannot use an object or an array as a model');
					}
					return value;
				});

				ngModel.$render = function () {
					session.setValue(ngModel.$viewValue);
				};
			}

			// Listen for option updates
			var updateOptions = function (current, previous) {
				if (current === previous) return;
				opts = attrs;

				// EVENTS

				// unbind old change listener
				session.removeListener('change', onChangeListener);

				// bind new change listener
				onChangeListener = listenerFactory.onChange(opts.onChange);
				session.on('change', onChangeListener);

				// unbind old blur listener
				//session.removeListener('blur', onBlurListener);
				acee.removeListener('blur', onBlurListener);

				// bind new blur listener
				onBlurListener = listenerFactory.onBlur(opts.onBlur);
				acee.on('blur', onBlurListener);

				setOptions(scope, acee, session, opts);
			};

			options = [
				'aceWorkerPath',
				'aceRequire',
				'aceShowGutter',
				'aceUseWrapMode',
				'aceShowInvisibles',
				'aceShowIndentGuides',
				'aceUseSoftTabs',
				'aceShowPrintMargin',
				'aceDisableSearch',
				'aceTheme',
				'aceMode',
				'aceFirstLineNumber',
				'aceAdvanced',
				'aceRendererOptions',
			];

			for (var opt in options) {
				attrs.$observe(options[opt], updateOptions);
			}

			scope.$watch('isLoading', function(value) {
				acee.setReadOnly(value === true);
			});

			// scope.$watch(attrs, updateOptions, /* deep watch */ true);

			elm.on('$destroy', function () {
				acee.session.$stopWorker();
				acee.destroy();
			});

			scope.$watch(function() {
				return [elm[0].offsetWidth, elm[0].offsetHeight];
			}, function() {
				acee.resize();
				acee.renderer.updateFull();
			}, true);

		}
	}
});
{%- endmacro %}
