var _ = require('underscore');
var ko = require('knockout');

var gutil = require('app/common/gutil');

var dispose = require('../lib/dispose');

var modelUtil = require('./modelUtil');


/**
 * BaseRowModel is an observable model for a record (or row) of a data (DataRowModel) or meta
 * (MetaRowModel) table. It takes a reference to the containing TableModel, and a list of
 * column names, and creates an observable for each field.
 * TODO: We need to have a way to dispose RowModels, and have them dispose individual fields,
 * which should in turn unsubscribe from various events on disposal. And it all should be tested.
 *
 */
function BaseRowModel(tableModel, colNames) {
  this._table  = tableModel;
  this._fields = colNames.slice(0);
  this._index  = ko.observable(null);    // The index in the observable to which it belongs.
  this._rowId  = null;

  // Create a field for everything in `_fields`.
  this._fields.forEach(function(colName) {
    this._createField(colName);
  }, this);
}
dispose.makeDisposable(BaseRowModel);

// This adds the dispatchAction() method to RowModel.
_.extend(BaseRowModel.prototype, modelUtil.ActionDispatcher);

/**
 * Returns the rowId to which this RowModel is assigned. This is also normally available as the
 * `rowModel.id` observable.
 */
BaseRowModel.prototype.getRowId = function() {
  return this._rowId;
};

/**
 * Creates a field for colName. This is either a top level observable like this[colName]
 * for MetaRowModels or a property field like this[name][prop] for DataRowModels
 */
BaseRowModel.prototype._createField = function(colName) {
  this[colName] = modelUtil.addSaveInterface(ko.observable(), v => this._saveField(colName, v));
};

/**
 * Helper method to send a user action to save a field of the current row to the server.
 */
BaseRowModel.prototype._saveField = function(colName, value) {
  var colValues = {};
  colValues[colName] = value;
  return this.updateColValues(colValues);
};

/**
 * Send an update to the server to update multiple columns for this row.
 * @param {Object} colValues: Maps colIds to values.
 * @returns {Promise} Resolved when the update succeeds.
 */
BaseRowModel.prototype.updateColValues = function(colValues) {
  return this._table.sendTableAction(["UpdateRecord", this._rowId, colValues]);
};

/**
 * Assigns the field of this RowModel named by `colName` to its corresponding value.
 */
BaseRowModel.prototype._assignColumn = function(colName) {
  throw new Error("Not Implemented");
};

//----------------------------------------------------------------------

/**
 * Implements the interface expected by modelUtil.ActionDispatcher. We only implement the
 * actions that affect individual rows. Note that BulkUpdateRecord needs to be translated to individual
 * UpdateRecords for RowModel to know what to do. Messages not here must be implemented by subclasses.
 * Some of these require helper methods defined in subclasses
 */

BaseRowModel.prototype._process_RemoveColumn = function(action, tableId, colId) {
  if (!gutil.arrayRemove(this._fields, colId)) {
    console.error("RowModel #RemoveColumn %s %s: column not found", tableId, colId);
  }
  delete this[colId];
};

BaseRowModel.prototype._process_ModifyColumn = function(action, tableId, colId, colInfo) {
  // No-op for us, because we don't care about any of the column properties.
};

BaseRowModel.prototype._process_UpdateRecord = function(action, tableId, rowId, columnValues) {
  for (var colName in columnValues) {
    this._assignColumn(colName);
  }
};

BaseRowModel.prototype._process_BulkUpdateRecord = function(action, tableId, rowId, columnValues) {
  // We get notified when a BulkUpdateRecord affects us, but since we just update all fields from
  // the underlying data, we don't need to find our row in the action.
  for (var colName in columnValues) {
    this._assignColumn(colName);
  }
};

// TODO: if AddColumn messages aren't sent for properties, we will need to find a different
// way to create and set the properties than here
BaseRowModel.prototype._process_AddColumn = function(action, tableId, colId, colInfo) {
  this._fields.push(colId);
  this._createField(colId);
  this._assignColumn(colId);
};

BaseRowModel.prototype._process_RenameColumn = function(action, tableId, oldColId, newColId) {
  // handle standard renames differently
  if (this._fields.indexOf(newColId) !== -1) {
    console.error("RowModel #RenameColumn %s %s %s: already exists", tableId, oldColId, newColId);
    return;
  }
  var index = this._fields.indexOf(oldColId);
  if (index === -1) {
    console.error("RowModel #RenameColumn %s %s %s: not found", tableId, oldColId, newColId);
    return;
  }
  this._fields[index] = newColId;

  // Reuse the old observable, but replace its "save" family of functions.
  this[newColId] = this[oldColId];
  modelUtil.addSaveInterface(this[newColId], this._saveField.bind(this, newColId));
  this._assignColumn(newColId);
  delete this[oldColId];
};

module.exports = BaseRowModel;