/* global ncrc32 */
const path = require('path')
const _ = require('lodash')
const csShared = require('@unitybase/cs-shared')
const UBDomain = csShared.UBDomain
const lds = csShared.LocalDataStore

/**
 * This component is deprecated. Use [fsStorage mixin](https://unitybase.info/api/server-v5/tutorial-mixins_fsstorage.html) instead.
 *
 * UnityBase file-system based virtual store **select**. Able to load files & transform it content to {@link TubCachedData} format.
 *
 * Good sample of usage can be found in `ubm_form.loadAllForm`
 *
 * For work with data, loaded by FileBasedStoreLoader you can use {@link LocalDataStore} class
 *
 * @deprecated
 * @module FileBasedStoreLoader
 * @memberOf module:@unitybase/base
 */
module.exports = FileBasedStoreLoader
/**
 * @example

const FileBasedStoreLoader = require('@unitybase/base').FileBasedStoreLoader
let loader = new FileBasedStoreLoader({
  entity: me.entity,
  foldersConfig: folders,
  fileMask: /-fm\.def$/,
  onBeforeRowAdd: postProcessing
})
let resultDataCache = loader.load()

 * @class
 * @deprecated
 * @param {Object}    config
 * @param {UBEntity} config.entity
 * @param {Array.<{path: string}>} config.foldersConfig   Array of folder configuration to scan for files.
 *                                              Necessary param is path - path to folder. You can also pass additional information
 *                                              for use in  `onBeforeRowAdd` and `onNewFolder` callbacks.
 *                                              Currently processed root folder accessible from FileBasedStoreLoader.processingRootFolder
 * @param {Boolean}   config.zipToArray     Transform result from array of object to array-of-array representation. Default true
 * @param {Boolean}   config.uniqueID       Result data must contain ID attribute and values must be unique. Default true.
 * @param {RegExp}    [config.fileMask]     Regular expression to filter folder files. Each fileName (without path) will be tested by this regExp
 * @param {String}    [config.attributeRegExpString] String representation of regular expression to found attribute and it value in input content.
 *                                                   Default is '^\\/\\/@(\\w+)\\s"(.*?)"' what mean find all string like: //@attribute "value"
 *                                                   You can pass empty string to disable attribute parsing by regExp and do it manually in `onBeforeRowAdd` handler.
 * @param {Function}  [config.onBeforeRowAdd] Callback called for each row BEFORE it added to store. In case it return false row not added.
 *                          Called with args (this: FileBasedStoreLoader, fullFilePath: string, fileContent: string, oneRow: Object);
 * @param {Function}  [config.onNewFolder] Callback called for each new folder in case of recursive folder.
 *                          In case callback return false or not defined - folder not processed.
 *                          Called with args (this: FileBasedStoreLoader, fullFolderPath: string, recursionLevel: integer);
 */
function FileBasedStoreLoader (config) {
  let entityAttributes
  if (config.entity instanceof UBDomain.UBEntity) {
    entityAttributes = config.entity.attributes
  } else {
    // eslint-disable-next-line
    entityAttributes = App.domainInfo.get(config.entity.name).attributes
  }

  /**
   * Configuration
   * @type {Object}
   */
  this.config = _.clone(config)
  if (!Array.isArray(config.foldersConfig)) {
    throw new Error('config.foldersConfig must be array')
  }
  if (config.attributeRegExpString !== '') {
    this.config.attributeRegExpString = config.attributeRegExpString || FileBasedStoreLoader.JSON_ATTRIBURE_REGEXP
  }

  if (!this.config.hasOwnProperty('uniqueID')) { this.config.uniqueID = true }
  if (!this.config.hasOwnProperty('zipToArray')) { this.config.zipToArray = true }

  /**
   * Entity attributes array
   * @type {Array.<Object>}
   * @readonly
   */
  this.attributes = []
  for (const attrName in entityAttributes) {
    const attr = entityAttributes[attrName]
    this.attributes.push({
      name: attr.name,
      dataType: attr.dataType,
      defaultValue: attr.defaultValue,
      defaultView: attr.defaultView
    })
  }
  /**
   * Is `mStore.simpleAudit` enabled for current entity (exist `mi_modifyDate` attribute)
   * @type {Boolean}
   * @readonly
   */
  this.haveModifyDate = this.attributes.findIndex((attr) => attr.name === 'mi_modifyDate') > -1
  /**
   * Is `mStore.simpleAudit` enabled for current entity (exist `mi_createDate` attribute)
   * @type {Boolean}
   * @readonly
   */
  this.haveCreateDate = this.attributes.findIndex((attr) => attr.name === 'mi_createDate') > -1

  /**
   * Currently processed root folder
   * @type {*}
   * @readonly
   */
  this.processingRootFolder = null
}

FileBasedStoreLoader.JSON_ATTRIBURE_REGEXP = '^\\/\\/[ \t]?@(\\w+)\\s"(.*?)"'
FileBasedStoreLoader.XML_ATTRIBURE_REGEXP = '<!--@(\\w+)\\s*"(.+)"\\s*-->'

/**
 * Perform actual loading.
 * @return {TubCachedData}
 */
FileBasedStoreLoader.prototype.load = function () {
  const me = this
  let result

  /**
   * Array of Object representing dirty result
   * @type {Array.<Object>}
   * @protected
   */
  this.resultCollection = []
  me.config.foldersConfig.forEach(function (folderConfig) {
    me.processingRootFolder = folderConfig
    me.parseFolder(folderConfig.path, 0)
  })
  // transformation to array=of=array
  if (me.config.zipToArray) {
    result = {
      data: [],
      fields: [],
      rowCount: 0
    }
    result.fields = _.map(me.attributes, 'name')
    result.data = lds.arrayOfObjectsToSelectResult(me.resultCollection, result.fields)
    result.rowCount = result.data.length
    const l = result.fields.indexOf('mi_modifyDate')
    if (l !== -1) {
      let dataVersion = 0
      // for UnityBase calculate accum of crc32(prev, fileDate.toString()) and forms count
      if (typeof ncrc32 === 'function') {
        result.data.forEach((row) => {
          dataVersion = ncrc32(dataVersion, '' + row[l])
        })
        result.version = ncrc32(dataVersion, '' + result.data.length)
      } else {
        // for NodeJS - max date
        // we can update one model earlier than other, so max date of file changes is a bad choice
        result.data.forEach((row) => {
          if (dataVersion < row[l]) {
            dataVersion = row[l]
          }
        })
        // add a row count for case when some row are deleted or added with old date
        result.version = new Date(dataVersion).getTime() + result.data.length
      }
    }
  } else {
    result = me.resultCollection
  }
  me.resultCollection = []
  return result
}

/**
 * @method parseFolder
 * @protected
 * @param {String} folderPath Folder to parse
 * @param {Number} recursionLevel current level of folder recursion
 */
FileBasedStoreLoader.prototype.parseFolder = function (folderPath, recursionLevel) {
  const fs = require('fs')
  const config = this.config

  if (!fs.existsSync(folderPath)) {
    return
  }

  if (config.onNewFolder) {
    if (config.onNewFolder(this, folderPath, recursionLevel) === false) return
  }
  const folderFiles = fs.readdirSync(folderPath)

  folderFiles.forEach((fileName) => {
    const fullPath = path.join(folderPath, fileName)
    const stat = fs.statSync(fullPath)

    if (stat.isDirectory()) {
      if (config.onNewFolder) {
        const newFolderCheck = config.onNewFolder(this, folderPath + fileName, recursionLevel + 1)
        if (newFolderCheck !== false) {
          this.parseFolder(fullPath, recursionLevel + 1)
        }
      }
    } else if (!this.config.fileMask || this.config.fileMask.test(fileName)) { // filtration by mask
      const content = fs.readFileSync(fullPath, 'utf8')
      const oneRow = this.extractAttributesValues(content)

      if (this.haveModifyDate) {
        oneRow.mi_modifyDate = stat.mtime
      }
      if (this.haveCreateDate) {
        oneRow.mi_createDate = stat.ctime
      }
      let canAdd = this.config.onBeforeRowAdd ? this.config.onBeforeRowAdd(this, fullPath, content, oneRow) : true
      // check unique ID
      if (canAdd && config.uniqueID) {
        if (!oneRow.ID) {
          console.error(`Parameter ID not set. File "${fullPath}" ignored`)
          canAdd = false
        } else if (this.resultCollection.indexOf((elm) => elm.ID === oneRow.ID) > -1) {
          console.error(`Record with ID "${oneRow.ID}" already exist. File "${fullPath}" ignored`)
          canAdd = false
        }
      }
      if (canAdd) {
        this.resultCollection.push(oneRow)
      }
    }
  })
}

/**
 * Extract attribute values from content using regular expression passed in the config.attributeRegExpString.
 *
 * Convert values from string representation to JS data type using entity attribute dataType information
 *
 * Add default values for missed attributes
 *
 * @private
 * @param {String} content
 * @result {Object} dictionary looking like {attrbuteName: "value"}
 */
FileBasedStoreLoader.prototype.extractAttributesValues = function (content) {
  const me = this
  const regexp = me.config.attributeRegExpString ? new RegExp(me.config.attributeRegExpString, 'gm') : false
  const result = {}

  // extraction block
  if (regexp !== false) {
    let attrVal = regexp.exec(content)
    while (attrVal !== null) {
      result[attrVal[1]] = attrVal[2]
      attrVal = regexp.exec(content)
    }
  }
  // default block
  me.attributes.forEach(function (attribute) {
    if (attribute.defaultValue !== '' && !result[attribute.name]) {
      result[attribute.name] = attribute.defaultValue
    }
  })
  // transformation block
  _.forEach(result, function (value, attribute) {
    const attr = me.attributes.find(elm => elm.name === attribute)

    if (!attr) { return }
    switch (attr.dataType) {
      case UBDomain.ubDataTypes.Int:
      case UBDomain.ubDataTypes.BigInt:
      case UBDomain.ubDataTypes.ID:
      case UBDomain.ubDataTypes.Float:
      case UBDomain.ubDataTypes.Currency:
      case UBDomain.ubDataTypes.Entity:
      case UBDomain.ubDataTypes.TimeLog:
        result[attribute] = +value
        break
      case UBDomain.ubDataTypes.Boolean:
        result[attribute] = (value === true) || (value === 'true') || (value === '1')
        break
      case UBDomain.ubDataTypes.DateTime:
        result[attribute] = _.isDate(value) ? value : new Date(value)
        break
      case UBDomain.ubDataTypes.Unknown:
      case UBDomain.ubDataTypes.String:
      case UBDomain.ubDataTypes.Text:
      case UBDomain.ubDataTypes.Many:
      case UBDomain.ubDataTypes.Document:
      case UBDomain.ubDataTypes.Enum:
      case UBDomain.ubDataTypes.BLOB:
        break // just to be sure we handle all types
      default:
        throw new Error('Unknown attribute type ' + attr.dataType)
    }
  })
  return result
}