/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const tableMappingsModule = require('./table-mappings');
const TableMappings = tableMappingsModule.TableMappings;
const DefaultTableMappings = tableMappingsModule.DefaultTableMappings;

/**
 * Represents the parsed user information of the table mappings of a model.
 * @ignore
 */
class ModelMappingInfo {
  /**
   * @param {String} keyspace
   * @param {Array<{name, isView}>} tables
   * @param {TableMappings} mappings
   * @param {Map<String,ModelColumnInfo>} columns
   */
  constructor(keyspace, tables, mappings, columns) {
    this.keyspace = keyspace;
    this.tables = tables;
    this._mappings = mappings;
    this._columns = columns;

    // Define a map of column information per property name
    /** @type {Map<String, ModelColumnInfo>} */
    this._documentProperties = new Map();
    for (const modelColumnInfo of columns.values()) {
      this._documentProperties.set(modelColumnInfo.propertyName, modelColumnInfo);
    }
  }

  getColumnName(propName) {
    const modelColumnInfo = this._documentProperties.get(propName);
    if (modelColumnInfo !== undefined) {
      // There is an specific name transformation between the column name and the property name
      return modelColumnInfo.columnName;
    }
    // Rely on the TableMappings (i.e. maybe there is a convention defined for this property)
    return this._mappings.getColumnName(propName);
  }

  getPropertyName(columnName) {
    const modelColumnInfo = this._columns.get(columnName);
    if (modelColumnInfo !== undefined) {
      // There is an specific name transformation between the column name and the property name
      return modelColumnInfo.propertyName;
    }
    // Rely on the TableMappings (i.e. maybe there is a convention defined for this column)
    return this._mappings.getPropertyName(columnName);
  }

  getFromModelFn(propName) {
    const modelColumnInfo = this._documentProperties.get(propName);
    return modelColumnInfo !== undefined ? modelColumnInfo.fromModel : null;
  }

  getToModelFn(columnName) {
    const modelColumnInfo = this._columns.get(columnName);
    return modelColumnInfo !== undefined ? modelColumnInfo.toModel : null;
  }

  newInstance() {
    return this._mappings.newObjectInstance();
  }

  /**
   * Parses the user options into a map of model names and ModelMappingInfo.
   * @param {MappingOptions} options
   * @param {String} currentKeyspace
   * @returns {Map<String, ModelMappingInfo>}
   */
  static parse(options, currentKeyspace) {
    const result = new Map();
    if (!options || !options.models) {
      return result;
    }

    Object.keys(options.models).forEach(modelName => {
      const modelOptions = options.models[modelName];
      result.set(modelName, ModelMappingInfo._create(modelName, currentKeyspace, modelOptions));
    });

    return result;
  }

  static _create(modelName, currentKeyspace, modelOptions) {
    if (!currentKeyspace && (!modelOptions || !modelOptions.keyspace)) {
      throw new Error(
        'You should specify the keyspace of the model in the MappingOptions when the Client is not using a keyspace');
    }

    if (!modelOptions) {
      return ModelMappingInfo.createDefault(modelName, currentKeyspace);
    }

    let tables;

    if (modelOptions.tables && modelOptions.tables.length > 0) {
      tables = modelOptions.tables.map(item => {
        const table = { name: null, isView: false };
        if (typeof item === 'string') {
          table.name = item;
        } else if (item) {
          table.name = item.name;
          table.isView = !!item.isView;
        }

        if (!table.name) {
          throw new Error(`Table name not specified for model '${modelName}'`);
        }

        return table;
      });
    } else {
      tables = [ { name: modelName, isView: false }];
    }

    if (modelOptions.mappings && !(modelOptions.mappings instanceof TableMappings)) {
      throw new Error('mappings should be an instance of TableMappings');
    }

    const columns = new Map();
    if (modelOptions.columns !== null && typeof modelOptions.columns === 'object') {
      Object.keys(modelOptions.columns).forEach(columnName => {
        columns.set(columnName, ModelColumnInfo.parse(columnName, modelOptions.columns[columnName]));
      });
    }

    return new ModelMappingInfo(
      modelOptions.keyspace || currentKeyspace,
      tables,
      modelOptions.mappings || new DefaultTableMappings(),
      columns
    );
  }

  static createDefault(modelName, currentKeyspace) {
    return new ModelMappingInfo(
      currentKeyspace,
      [ { name: modelName, isView: false }],
      new DefaultTableMappings(),
      new Map());
  }
}

class ModelColumnInfo {
  constructor(columnName, propertyName, toModel, fromModel) {
    this.columnName = columnName;
    this.propertyName = propertyName;

    if (toModel && typeof toModel !== 'function') {
      throw new TypeError(`toModel type for property '${propertyName}' should be a function (obtained ${
        typeof toModel})`);
    }

    if (fromModel && typeof fromModel !== 'function') {
      throw new TypeError(`fromModel type for property '${propertyName}' should be a function (obtained ${
        typeof fromModel})`);
    }

    this.toModel = toModel;
    this.fromModel = fromModel;
  }

  static parse(columnName, value) {
    if (!value) {
      return new ModelColumnInfo(columnName, columnName);
    }

    if (typeof value === 'string') {
      return new ModelColumnInfo(columnName, value);
    }

    return new ModelColumnInfo(columnName, value.name || columnName, value.toModel, value.fromModel);
  }
}

module.exports = ModelMappingInfo;