/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

/* jshint validthis: true */

"use strict";

var ui = angular.module('axelor.ui');
var widgets = {};
var registry = {};
var metaWidgets = [];

/**
 * Perform common compile operations.
 *
 * example:
 *    ui.formCompile.call(this, element, attrs)
 */
ui.formCompile = function(element, attrs, linkerFn) {

  var showTitle = attrs.showTitle || this.showTitle,
    title = attrs.title || attrs.field;

  attrs.$set('show-title', showTitle, true, 'x-show-title');
  if (title) {
    attrs.$set('title', title, true, 'x-title');
  }
  if (this.cellCss) {
    attrs.$set('x-cell-css', this.cellCss);
  }

  function link(scope, element, attrs, controller) {

    element.addClass(this.css).parent().addClass(this.cellCss);
    element.data('$attrs', attrs); // store the attrs object for event handlers

    var getViewDef = this.getViewDef || scope.getViewDef || function() { return {}; };

    var field = getViewDef.call(scope, element);
    var props = _.extend(_.pick(field, 'readonly,required,hidden,collapse,precision,scale,prompt,title,domain,css,icon,selection-in'.split(',')),
               _.pick(field.widgetAttrs || {}, 'precision,scale,domain'.split(',')));

    var state = _.clone(props);

    function resetAttrs() {
      var label = element.data('label');
      state = _.clone(props);
      state["force-edit"] = false;
      if (label && state.title) {
        var span = label.children('span[ui-help-popover]:first');
        if (span.length === 0) {
          span = label;
        }
        span.html(state.title);
      }
    }

    if (field.css) {
      element.addClass(field.css);
    }
    if (field.width && field.width !== '*' && !element.is('label')) {
      element.width(field.width);
    }
    if (field.translatable) {
      element.addClass("translatable");
    }

    scope.$events = {};
    scope.field = field || {};

    scope.$$readonly = undefined;

    scope.attr = function(name) {
      if (arguments.length > 1) {
        var old = state[name];
        state[name] = arguments[1];
        if (name === "highlight") {
          setHighlight(state.highlight);
        }
        if (old !== state[name]) {
          scope.$broadcast("on:attrs-changed", {
            name: name,
            value: state[name]
          });
        }
      }
      var res = state[name];
      if (res === undefined) {
        res = field[name];
      }
      return res;
    };

    scope.$on("on:edit", function(e, rec) {
      if (angular.equals(rec, {})) {
        resetAttrs();
      }
      scope.$$readonly = scope.$$isReadonly();
    });

    scope.$on("on:attrs-changed", function(event, attr) {
      if (attr.name === "readonly" || attr.name === "force-edit") {
        scope.$$readonly = scope.$$isReadonly();
      }
      if (attr.name === "readonly") {
        element.attr("x-readonly", scope.$$readonly);
      }
    });

    scope.$watch("isEditable()", function isEditableWatch(editable, old) {
      if (editable === undefined) return;
      if (editable === old) return;
      scope.$$readonly = scope.$$isReadonly();
    });

    // js expressions should be evaluated on dummy value changes
    if (field.name && field.name[0] === '$') {
      scope.$watch('record.' + field.name, function fieldValueWatch(a, b) {
        if (a !== b) {
          scope.$broadcastRecordChange();
        }
      });
    }

    scope.isRequired = function() {
      return this.attr("required") && this.text !== 0 && !this.text;
    };

    scope.isReadonlyExclusive = function() {
      var parent = this.$parent || {};
      var readonly = this.attr("readonly");

      if (scope._isPopup && !parent._isPopup) {
        return readonly || false;
      }
      if (parent.isReadonlyExclusive && parent.isReadonlyExclusive()) {
        return true;
      }
      if (readonly !== undefined) {
        return readonly || false;
      }

      return readonly || false;
    };

    scope.isReadonly = function() {
      if (scope.$$readonly === undefined) {
        scope.$$readonly = scope.$$isReadonly();
      }
      return scope.$$readonly;
    };

    scope.$$isReadonly = function() {
      if ((this.hasPermission && !this.hasPermission('read')) || this.isReadonlyExclusive()) {
        return true;
      }
      if (!this.attr("readonly") && this.attr("force-edit")) {
        return false;
      }
      if (scope.isEditable && !scope.isEditable()) {
        return true;
      }
      return this.attr("readonly") || false;
    };

    scope.isHidden = function() {
      return this.attr("hidden") || (this.$parent && this.$parent.isHidden && this.$parent.isHidden()) || false;
    };

    scope.fireAction = function(name, success, error) {
      var handler = this.$events[name];
      if (handler) {
        return handler().then(success, error);
      }
    };

    if (angular.isFunction(this._link_internal)) {
      this._link_internal.call(this, scope, element, attrs, controller);
    }
    if (angular.isFunction(this.init)) {
      this.init.call(this, scope);
    }
    if (angular.isFunction(this.link)) {
      this.link.call(this, scope, element, attrs, controller);
    }

    function hideWidget(hidden) {
      var elem = element,
        parent = elem.parent('td,.form-item'),
        label = elem.data('label') || $(),
        label_parent = label.parent('td,.form-item'),
        isTable = parent.is('td');

      // label scope should use same isHidden method (#1514)
      var lScope = label.data('$scope');
      if (lScope && lScope.isHidden !== scope.isHidden) {
        lScope.isHidden = scope.isHidden;
      }

      elem = isTable && parent.length ? parent : elem;
      label = isTable && label_parent.length ? label_parent : label;

      if (!isTable) {
        parent.toggleClass("form-item-hidden", hidden);
      }

      if (hidden) {
        elem.add(label).hide();
      } else {
        elem.add(label).show().css('display', ''); //XXX: jquery may add display style
      }

      return axelor.$adjustSize();
    }

    var hideFn = _.contains(this.handles, 'isHidden') ? angular.noop : hideWidget;

    var hiddenSet = false;
    scope.$watch("isHidden()", function isHiddenWatch(hidden, old) {
      if (hiddenSet && hidden === old) return;
      hiddenSet = true;
      return hideFn(hidden);
    });

    var readonlySet = false;
    scope.$watch("isReadonly()", function isReadonlyWatch(readonly, old) {
      if (readonlySet && readonly === old) return;
      readonlySet = true;
      element.toggleClass("readonly", readonly);
      element.toggleClass("editable", !readonly);
      if (scope.canEdit) {
        element.toggleClass("no-edit", scope.canEdit() === false);
      }
    });

    function setHighlight(args) {

      function doHilite(params, passed) {
        var label = element.data('label') || $();
        element.toggleClass(params.css, passed);
        label.toggleClass(params.css.replace(/(hilite-[^-]+\b(?!-))/g, ''), passed);
      }

      _.each(field.hilites, function(p) {
        if (p.css) doHilite(p, false);
      });

      if (args && args.hilite && args.hilite.css) {
        doHilite(args.hilite, args.passed);
      }
    }

    this.prepare(scope, element, attrs, controller);

    scope.$evalAsync(function() {
      if (scope.isHidden()) {
        hideFn(true);
      }
    });
  }

  return angular.bind(this, link);
};

ui.formDirective = function(name, object) {

  if (object.compile === undefined) {
    object.compile = angular.bind(object, function(element, attrs){
      return ui.formCompile.apply(this, arguments);
    });
  }

  if (object.restrict === undefined) {
    object.restrict = 'EA';
  }

  if (object.template && !object.replace) {
    object.replace = true;
  }

  if (object.cellCss === undefined) {
    object.cellCss = 'form-item';
  }

  if (object.scope === undefined) {
    object.scope = true;
  }

  if (object.require === undefined) {
    object.require = '?ngModel';
  }

  function prepare_templates($compile) {

    object.prepare = angular.bind(object, function(scope, element, attrs, model) {

      var self = this;

      if (!this.template_editable && !this.template_readonly) {
        return;
      }

      scope.$elem_editable = null;
      scope.$elem_readonly = null;

      function showEditable() {
        var template_editable = self.template_editable;
        if (scope.field && scope.field.editor) {
          template_editable = $('<div ui-panel-editor>');
        }
        if (_.isFunction(self.template_editable)) {
          template_editable = self.template_editable(scope);
        }
        if (!template_editable) {
          return false;
        }
        if (!scope.$elem_editable) {
          scope.$elem_editable = $compile(template_editable)(scope);
          if (self.link_editable) {
            self.link_editable.call(self, scope, scope.$elem_editable, attrs, model);
          }
          if (scope.validate) {
            model.$validators.valid = function(modelValue, viewValue) {
              return !!scope.validate(viewValue);
            };
          }
          // focus the first input field
          if (scope.$elem_editable.is('.input-append,.picker-input')) {
            scope.$elem_editable.on('click', '.btn, i', function(){
              if (!axelor.device.mobile) {
                scope.$elem_editable.find('input:first').focus();
              }
            });
          }

          if (scope.$elem_editable.is(':input')) {
            scope.$elem_editable.attr('placeholder', scope.field.placeholder);
          }

          if (scope.$elem_editable.is('.picker-input:not(.tag-select)')) {
            scope.$elem_editable.find(':input:first').attr('placeholder', scope.field.placeholder);
          }
        }
        if (scope.$elem_readonly) {
          scope.$elem_readonly.detach();
        }
        element.append(scope.$elem_editable);
        if (scope.$render_editable) scope.$render_editable();
        return true;
      }

      function showReadonly() {
        var field = scope.field || {};
        var template_readonly = self.template_readonly;
        if (field.viewer) {
          template_readonly = field.viewer.template;
          scope.$moment = function(d) { return moment(d); };
          scope.$number = function(d) { return +d; };
          scope.$image = function (fieldName, imageName) { return ui.formatters.$image(this, fieldName, imageName); };
          scope.$fmt = function (fieldName, fieldValue) {
            var args = [this, fieldName];
            if (arguments.length > 1) {
              args.push(fieldValue);
            }
            return ui.formatters.$fmt.apply(null, args);
          };
        } else if (field.editor && field.editor.viewer) {
          return showEditable();
        }
        if (_.isFunction(self.template_readonly)) {
          template_readonly = self.template_readonly(scope);
        }
        if (!template_readonly) {
          return false;
        }
        if (_.isString(template_readonly)) {
          template_readonly = axelor.sanitize(template_readonly.trim());
          if (template_readonly[0] !== '<' || $(template_readonly).length > 1) {
            template_readonly = '<span>' + template_readonly + '</span>';
          }
          if (field.viewer) {
            template_readonly = template_readonly.replace(/^(\s*<\w+)/, '$1 ui-panel-viewer');
          }
        }
        if (!scope.$elem_readonly) {
          scope.$elem_readonly = $compile(template_readonly)(scope);
          if (self.link_readonly) {
            self.link_readonly.call(self, scope, scope.$elem_readonly, attrs, model);
          }
        }
        if (scope.$elem_editable) {
          scope.$elem_editable.detach();
        }
        element.append(scope.$elem_readonly);
        return true;
      }

      scope.$watch("isReadonly()", function isReadonlyWatch(readonly) {
        if (readonly && showReadonly()) {
          return;
        }
        return showEditable();
      });
      scope.$watch("isRequired()", function isRequiredWatch(required, old) {
        if (required === old) return;
        var elem = element,
          label = elem.data('label') || $();
        if (label) {
          label.toggleClass('required', required);
        }
        attrs.$set('required', required);
      });

      if (scope.field && scope.field.validIf) {
        scope.$watch("attr('valid')", function attrValidWatch(valid) {
          if (valid === undefined) return;
          model.$setValidity('invalid', valid);
        });
      }

      scope.$on('$destroy', function() {
        if (scope.$elem_editable) {
          scope.$elem_editable.remove();
          scope.$elem_editable = null;
        }
        if (scope.$elem_readonly) {
          scope.$elem_readonly.remove();
          scope.$elem_readonly = null;
        }
      });
    });

    return object;
  }

  return ui.directive(name, ['$compile', function($compile) {
    return prepare_templates($compile);
  }]);
};

var FormItem = {

  css: 'form-item',

  cellCss: 'form-item'
};

var FormInput = {

  _link_internal: function(scope, element, attrs, model) {

    scope.format = function(value) {
      return value;
    };

    scope.parse = function(value) {
      return value;
    };

    scope.validate = function(value) {
      return true;
    };

    scope.setValue = function(value, fireOnChange) {

      var val = this.parse(value);
      var txt = this.format(value);
      var onChange = this.$events.onChange;

      model.$setViewValue(val);
      this.text = txt;

      model.$render();
      if (onChange && fireOnChange) {
        onChange();
      }
    };

    scope.getValue = function() {
      if (model) {
        return model.$viewValue;
      }
      return null;
    };

    scope.getText = function() {
      return this.text;
    };

    scope.initValue = function(value) {
      this.text = this.format(value);
    };

    model.$render = function() {
      scope.initValue(scope.getValue());
      if (scope.$render_editable) {
        scope.$render_editable();
      }
      if (scope.$render_readonly) {
        scope.$render_readonly();
      }
    };

    // Clear invalid fields (use $setPrestine of angular.js 1.1)
    scope.$on('on:new', function(e, rec) {
      if (!model.$valid && model.$viewValue) {
        model.$viewValue = undefined;
        model.$render();
      }
    });
  },

  link: function(scope, element, attrs, model) {

  },

  link_editable: function(scope, element, attrs, model) {

    scope.$render_editable = function() {
      var value = this.format(this.getValue());
      element.val(value);
    };

    function bindListeners() {
      var onChange = scope.$events.onChange || angular.noop,
        onChangePending = false;

      function listener() {
        var value = _.str.trim(element.val()) || null;
        if (value !== model.$viewValue) {
          scope.$applyAsync(function() {
            var val = scope.parse(value);
            var txt = scope.format(value);
            if (scope.$$setEditorValue && !scope.record) { // m2o editor with null value?
              scope.$$setEditorValue({}, false);
            }
            model.$setViewValue(val);
            scope.text = txt;
          });
          onChangePending = true;
        }
      }

      var field = scope.field || {};
      if (!field.bind) {
        element.bind('input', listener);
      }

      element.change(listener);

      element.blur(function(e){
        if (onChangePending) {
          onChangePending = false;
          setTimeout(onChange);
        }
      });
    }

    if (element.is(':input')) {
      setTimeout(bindListeners);
      // clear input value
      if (scope.$$setEditorValue) {
        scope.$on('on:edit', function () {
          if (model.$viewValue && !scope.record) {
            model.$setViewValue(undefined);
            scope.$render_editable();
          }
        });
      }
    }

    scope.$render_editable();
  },

  link_readonly: function(scope, element, attrs, model) {

  },

  template_editable: '<input type="text">',

  template_readonly: '<span class="display-text">{{text}}</span>',

  template: '<span class="form-item-container"></span>'
};

function inherit(array) {

  var args = _.chain(array).rest(1).flatten(true).value();
  var last = _.last(args);
  var base = null;
  var obj = {};

  _.chain(args).each(function(source, index) {
    if (_.isString(source)) {
      source = widgets[source];
    }
    if (index === args.length - 2) {
      base = source;
    }
    _.extend(obj, source);
  });

  if (!base) {
    return obj;
  }

  function overridden(name) {
    return name !== "controller" &&
      _.isFunction(last[name]) && !last[name].$inject &&
      _.isFunction(base[name]);
  }

  function override(name, fn){
    return function() {
      var tmp = this._super;
      this._super = base[name];
      var ret = fn.apply(this, arguments);
      this._super = tmp;
      return ret;
    };
  }

  for(var name in last) {
    if (overridden(name)) {
      obj[name] = override(name, obj[name]);
    }
  }

  return obj;
}

ui.formWidget = function(name, object) {
  var obj = inherit(arguments);
  var widget = _.str.capitalize(name.replace(/^ui/, ''));
  var directive = "ui" + widget;

  if (obj.metaWidget) {
    metaWidgets.push(widget);
  }

  registry[directive] = directive;
  _.each(obj.widgets, function(alias){
    registry[alias] = directive;
  });
  delete obj.widgets;

  widgets[widget] = _.clone(obj);

  ui.formDirective(directive, obj);

  return obj;
};

ui.formItem = function(name, object) {
  return ui.formWidget(name, FormItem, _.rest(arguments, 1));
};

ui.formInput = function(name, object) {
  return ui.formWidget(name, FormItem, FormInput, _.rest(arguments, 1));
};

ui.getWidget = function(type) {
  var name = type,
    widget = registry["ui" + name] || registry[name];
  if (!widget) {
    name = _.str.classify(name);
    widget = registry["ui" + name] || registry[name];
  }
  if (widget) {
    widget = widget.replace(/^ui/, '');
    return _.chain(widget).underscored().dasherize().value();
  }
  return null;
};

ui.getWidgetDef = function (name) {
  return widgets[name];
};

ui.getMetaWidgets = function () {
  return metaWidgets;
};

})();
