text: var Backbone = require('backbone');
  var _ = require('underscore');
  var syncAbort = require('./backbone/sync-abort');
  var StyleDefinitionModel = require('builder/editor/style/style-definition-model');
  var StyleCartoCSSModel = require('builder/editor/style/style-cartocss-model');
  var DataSQLModel = require('builder/editor/layers/layer-content-views/data/data-sql-model');
  var layerTypesAndKinds = require('./layer-types-and-kinds');
  var InfowindowModel = require('./infowindow-click-model');
  var TooltipModel = require('./infowindow-hover-model');
  var TableNameUtils = require('builder/helpers/table-name-utils');
  var layerColors = require('./layer-colors');
  
  // from_layer_id and from_letter are not attributes for the model, but are sent to the layer creation
  // endpoint when creating a layer from an existing analysis node (see user-actions)
  var ATTR_NAMES = ['id', 'order', 'infowindow', 'tooltip', 'error', 'from_layer_id', 'from_letter'];
  
  /**
   * Model to edit a layer definition.
   * Should always exist as part of a LayerDefinitionsCollection, so its URL is given from there.
   */
  module.exports = Backbone.Model.extend({
  
    /**
     * @override {Backbone.prototype.sync} abort ongoing request if there is any
     */
    sync: syncAbort,
  
    parse: function (response, opts) {
      response.options = response.options || {};
  
      // Flatten the attrs, to avoid having this.get('options').foobar internally
      var attrs = _
        .defaults(
          _.pick(response, ATTR_NAMES),
          _.omit(response.options, ['query', 'tile_style'])
        );
  
      // Only use type on the frontend, it will be mapped back when the model is serialized (see .toJSON)
      attrs.type = attrs.type || layerTypesAndKinds.getType(response.kind);
  
      // Map API endpoint attrs to the new names used client-side (cartodb.js in particular)
      if (response.options.tile_style) {
        attrs.cartocss = response.options.tile_style;
      }
      if (response.options.query) {
        attrs.sql = response.options.query;
      }
  
      if (response.infowindow) {
        if (!this.infowindowModel) {
          this.infowindowModel = new InfowindowModel(response.infowindow, {
            configModel: opts.configModel || this._configModel
          });
        }
      }
      if (response.tooltip) {
        if (!this.tooltipModel) {
          this.tooltipModel = new TooltipModel(response.tooltip, {
            configModel: opts.configModel || this._configModel
          });
        }
      }
      if (response.options.table_name) {
        // Set autostyle as false if it doesn't contain any id
        attrs.autoStyle = attrs.autoStyle || false;
  
        if (!this.styleModel) {
          this.styleModel = new StyleDefinitionModel(response.options.style_properties, {
            parse: true
          });
  
          this.cartocssModel = new StyleCartoCSSModel({
            content: attrs.cartocss
          }, {
            history: response.options.cartocss_history || response.options.tile_style_history
          });
        }
  
        if (!this.sqlModel) {
          this.sqlModel = new DataSQLModel({
            content: attrs.sql
          }, {
            history: response.options.sql_history
          });
        }
      }
  
      // Flatten the rest of the attributes
      return attrs;
    },
  
    initialize: function (attrs, opts) {
      if (!opts.configModel) throw new Error('configModel is required');
  
      this._configModel = opts.configModel;
  
      this.on('change:source change:sql', this._onPosibleLayerSchemaChanged, this);
  
      if (this.styleModel) {
        this.styleModel.bind('change:type change:animated', function () {
          if (this.styleModel.isAggregatedType() || this.styleModel.isAnimation()) {
            // setTemplate will clear fields
            this.infowindowModel && this.infowindowModel.unsetTemplate();
            this.tooltipModel && this.tooltipModel.unsetTemplate();
          }
        }, this);
      }
    },
  
    save: function (attrs, options) {
      attrs = attrs || {};
      options = options || {};
  
      // We assume that if the layer is saved, we have to disable autostyle
      var autoStyleAttrs = {
        autoStyle: false
      };
  
      // But if the layer is saved with shouldPreserveAutoStyle option, we should preserve autostyle
      if (options && options.shouldPreserveAutoStyle) {
        delete autoStyleAttrs.autoStyle;
      } else if (this.get('autoStyle')) {
        this.styleModel && this.styleModel.resetPropertiesFromAutoStyle();
      }
  
      attrs = _.extend(
        {},
        autoStyleAttrs,
        attrs
      );
  
      return Backbone.Model.prototype.save.call(this, attrs, options);
    },
  
    toJSON: function () {
      // Un-flatten the internal attrs to the datastructure that's expected by the API endpoint
      var options = _.omit(this.attributes, ATTR_NAMES.concat(['cartocss', 'sql', 'autoStyle']));
  
      // Map back internal attrs to the expected attrs names by the API endpoint
      var cartocss = this.get('cartocss');
  
      if (cartocss) {
        options.tile_style = cartocss;
      }
      var sql = this.get('sql');
      if (sql) {
        options.query = sql;
      }
  
      var defaultAttributes = {
        kind: layerTypesAndKinds.getKind(this.get('type')),
        options: options
      };
  
      var infowindowData = this.infowindowModel && this.infowindowModel.toJSON();
      if (!_.isEmpty(infowindowData)) {
        defaultAttributes.infowindow = this.infowindowModel.toJSON();
      }
  
      var tooltipData = this.tooltipModel && this.tooltipModel.toJSON();
      if (!_.isEmpty(tooltipData)) {
        defaultAttributes.tooltip = this.tooltipModel.toJSON();
      }
  
      if (this.styleModel && !this.styleModel.isAutogenerated()) {
        defaultAttributes.options.style_properties = this.styleModel.toJSON();
      }
  
      if (this.cartocssModel) {
        defaultAttributes.options.cartocss_history = this.cartocssModel.getHistory();
      }
  
      if (this.sqlModel) {
        defaultAttributes.options.sql_history = this.sqlModel.getHistory();
      }
  
      var attributes = _.omit(this.attributes, 'infowindow', 'tooltip', 'options', 'error', 'autoStyle');
  
      return _.defaults(
        defaultAttributes,
        _.pick(attributes, ATTR_NAMES)
      );
    },
  
    canBeDeletedByUser: function () {
      return this.collection.getNumberOfDataLayers() > 1 && this.isDataLayer() &&
        (this._canBeFoldedUnderAnotherLayer() || !this._isAllDataLayersDependingOnAnyAnalysisOfThisLayer());
    },
  
    isOwnerOfAnalysisNode: function (nodeModel) {
      return nodeModel && nodeModel.letter() === this.get('letter');
    },
  
    ownedPrimaryAnalysisNodes: function () {
      var nodeDefModel = this.getAnalysisDefinitionNodeModel();
      return this.isOwnerOfAnalysisNode(nodeDefModel)
        ? nodeDefModel.linkedListBySameLetter()
        : [];
    },
  
    getName: function () {
      return this.get('name') ||
      this.get('table_name_alias') ||
      this.get('table_name');
    },
  
    getTableName: function () {
      return this.get('table_name') || '';
    },
  
    getColor: function () {
      return layerColors.getColorForLetter(this.get('letter'));
    },
  
    containsNode: function (other) {
      var nodeDefModel = this.getAnalysisDefinitionNodeModel();
      return nodeDefModel && nodeDefModel.containsNode(other);
    },
  
    getAnalysisDefinitionNodeModel: function () {
      return this.findAnalysisDefinitionNodeModel(this.get('source'));
    },
  
    findAnalysisDefinitionNodeModel: function (id) {
      return this.collection && this.collection.findAnalysisDefinitionNodeModel(id);
    },
  
    _onPosibleLayerSchemaChanged: function (eventName, attrs, options) {
      // Used to avoid resetting styles on source_id changes when we have saved styles for the node
      if (options && options.ignoreSchemaChange) {
        return;
      }
  
      if (this.infowindowModel) {
        this.infowindowModel.clearFields();
      }
      if (this.tooltipModel) {
        this.tooltipModel.clearFields();
      }
      if (this.styleModel) {
        this.styleModel.resetStyles();
      }
    },
  
    toggleVisible: function () {
      this.set('visible', !this.get('visible'));
    },
  
    toggleCollapse: function () {
      this.set('collapsed', !this.get('collapsed'));
    },
  
    hasAnalyses: function () {
      return this.getNumberOfAnalyses() > 0;
    },
  
    hasAggregatedStyles: function () {
      return this.styleModel && this.styleModel.isAggregatedType();
    },
  
    getNumberOfAnalyses: function () {
      var analysisNode = this.getAnalysisDefinitionNodeModel();
      var count = 0;
  
      while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
        analysisNode = analysisNode.getPrimarySource();
  
        if (analysisNode) {
          count += 1;
        }
      }
  
      return count;
    },
  
    getQualifiedTableName: function () {
      var userName = this.get('user_name') || this.collection.userModel.get('username');
      return TableNameUtils.getQualifiedTableName(
        this.getTableName(),
        userName,
        this.collection.userModel.isInsideOrg()
      );
    },
  
    getColumnNamesFromSchema: function () {
      return this._getQuerySchemaModel().getColumnNames();
    },
  
    _getQuerySchemaModel: function () {
      var nodeDefModel = this.getAnalysisDefinitionNodeModel();
      return nodeDefModel.querySchemaModel;
    },
  
    isDataLayer: function () {
      var layerType = this.get('type');
      return layerTypesAndKinds.isCartoDBType(layerType) ||
        layerTypesAndKinds.isTorqueType(layerType);
    },
  
    isTorqueLayer: function () {
      return this.get('type') === 'torque';
    },
  
    _canBeFoldedUnderAnotherLayer: function () {
      var thisNodeDefModel = this.getAnalysisDefinitionNodeModel();
  
      return this.collection.any(function (m) {
        if (m !== this && m.isDataLayer()) {
          var otherNodeDefModel = m.getAnalysisDefinitionNodeModel();
          if (otherNodeDefModel === thisNodeDefModel) return true;
  
          var lastNode = _.last(otherNodeDefModel.linkedListBySameLetter());
          return lastNode.getPrimarySource() === thisNodeDefModel;
        }
      }, this);
    },
  
    _isAllDataLayersDependingOnAnyAnalysisOfThisLayer: function () {
      var nodeDefModel = this.getAnalysisDefinitionNodeModel();
      if (!nodeDefModel) return false;
      if (!this.isOwnerOfAnalysisNode(nodeDefModel)) return false;
  
      var linkedNodesList = nodeDefModel.linkedListBySameLetter();
  
      return this.collection.chain()
        .filter(function (m) {
          return m !== this && !!m.get('source');
        }, this)
        .all(function (m) {
          return _.any(linkedNodesList, function (node) {
            return m.containsNode(node);
          });
        }, this)
        .value();
    },
  
    getAllDependentLayers: function () {
      var self = this;
      var layersCount = 0;
  
      var layerDefinitionsCollectionModels = self.collection.models;
  
      for (var i = 0; i < layerDefinitionsCollectionModels.length; i++) {
        var layer = layerDefinitionsCollectionModels[i];
        var dependentAnalysis = false;
  
        if (layer !== self) {
          var analysisNode = layer.getAnalysisDefinitionNodeModel();
  
          while (analysisNode) {
            if (self.isOwnerOfAnalysisNode(analysisNode)) {
              dependentAnalysis = true;
            }
            analysisNode = analysisNode.getPrimarySource();
          }
  
          if (dependentAnalysis) {
            layersCount += 1;
          }
        }
      }
      return layersCount;
    },
  
    matchesAttrs: function (otherAttrs) {
      if (this.get('type') !== otherAttrs.type) {
        return false;
      }
  
      if (layerTypesAndKinds.isTiledType(otherAttrs.type)) {
        return this.get('name') === otherAttrs.name &&
          this.get('urlTemplate') === otherAttrs.urlTemplate;
      }
  
      if (layerTypesAndKinds.isGMapsBase(otherAttrs.type)) {
        return this.get('name') === otherAttrs.name &&
          this.get('baseType') === otherAttrs.baseType &&
            this.get('style') === otherAttrs.style;
      }
  
      if (layerTypesAndKinds.isPlainType(otherAttrs.type)) {
        return this.get('color') === otherAttrs.color;
      }
  
      return false;
    },
  
    hasGeocodingAnalysisApplied: function () {
      var analysisNode = this.getAnalysisDefinitionNodeModel();
  
      if (analysisNode && analysisNode.get('type') === 'geocoding') {
        return true;
      }
  
      while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
        analysisNode = analysisNode.getPrimarySource();
  
        if (analysisNode && analysisNode.get('type') === 'geocoding') {
          return true;
        }
      }
  
      return false;
    },
  
    _hasAnyAnalysisApplied: function () {
      var analysisNode = this.getAnalysisDefinitionNodeModel();
  
      return analysisNode.get('type') !== 'source';
    },
  
    isEmpty: function () {
      throw new Error('LayerDefinitionModel.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
    },
  
    isEmptyAsync: function () {
      var nodeModel = this.getAnalysisDefinitionNodeModel();
      var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
      var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
  
      return new Promise(function (resolve, reject) {
        nodeModel.queryRowsCollection.isEmptyAsync()
          .then(function (isEmpty) {
            resolve(!hasAnyAnalysisApplied && !hasCustomQueryApplied && isEmpty);
          });
      });
    },
  
    isDataFiltered: function () {
      var nodeModel = this.getAnalysisDefinitionNodeModel();
      var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
      var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
      return new Promise(function (resolve, reject) {
        nodeModel.queryRowsCollection.isEmptyAsync()
          .then(function (isEmpty) {
            resolve((hasAnyAnalysisApplied || hasCustomQueryApplied) && isEmpty);
          });
      });
    },
  
    isDone: function () {
      var nodeModel = this.getAnalysisDefinitionNodeModel();
      return nodeModel.queryRowsCollection.isDone() &&
        nodeModel.queryGeometryModel.isDone() &&
        nodeModel.querySchemaModel.isDone();
    },
  
    canBeGeoreferenced: function () {
      var self = this;
      var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
      var emptyPromise = self.isEmptyAsync();
      var geomPromise = analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync();
  
      return Promise.all([emptyPromise, geomPromise])
        .then(function (values) {
          var isEmpty = values[0];
          var hasGeom = values[1];
  
          var canBeGeoreferenced =
            !isEmpty &&
            !hasGeom &&
            !self.hasGeocodingAnalysisApplied() &&
            !self._hasAnyAnalysisApplied() &&
            !analysisDefinitionNodeModel.isCustomQueryApplied();
  
          return canBeGeoreferenced;
        });
    },
  
    fetchQueryRowsIfRequired: function () {
      var self = this;
      var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
  
      analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync()
        .then(function (geomHasValue) {
          if (!self.hasGeocodingAnalysisApplied() &&
            !geomHasValue &&
            !analysisDefinitionNodeModel.isCustomQueryApplied() &&
            analysisDefinitionNodeModel.queryRowsCollection.isUnavailable()) {
            analysisDefinitionNodeModel.queryRowsCollection.fetch();
          }
        });
    }
  });
  