const LocalDataStore = require('@unitybase/cs-shared').LocalDataStore
const ClientRepository = require('./ClientRepository')
const _ = require('lodash')

/**
 * Format for data, used to initialize LocalRepository
 *
 * @typedef {object} TubLocalData
 * @property {Array<Array>} data
 * @property {Array<string>} fields
 * @property {number} [rowCount] Optional. If not set, will be calculated from data
 */

/**
 * @classdesc
 * A CustomRepository descendant to be used with local data
 *
 * Usually created using UB.LocalRepository fabric function
 * @example
const UB = require('@unitybase/ub-pub')
const localData = {data: [[1, 'Jon'], [2, 'Bob']], fields: ['ID', 'name'], rowCount: 2}

await UB.LocalRepository(localData, 'uba_user').attrs('ID', 'name').selectAsArray()
// {"resultData":{"data":[[1,"Jon"],[2,"Bob"]],"fields":["ID","name"]},"total":2}

await UB.LocalRepository(localData, 'uba_user').attrs('ID').selectAsArrayOfValues()
// [1, 2]

await UB.LocalRepository(localData, 'uba_user').attrs('name').where('ID', '=', 2).selectScalar()
// "Bob"
 * @class LocalRepository
 * @augments ClientRepository
 */
class LocalRepository extends ClientRepository {
  /**
   * Do not create directly - use UB.LocalRepository instead
   *
   * @example
const UB = require('@unitybase/ub-pub')
const localData = {data: [[1, 'Jon'], [2, 'Bob']], fields: ['ID', 'name']}
await UB.LocalRepository(localData, 'uba_user').attrs('ID', 'name').selectAsArray()
   * @example
const UB = require('@unitybase/ub-pub')
const localData = {data: [[1, 'Jon', '#maxdate'], [2, 'Bob', new Date(2023, 2, 15]], fields: ['ID', 'name', 'mi_deleteDate']}
await UB.LocalRepository(localData, 'uba_user').attrs('ID', 'name').selectAsArray()
   * @example
const UB = require('@unitybase/ub-pub')
const localData = [{ID: 1, name: 'Jon'}, {ID: 2, name: 'Bob'}]
await UB.LocalRepository(localData, 'uba_user').attrs('ID', 'name').selectAsArray()
   * @override
   * @private
   * @param {Array|TubLocalData} source
   * @param {string} entityName
   * @param {UBConnection} [conn]
   */
  constructor (source, entityName, conn) {
    super(conn, entityName)

    let localData

    if (Array.isArray(source)) {
      if (source.length === 0) {
        localData = { data: [], fields: ['ID'], rowCount: 0 }
      } else {
        const fields = Object.keys(source[0])
        localData = {
          fields,
          data: source.map(row => fields.map(field => row[field])),
          rowCount: source.length
        }

        // Automatically add mi_deleteDate field if it is not present in the fields, and set it to "#maxdate",
        // but only for entities which have mi_deleteDate attribute.
        if (!fields.includes('mi_deleteDate') && this.connection) {
          const entityInfo = this.connection.domain.get(entityName, false)
          if (entityInfo && ('mi_deleteDate' in entityInfo.attributes)) {
            fields.push('mi_deleteDate')
            const maxDate = new Date(9999, 11, 31)
            for (const row of localData.data) {
              row.push(maxDate)
            }
          }
        }
      }
    } else {
      // "source" is TubLocalData (not array of objects)
      localData = source

      if (Array.isArray(localData.data)) {
        // Replace "#maxdate" with for mi_deleteDate attributes with 9999-12-31, which means "not deleted"
        const deleteDateAttributeIndexes = localData.fields
          .map((f, i) => f === 'mi_deleteDate' || f.endsWith('.mi_deleteDate') ? i : -1)
          .filter(i => i !== -1)
        if (deleteDateAttributeIndexes.length > 0) {
          const maxDate = new Date(9999, 11, 31)
          for (const dataRow of localData.data) {
            for (const attributeIndex of deleteDateAttributeIndexes) {
              if (dataRow[attributeIndex] === '#maxdate') {
                dataRow[attributeIndex] = maxDate
              }
            }
          }
        }

        if (localData.rowCount === undefined) {
          localData.rowCount = localData.data.length
        }
      }
    }

    /**
     * @private
     * @property {TubCachedData} _localData
     */
    Object.defineProperty(this, '_localData', { enumerable: false, writable: false, value: localData })
  }

  /**
   * @returns {Promise<{resultData: TubCachedData, total: number}>}
   */
  selectAsArray () {
    const _ubql = this.ubql()
    const filtered = LocalDataStore.doFilterAndSort(this._localData, _ubql)
    // transform a result according to passed fieldList (if needed)
    const rd = filtered.resultData // ref
    if (
      !_ubql.fieldList.length ||
      (_ubql.fieldList.length === 1 && _ubql.fieldList[0] === '*') ||
      !rd.data.length ||
      _.isEqual(_ubql.fieldList, rd.fields)
    ) {
      // Repository attributes list is equal to localData fields list -  no additional transformation required
      return Promise.resolve(filtered)
    } else {
      const localFl = rd.fields
      const idxMap = _ubql.fieldList.map(f => localFl.indexOf(f)) // map of requested fieldList to localData field list
      const dataAsInFieldList = rd.data.map(r => {
        return idxMap.map(i => r[i])
      })
      return Promise.resolve({
        resultData: { data: dataAsInFieldList, fields: _ubql.fieldList, rowCount: dataAsInFieldList.length },
        total: rd.total
      })
    }
  }

  /**
   * @override
   * @returns {LocalRepository}
   */
  clone () {
    const cloned = super.clone()

    // copy _localData by hand since it is private property
    Object.defineProperty(cloned, '_localData', { enumerable: false, writable: false, value: this._localData })
    return cloned
  }
}

module.exports = LocalRepository