define([
  'underscore', 'underscore.string', 'sources/pgadmin', 'jquery', 'backbone',
], function(_, S, pgAdmin, $, Backbone) {
  var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};

  pgBrowser.DataModel = Backbone.Model.extend({
    /*
     * Parsing the existing data
     */
    on_server: false,
    parse: function(res) {
      var self = this;
      if (res && _.isObject(res) && 'node' in res && res['node']) {
        self.tnode = _.extend({}, res.node);
        delete res.node;
      }
      var objectOp = function(schema) {
        if (schema && _.isArray(schema)) {
          _.each(schema, function(s) {
            var obj, val;
            switch (s.type) {
            case 'collection':
              obj = self.get(s.id);
              val = res[s.id];
              if (_.isArray(val) || _.isObject(val)) {
                if (!obj || !(obj instanceof Backbone.Collection)) {
                  obj = new(pgBrowser.Node.Collection)(val, {
                    model: ((_.isString(s.model) &&
                          s.model in pgBrowser.Nodes) ?
                        pgBrowser.Nodes[s.model].model : s.model),
                    top: self.top || self,
                    handler: self,
                    parse: true,
                    silent: true,
                    attrName: s.id,
                  });

                    /*
                     * Nested collection models may or may not have idAttribute.
                     * So to decide whether model is new or not set 'on_server'
                     * flag on such models.
                     */

                  self.set(s.id, obj, {
                    silent: true,
                    parse: true,
                    on_server: true,
                  });
                } else {
                    /*
                     * Nested collection models may or may not have idAttribute.
                     * So to decide whether model is new or not set 'on_server'
                     * flag on such models.
                     */
                  obj.reset(val, {
                    silent: true,
                    parse: true,
                    on_server: true,
                  });
                }
              } else {
                obj = null;
              }
              self.set(s.id, obj, {
                silent: true,
              });
              res[s.id] = obj;
              break;
            case 'model':
              obj = self.get(s.id);
              val = res[s.id];
              if (!_.isUndefined(val) && !_.isNull(val)) {
                if (!obj || !(obj instanceof Backbone.Model)) {
                  if (_.isString(s.model) &&
                      s.model in pgBrowser.Nodes[s.model]) {
                    obj = new(pgBrowser.Nodes[s.model].Model)(
                        obj, {
                          silent: true,
                          top: self.top || self,
                          handler: self,
                          attrName: s.id,
                        }
                      );
                  } else {
                    obj = new(s.model)(obj, {
                      silent: true,
                      top: self.top || self,
                      handler: self,
                      attrName: s.id,
                    });
                  }
                }
                obj.set(val, {
                  parse: true,
                  silent: true,
                });
              } else {
                obj = null;
              }
              res[s.id] = obj;
              break;
            case 'nested':
              objectOp(s.schema);

              break;
            default:
              break;
            }
          });
        }
      };

      objectOp(self.schema);

      return res;
    },
    isNew: function() {
      if (this.has(this.idAttribute)) {
        return !this.has(this.idAttribute);
      }
      return !this.on_server;
    },
    primary_key: function() {
      if (this.keys && _.isArray(this.keys)) {
        var res = {},
          self = this;

        _.each(self.keys, function(k) {
          res[k] = self.attributes[k];
        });

        return JSON.stringify(res);
      }
      return this.cid;
    },
    initialize: function(attributes, options) {
      var self = this;
      self._previous_key_values = {};

      if (!_.isUndefined(options) && 'on_server' in options && options.on_server) {
        self.on_server = true;
      }

      Backbone.Model.prototype.initialize.apply(self, arguments);

      if (_.isUndefined(options) || _.isNull(options)) {
        options = attributes || {};
        attributes = null;
      }

      self.sessAttrs = {};
      self.fieldData = {};
      self.origSessAttrs = {};
      self.objects = [];
      self.arrays = [];
      self.attrName = options.attrName,
        self.top = (options.top || self.collection && self.collection.top || self.collection || self);
      self.handler = options.handler ||
        (self.collection && self.collection.handler);
      self.trackChanges = false;
      self.errorModel = new Backbone.Model();
      self.node_info = options.node_info;

      var obj;
      var objectOp = function(schema) {
        if (schema && _.isArray(schema)) {
          _.each(schema, function(s) {

            switch (s.type) {
            case 'int':
            case 'numeric':
              self.fieldData[s.id] = {
                id: s.id,
                label: s.label,
                type: s.type,
                min: s.min || undefined,
                max: s.max || undefined,
              };
              break;
            default:
              self.fieldData[s.id] = {
                id: s.id,
                label: s.label,
                type: s.type,
              };
            }

            switch (s.type) {
            case 'array':
              self.arrays.push(s.id);

              break;
            case 'collection':
              obj = self.get(s.id);
              if (!obj || !(obj instanceof pgBrowser.Node.Collection)) {
                if (_.isString(s.model) &&
                    s.model in pgBrowser.Nodes) {
                  var node = pgBrowser.Nodes[s.model];
                  obj = new(node.Collection)(obj, {
                    model: node.model,
                    top: self.top || self,
                    handler: self,
                    attrName: s.id,
                  });
                } else {
                  obj = new(pgBrowser.Node.Collection)(obj, {
                    model: s.model,
                    top: self.top || self,
                    handler: self,
                    attrName: s.id,
                  });
                }
              }

              obj.name = s.id;
              self.objects.push(s.id);
              self.set(s.id, obj, {
                silent: true,
              });

              break;
            case 'model':
              obj = self.get(s.id);
              if (!obj || !(obj instanceof Backbone.Model)) {
                if (_.isString(s.model) &&
                    s.model in pgBrowser.Nodes[s.model]) {
                  obj = new(pgBrowser.Nodes[s.model].Model)(
                      obj, {
                        top: self.top || self,
                        handler: self,
                        attrName: s.id,
                      }
                    );
                } else {
                  obj = new(s.model)(
                      obj, {
                        top: self.top || self,
                        handler: self,
                        attrName: s.id,
                      });
                }
              }

              obj.name = s.id;
              self.objects.push(s.id);
              self.set(s.id, obj, {
                silent: true,
              });

              break;
            case 'nested':
              objectOp(s.schema);
              break;
            default:
              return;
            }
          });
        }
      };
      objectOp(self.schema);

      if (self.handler && self.handler.trackChanges) {
        self.startNewSession();
      }

      if ('keys' in self && _.isArray(self.keys)) {
        _.each(self.keys, function(key) {
          self.on('change:' + key, function(m) {
            self._previous_key_values[key] = m.previous(key);
          });
        });
      }
      return self;
    },
    // Create a reset function, which allow us to remove the nested object.
    reset: function(opts) {
      var obj;

      if (opts && opts.stop)
        this.stopSession();

      for (var id in this.objects) {
        obj = this.get(id);

        if (obj) {
          if (obj instanceof pgBrowser.DataModel) {
            obj.reset(opts);
          } else if (obj instanceof Backbone.Model) {
            obj.clear(opts);
          } else if (obj instanceof pgBrowser.DataCollection) {
            obj.reset(opts);
          } else if (obj instanceof Backbone.Collection) {
            obj.each(function(m) {
              if (m instanceof Backbone.DataModel) {
                obj.reset();
                obj.clear(opts);
              }
            });
            if (!(opts instanceof Array)) {
              opts = [opts];
            }
            Backbone.Collection.prototype.reset.apply(obj, opts);
          }
        }
      }
      this.clear(opts);
    },
    sessChanged: function() {
      var self = this;

      return (_.size(self.sessAttrs) > 0 ||
        _.some(self.objects, function(k) {
          var obj = self.get(k);
          if (!(_.isNull(obj) || _.isUndefined(obj))) {
            return obj.sessChanged();
          }
          return false;
        }));
    },
    sessValid: function() {
      var self = this;
      // Perform default validations.
      if ('default_validate' in self && typeof(self.default_validate) == 'function' &&
        _.isString(self.default_validate())) {
        return false;
      }

      if ('validate' in self && _.isFunction(self.validate) &&
        _.isString(self.validate.apply(self))) {
        return false;
      }
      return true;
    },
    set: function(key, val, options) {
      var opts = _.isObject(key) ? val : options;

      this._changing = true;
      this._previousAttributes = _.clone(this.attributes);
      this.changed = {};

      var res = Backbone.Model.prototype.set.call(this, key, val, options);
      this._changing = false;

      if ((opts && opts.internal) || !this.trackChanges) {
        return true;
      }

      if (key != null && res) {
        var attrs = {},
          self = this,
          msg;

        var attrChanged = function(v, k) {
          if (k in self.objects) {
            return;
          }
          attrs[k] = v;
          if (_.isEqual(self.origSessAttrs[k], v)) {
            delete self.sessAttrs[k];
          } else {
            self.sessAttrs[k] = v;
          }
        };

        // Handle both `"key", value` and `{key: value}` -style arguments.
        if (typeof key === 'object') {
          _.each(key, attrChanged);
        } else {
          attrChanged(val, key);
        }

        self.trigger('pgadmin-session:set', self, attrs);
        if (!options || !options.silent) {
          self.trigger('change', self, options);
        }

        // Perform default validations.

        if ('default_validate' in self && typeof(self.default_validate) == 'function') {
          msg = self.default_validate();
        }

        if ('validate' in self && typeof(self['validate']) === 'function') {

          if (!msg) {
            msg = self.validate(_.keys(attrs));
          }
        }

        /*
         * If any parent present, we will need to inform the parent - that
         * I have some issues/fixed the issue.
         *
         * If not parent found, we will raise the issue
         */
        if (_.size(self.errorModel.attributes) == 0) {
          if (self.collection || self.handler) {
            (self.collection || self.handler).trigger(
              'pgadmin-session:model:valid', self, (self.collection || self.handler)
            );
          } else {
            self.trigger('pgadmin-session:valid', self.sessChanged(), self);
          }
        } else {
          msg = msg || _.values(self.errorModel.attributes)[0];
          if (self.collection || self.handler) {
            (self.collection || self.handler).trigger(
              'pgadmin-session:model:invalid', msg, self, (self.collection || self.handler)
            );
          } else {
            self.trigger('pgadmin-session:invalid', msg, self);
          }
        }

        return res;
      }
      return res;
    },
    /*
     * We do support modified data only through session by tracking changes.
     *
     * In normal mode, we will use the toJSON function of Backbone.Model.
     * In session mode, we will return all the modified data only. And, the
     * objects (collection, and model) will be return as stringified JSON,
     * only from the parent object.
     */
    toJSON: function(session, method) {
      var self = this,
        res, isNew = self.isNew();

      session = (typeof(session) != 'undefined' && session == true);

      if (!session || isNew) {
        res = Backbone.Model.prototype.toJSON.call(this, arguments);
      } else {
        res = {};
        res[self.idAttribute || '_id'] = self.get(self.idAttribute || '_id');
        res = _.extend(res, self.sessAttrs);
      }

      /*
       * We do have number objects (models, collections), which needs to be
       * converted to JSON data manually.
       */
      _.each(
        self.objects,
        function(k) {
          var obj = self.get(k);
          /*
           * For session changes, we only need the modified data to be
           * transformed to JSON data.
           */
          if (session) {
            if (res[k] instanceof Array) {
              res[k] = JSON.stringify(res[k]);
            } else if ((obj && obj.sessChanged && obj.sessChanged()) || isNew) {
              res[k] = obj && obj.toJSON(!isNew);
              /*
               * We will run JSON.stringify(..) only from the main object,
               * not for the JSON object within the objects, that only when
               * HTTP method is 'GET'.
               *
               * We do stringify the object, so that - it will not be
               * translated to wierd format using jQuery.
               */
              if (obj && method && method == 'GET') {
                res[k] = JSON.stringify(res[k]);
              }
            } else {
              delete res[k];
            }
          } else if (!(res[k] instanceof Array)) {
            res[k] = (obj && obj.toJSON());
          }
        });
      if (session) {
        _.each(
          self.arrays,
          function(a) {
            /*
             * For session changes, we only need the modified data to be
             * transformed to JSON data.
             */
            if (res[a] && res[a] instanceof Array) {
              res[a] = JSON.stringify(res[a]);
            }
          });
      }
      return res;
    },
    startNewSession: function() {
      var self = this;

      if (self.trackChanges) {
        self.trigger('pgadmin-session:stop', self);
        self.off('pgadmin-session:model:invalid', self.onChildInvalid);
        self.off('pgadmin-session:model:valid', self.onChildValid);
        self.off('pgadmin-session:changed', self.onChildChanged);
        self.off('pgadmin-session:added', self.onChildChanged);
        self.off('pgadmin-session:removed', self.onChildChanged);
      }

      self.trackChanges = true;
      self.sessAttrs = {};
      self.origSessAttrs = _.clone(self.attributes);

      _.each(self.objects, function(o) {
        var obj = self.get(o);

        if (_.isUndefined(obj) || _.isNull(obj)) {
          return;
        }

        delete self.origSessAttrs[o];

        if (obj && 'startNewSession' in obj && _.isFunction(obj.startNewSession)) {
          obj.startNewSession();
        }
      });

      // Let people know, I have started session hanlding
      self.trigger('pgadmin-session:start', self);

      // Let me listen to the my child invalid/valid messages
      self.on('pgadmin-session:model:invalid', self.onChildInvalid);
      self.on('pgadmin-session:model:valid', self.onChildValid);
      self.on('pgadmin-session:changed', self.onChildChanged);
      self.on('pgadmin-session:added', self.onChildChanged);
      self.on('pgadmin-session:removed', self.onChildChanged);
    },

    onChildInvalid: function(msg, obj) {
      var self = this;

      if (self.trackChanges && obj) {
        var objName = obj.attrName;

        if (!objName) {
          var hasPrimaryKey = obj.primary_key &&
            typeof(obj.primary_key) === 'function';
          var key = hasPrimaryKey ? obj.primary_key() : obj.cid,
            comparator = hasPrimaryKey ?
            function(k) {
              var o = self.get('k');

              if (o && o.primary_key() === key) {
                objName = k;
                return true;
              }

              return false;
            } :
            function(k) {
              var o = self.get(k);

              if (o.cid === key) {
                objName = k;
                return true;
              }

              return false;
            };
          _.findIndex(self.objects, comparator);
        }

        if (objName) {
          /*
           * Update the error message for this object.
           */
          self.errorModel.set(objName, msg);

          if (self.handler) {
            (self.handler).trigger('pgadmin-session:model:invalid', msg, self, self.handler);
          } else {
            self.trigger('pgadmin-session:invalid', msg, self);
          }
        }
      }

      return this;
    },
    onChildValid: function(obj) {
      var self = this;

      if (self.trackChanges && obj) {
        var objName = obj.attrName;

        if (!objName) {
          var hasPrimaryKey = (obj.primary_key &&
            (typeof(obj.primary_key) === 'function'));
          var key = hasPrimaryKey ? obj.primary_key() : obj.cid,
            comparator = hasPrimaryKey ?
            function(k) {
              var o = self.get('k');

              if (o && o.primary_key() === key) {
                objName = k;
                return true;
              }

              return false;
            } :
            function(k) {
              var o = self.get('k');

              if (o && o.cid === key) {
                objName = k;
                return true;
              }

              return false;
            };

          _.findIndex(self.objects, comparator);
        }

        var msg = null,
          validate = function(m, attrs) {
            if ('default_validate' in m && typeof(m.default_validate) == 'function') {
              msg = m.default_validate();
              if (_.isString(msg)) {
                return msg;
              }
            }

            if ('validate' in m && typeof(m.validate) == 'function') {
              msg = m.validate(attrs);

              return msg;
            }
            return null;
          };

        if (obj instanceof Backbone.Collection) {
          for (var idx in obj.models) {
            if (validate(obj.models[idx]))
              break;
          }
        } else if (obj instanceof Backbone.Model) {
          validate(obj);
        }

        if (objName && self.errorModel.has(objName)) {
          if (!msg) {
            self.errorModel.unset(objName);
          } else {
            self.errorModel.set(objName, msg);
          }
        }

        /*
         * The object is valid, but does this has any effect on parent?
         * Let's validate this object itself, before making it clear.
         *
         * We will trigger validation information.
         */
        if (_.size(self.errorModel.attributes) == 0 &&
          !validate(self, (objName && [objName]))) {
          if (self.handler) {
            (self.handler).trigger('pgadmin-session:model:valid', self, self.handler);
          } else {
            self.trigger(
              'pgadmin-session:valid', self.sessChanged(), self
            );
          }
        } else {
          msg = msg || _.values(self.errorModel.attributes)[0];

          if (self.handler) {
            (self.handler).trigger(
              'pgadmin-session:model:invalid', msg, self, self.handler
            );
          } else {
            self.trigger('pgadmin-session:invalid', msg, self);
          }
        }
      }
    },

    onChildChanged: function() {
      var self = this;

      if (self.trackChanges && self.collection) {
        (self.collection).trigger('change', self);
      }
    },

    stopSession: function() {
      var self = this;

      if (self.trackChanges) {
        self.off('pgadmin-session:model:invalid', self.onChildInvalid);
        self.off('pgadmin-session:model:valid', self.onChildValid);
        self.off('pgadmin-session:changed', self.onChildChanged);
        self.off('pgadmin-session:added', self.onChildChanged);
        self.off('pgadmin-session:removed', self.onChildChanged);
      }

      self.trackChanges = false;
      self.sessAttrs = {};
      self.origSessAttrs = {};

      _.each(self.objects, function(o) {
        var obj = self.get(o);

        if (_.isUndefined(obj) || _.isNull(obj)) {
          return;
        }

        self.origSessAttrs[o] = null;
        delete self.origSessAttrs[o];

        if (obj && 'stopSession' in obj && _.isFunction(obj.stopSession)) {
          obj.stopSession();
        }
      });

      self.trigger('pgadmin-session:stop');
    },
    default_validate: function() {
      var msg, field, value, type;

      for (var i = 0, keys = _.keys(this.attributes), l = keys.length; i < l; i++) {

        value = this.attributes[keys[i]];
        field = this.fieldData[keys[i]];
        msg = null;

        if (!(_.isUndefined(value) || _.isNull(value) ||
            String(value).replace(/^\s+|\s+$/g, '') == '')) {

          if (!field) {
            continue;
          }

          type = field.type || undefined;
          if (!type) {
            continue;
          }

          switch (type) {
          case 'int':
            msg = this.integer_validate(value, field);
            break;
          case 'numeric':
            msg = this.number_validate(value, field);
            break;
          }

          if (msg) {
            this.errorModel.set(field.id, msg);
            return msg;
          } else {
            this.errorModel.unset(field.id);
          }
        } else {
          if (field) {
            this.errorModel.unset(field.id);
          }
        }
      }
      return null;
    },

    check_min_max: function(value, field) {
      var label = field.label,
        min_value = field.min,
        max_value = field.max;

      if (min_value && value < min_value) {
        return S(pgAdmin.Browser.messages.MUST_GR_EQ).sprintf(label, min_value).value();
      } else if (max_value && value > max_value) {
        return S(pgAdmin.Browser.messages.MUST_LESS_EQ).sprintf(label, max_value).value();
      }
      return null;
    },
    number_validate: function(value, field) {
      var pattern = new RegExp('^-?[0-9]+(\.?[0-9]*)?$');
      if (!pattern.test(value)) {
        return S(pgAdmin.Browser.messages.MUST_BE_NUM).sprintf(field.label).value();
      }
      return this.check_min_max(value, field);
    },
    integer_validate: function(value, field) {
      var pattern = new RegExp('^-?[0-9]*$');
      if (!pattern.test(value)) {
        return S(pgAdmin.Browser.messages.MUST_BE_INT).sprintf(field.label).value();
      }
      return this.check_min_max(value, field);
    },
  });

  pgBrowser.DataCollection = Backbone.Collection.extend({
    // Model collection
    initialize: function(attributes, options) {
      var self = this;

      options = options || {};
      /*
       * Session changes will be kept in this object.
       */
      self.sessAttrs = {
        'changed': [],
        'added': [],
        'deleted': [],
        'invalid': [],
      };
      self.top = options.top || self;
      self.attrName = options.attrName;
      self.handler = options.handler;
      self.trackChanges = false;

      /*
       * Listen to the model changes for the session changes.
       */
      self.on('add', self.onModelAdd);
      self.on('remove', self.onModelRemove);
      self.on('change', self.onModelChange);

      /*
       * We need to start the session, if the handler is already in session
       * tracking mode.
       */
      if (self.handler && self.handler.trackChanges) {
        self.startNewSession();
      }

      return self;
    },
    startNewSession: function() {
      var self = this,
        msg;

      if (self.trackChanges) {
        // We're stopping the existing session.
        self.trigger('pgadmin-session:stop', self);

        self.off('pgadmin-session:model:invalid', self.onModelInvalid);
        self.off('pgadmin-session:model:valid', self.onModelValid);
      }

      self.trackChanges = true;
      self.sessAttrs = {
        'changed': [],
        'added': [],
        'deleted': [],
        'invalid': [],
      };

      _.each(self.models, function(m) {
        if ('startNewSession' in m && _.isFunction(m.startNewSession)) {
          m.startNewSession();
        }

        if ('default_validate' in m && typeof(m.default_validate) == 'function') {
          msg = m.default_validate();
        }

        if (_.isString(msg)) {
          self.sessAttrs['invalid'][m.cid] = msg;
        } else if ('validate' in m && typeof(m.validate) === 'function') {
          msg = m.validate();

          if (msg) {
            self.sessAttrs['invalid'][m.cid] = msg;
          }
        }
      });

      // Let people know, I have started session hanlding
      self.trigger('pgadmin-session:start', self);

      self.on('pgadmin-session:model:invalid', self.onModelInvalid);
      self.on('pgadmin-session:model:valid', self.onModelValid);
    },
    onModelInvalid: function(msg, m) {
      var self = this,
        invalidModels = self.sessAttrs['invalid'];

      if (self.trackChanges) {
        // Do not add the existing invalid object
        invalidModels[m.cid] = msg;

        // Inform the parent that - I am an invalid model.
        if (self.handler) {
          (self.handler).trigger('pgadmin-session:model:invalid', msg, self, self.handler);
        } else {
          self.trigger('pgadmin-session:invalid', msg, self);
        }
      }

      return true;
    },
    onModelValid: function(m) {
      var self = this,
        invalidModels = self.sessAttrs['invalid'];

      if (self.trackChanges) {
        // Now check uniqueness of current model with other models.
        var isUnique = self.checkDuplicateWithModel(m);

        // If unique then find the object the invalid list, if found remove it from the list
        // and inform the parent that - I am a valid object now.

        if (isUnique && m.cid in invalidModels) {
          delete invalidModels[m.cid];
        }

        if (isUnique) {
          this.triggerValidationEvent.apply(this);
        }
      }

      return true;
    },
    stopSession: function() {
      var self = this;

      self.trackChanges = false;
      self.sessAttrs = {
        'changed': [],
        'added': [],
        'deleted': [],
        'invalid': [],
      };

      _.each(self.models, function(m) {
        if ('stopSession' in m && _.isFunction(m.stopSession)) {
          m.stopSession();
        }
      });
    },
    sessChanged: function() {
      return (
        this.sessAttrs['changed'].length > 0 ||
        this.sessAttrs['added'].length > 0 ||
        this.sessAttrs['deleted'].length > 0
      );
    },
    /*
     * We do support the changes through session tracking in general.
     *
     * In normal mode, we will use the general toJSON(..) function of
     * Backbone.Colletion.
     *
     * In session mode, we will return session changes as:
     * We will be returning the session changes as:
     * {
     *  'added': [JSON of each new model],
     *  'delete': [JSON of each deleted model],
     *  'changed': [JSON of each modified model with session changes]
     * }
     */
    toJSON: function(session) {
      var self = this;
      session = (typeof(session) != 'undefined' && session == true);

      if (!session) {
        return Backbone.Collection.prototype.toJSON.call(self);
      } else {
        var res = {};

        res['added'] = [];
        _.each(this.sessAttrs['added'], function(o) {
          res['added'].push(o.toJSON());
        });
        if (res['added'].length == 0) {
          delete res['added'];
        }
        res['changed'] = [];
        _.each(self.sessAttrs['changed'], function(o) {
          res['changed'].push(o.toJSON(true));
        });
        if (res['changed'].length == 0) {
          delete res['changed'];
        }
        res['deleted'] = [];
        _.each(self.sessAttrs['deleted'], function(o) {
          res['deleted'].push(o.toJSON());
        });
        if (res['deleted'].length == 0) {
          delete res['deleted'];
        }

        return (_.size(res) == 0 ? null : res);
      }
    },
    // Override the reset function, so that - we can reset the model
    // properly.
    reset: function(opts) {
      if (opts && opts.stop)
        this.stopSession();
      this.each(function(m) {
        if (!m)
          return;
        if (m instanceof pgBrowser.DataModel) {
          m.reset(opts);
        } else {
          m.clear(opts);
        }
      });
      Backbone.Collection.prototype.reset.apply(this, arguments);
    },
    objFindInSession: function(m, type) {
      var hasPrimaryKey = m.primary_key &&
        typeof(m.primary_key) == 'function',
        key = hasPrimaryKey ? m.primary_key() : m.cid,
        comparator = hasPrimaryKey ? function(o) {
          return (o.primary_key() === key);
        } : function(o) {
          return (o.cid === key);
        };

      return (_.findIndex(this.sessAttrs[type], comparator));
    },
    onModelAdd: function(obj) {
      if (!this.trackChanges)
        return true;

      var self = this,
        msg,
        idx = self.objFindInSession(obj, 'deleted');

      // Hmm.. - it was originally deleted from this collection, we should
      // remove it from the 'deleted' list.
      if (idx >= 0) {
        var origObj = self.sessAttrs['deleted'][idx];

        obj.origSessAttrs = _.clone(origObj.origSessAttrs);
        obj.attributes = _.extend(obj.attributes, origObj.attributes);
        obj.sessAttrs = _.clone(origObj.sessAttrs);

        self.sessAttrs['deleted'].splice(idx, 1);

        // It has been changed originally!
        if ((!('sessChanged' in obj)) || obj.sessChanged()) {
          self.sessAttrs['changed'].push(obj);
        }

        (self.handler || self).trigger('pgadmin-session:added', self, obj);


        if ('default_validate' in obj && typeof(obj.default_validate) == 'function') {
          msg = obj.default_validate();
        }

        if (_.isString(msg)) {
          (self.sessAttrs['invalid'])[obj.cid] = msg;
        } else if ('validate' in obj && typeof(obj.validate) === 'function') {
          msg = obj.validate();

          if (msg) {
            (self.sessAttrs['invalid'])[obj.cid] = msg;
          }
        }
      } else {

        if ('default_validate' in obj && typeof(obj.default_validate) == 'function') {
          msg = obj.default_validate();
        }

        if (_.isString(msg)) {
          (self.sessAttrs['invalid'])[obj.cid] = msg;
        } else if ('validate' in obj && typeof(obj.validate) === 'function') {
          msg = obj.validate();

          if (msg) {
            (self.sessAttrs['invalid'])[obj.cid] = msg;
          }
        }
        self.sessAttrs['added'].push(obj);

        /*
         * Session has been changed
         */
        (self.handler || self).trigger('pgadmin-session:added', self, obj);
      }

      // Let the parent/listener know about my status (valid/invalid).
      this.triggerValidationEvent.apply(this);

      return true;
    },
    onModelRemove: function(obj) {
      if (!this.trackChanges)
        return true;

      /* Once model is removed from collection clear its errorModel as it's no longer relevant
       * for us. Otherwise it creates problem in 'clearInvalidSessionIfModelValid' function.
       */
      obj.errorModel.clear();

      var self = this,
        invalidModels = self.sessAttrs['invalid'],
        copy = _.clone(obj),
        idx = self.objFindInSession(obj, 'added');

      // We need to remove it from the invalid object list first.
      if (obj.cid in invalidModels) {
        delete invalidModels[obj.cid];
      }

      // Hmm - it was newly added, we can safely remove it.
      if (idx >= 0) {
        self.sessAttrs['added'].splice(idx, 1);

        (self.handler || self).trigger('pgadmin-session:removed', self, copy);

        self.checkDuplicateWithModel(copy);

        // Let the parent/listener know about my status (valid/invalid).
        this.triggerValidationEvent.apply(this);

        return true;
      }

      // Hmm - it was changed in this session, we should remove it from the
      // changed models.
      idx = self.objFindInSession(obj, 'changed');

      if (idx >= 0) {
        self.sessAttrs['changed'].splice(idx, 1);
        (self.handler || self).trigger('pgadmin-session:removed', self, copy);
      } else {
        (self.handler || self).trigger('pgadmin-session:removed', self, copy);
      }

      self.sessAttrs['deleted'].push(obj);

      self.checkDuplicateWithModel(obj);

      // Let the parent/listener know about my status (valid/invalid).
      this.triggerValidationEvent.apply(this);

      /*
       * This object has been remove, we need to check (if we still have any
       * other invalid message pending).
       */

      return true;
    },
    triggerValidationEvent: function() {
      var self = this,
        msg = null,
        invalidModels = self.sessAttrs['invalid'],
        validModels = [];

      for (var key in invalidModels) {
        msg = invalidModels[key];
        if (msg) {
          break;
        } else {
          // Hmm..
          // How come - you have been assigned in invalid list.
          // I will make a list of it, and remove it later.
          validModels.push(key);
        }
      }

      // Let's remove the un
      for (key in validModels) {
        delete invalidModels[validModels[key]];
      }

      if (!msg) {
        if (self.handler) {
          self.handler.trigger('pgadmin-session:model:valid', self, self.handler);
        } else {
          self.trigger('pgadmin-session:valid', self.sessChanged(), self);
        }
      } else {
        if (self.handler) {
          self.handler.trigger('pgadmin-session:model:invalid', msg, self, self.handler);
        } else {
          self.trigger('pgadmin-session:invalid', msg, self);
        }
      }
    },
    onModelChange: function(obj) {
      var self = this;

      if (!this.trackChanges || !(obj instanceof pgBrowser.Node.Model))
        return true;

      var idx = self.objFindInSession(obj, 'added');

      // It was newly added model, we don't need to add into the changed
      // list.
      if (idx >= 0) {
        (self.handler || self).trigger('pgadmin-session:changed', self, obj);

        return true;
      }

      idx = self.objFindInSession(obj, 'changed');

      if (!('sessChanged' in obj)) {
        (self.handler || self).trigger('pgadmin-session:changed', self, obj);

        if (idx >= 0) {
          return true;
        }

        self.sessAttrs['changed'].push(obj);

        return true;
      }

      if (idx >= 0) {

        if (!obj.sessChanged()) {
          // This object is no more updated, removing it from the changed
          // models list.
          self.sessAttrs['changed'].splice(idx, 1);

          (self.handler || self).trigger('pgadmin-session:changed', self, obj);
          return true;
        }

        (self.handler || self).trigger('pgadmin-session:changed', self, obj);

        return true;
      }

      if (obj.sessChanged()) {
        self.sessAttrs['changed'].push(obj);
        (self.handler || self).trigger('pgadmin-session:changed', self, obj);
      }

      return true;
    },

    /*
     * This function will check if given model is unique or duplicate in
     * collection and set/clear duplicate errors on models.
     */
    checkDuplicateWithModel: function(model) {
      if (!('keys' in model) || _.isEmpty(model.keys)) {
        return true;
      }

      var self = this,
        condition = {},
        previous_condition = {};

      _.each(model.keys, function(key) {
        condition[key] = model.get(key);
        if (key in model._previous_key_values) {
          previous_condition[key] = model._previous_key_values[key];
        } else {
          previous_condition[key] = model.previous(key);
        }
      });

      // Reset previously changed values.
      model._previous_key_values = {};

      var old_conflicting_models = self.where(previous_condition);

      if (old_conflicting_models.length == 1) {
        var m = old_conflicting_models[0];
        self.clearInvalidSessionIfModelValid(m);
      }

      var new_conflicting_models = self.where(condition);
      if (new_conflicting_models.length == 0) {
        self.clearInvalidSessionIfModelValid(model);
      } else if (new_conflicting_models.length == 1) {
        self.clearInvalidSessionIfModelValid(model);
        self.clearInvalidSessionIfModelValid(new_conflicting_models[0]);
      } else {
        var msg = 'Duplicate rows.';
        setTimeout(function() {
          _.each(new_conflicting_models, function(m) {
            self.trigger(
              'pgadmin-session:model:invalid', msg, m, self.handler
            );
            m.trigger(
              'pgadmin-session:model:duplicate', m, msg
            );
          });
        }, 10);

        return false;
      }
      return true;
    },
    clearInvalidSessionIfModelValid: function(m) {
      var errors = m.errorModel.attributes,
        invalidModels = this.sessAttrs['invalid'];

      m.trigger('pgadmin-session:model:unique', m);
      if (_.size(errors) == 0) {
        delete invalidModels[m.cid];
      } else {
        invalidModels[m.cid] = errors[Object.keys(errors)[0]];
      }
    },
  });

  pgBrowser.Events = _.extend({}, Backbone.Events);

  return pgBrowser;
});