/**
 * Utility functions for @unitybase/ub-pub module
 *
 * @module utils
 * @memberOf module:@unitybase/ub-pub
 * @author UnityBase team
 */

/* global FileReader, Blob */
const i18n = require('./i18n').i18n

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {object} objectTo The receiver of the properties
 * @param {...object} objectsFrom The source(s) of the properties
 * @returns {object} returns objectTo
 */
module.exports.apply = function (objectTo, objectsFrom) {
  Array.prototype.forEach.call(arguments, function (obj) {
    if (obj && obj !== objectTo) {
      Object.keys(obj).forEach(function (key) {
        objectTo[key] = obj[key]
      })
    }
  })
  return objectTo
}

const FORMAT_RE = /{(\d+)}/g
/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {string} stringToFormat The string to be formatted.
 * @param {...*} values The values to replace tokens `{0}`, `{1}`, etc in order.
 * @returns {string} The formatted string.
 */
module.exports.format = function (stringToFormat, ...values) {
  return stringToFormat.replace(FORMAT_RE, function (m, i) {
    return values[i]
  })
}

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {string} namespacePath
 * @returns {object} The namespace object.
 */
module.exports.ns = function (namespacePath) {
  let root = window
  let part, j, subLn

  const parts = namespacePath.split('.')

  for (j = 0, subLn = parts.length; j < subLn; j++) {
    part = parts[j]
    if (!root[part]) root[part] = {}
    root = root[part]
  }
  return root
}

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {Date|string} value
 * @returns {Date}
 */
module.exports.iso8601Parse = function (value) {
  return value ? new Date(value) : null
}

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {*} v Value to convert
 * @returns {boolean|null}
 */
module.exports.booleanParse = function (v) {
  if (typeof v === 'boolean') return v
  if ((v === undefined || v === null || v === '')) return null
  return v === 1
}

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {File|ArrayBuffer|string|Blob|Array} data
 * @returns {Promise<string>} resolved to data converted to base64 string
 */
module.exports.base64FromAny = function (data) {
  return new Promise((resolve, reject) => {
    if (typeof Buffer === 'function') {
      const res = Buffer.from(data).toString('base64')
      resolve(res)
    } else {
      const reader = new FileReader()
      const blob = (data instanceof Blob) ? data : new Blob([data])
      reader.addEventListener('loadend', function () {
        resolve(reader.result.split(',', 2)[1]) // remove data:....;base64, from the beginning of string //TODO -use indexOf
      })
      reader.addEventListener('error', function (event) {
        reject(event)
      })
      reader.readAsDataURL(blob)
    }
  })
}

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {File} file
 * @returns {Promise<Uint8Array>} resolved to file content as Uint8Array
 */
module.exports.file2Uint8Array = function (file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = function () {
      resolve(new Uint8Array(reader.result))
    }
    reader.onerror = function (reason) {
      reject(reason)
    }
    reader.readAsArrayBuffer(file)
  })
}

const BASE64STRING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const BASE64ARR = [];
(function () {
  for (let i = 0, l = BASE64STRING.length - 1; i < l; i++) {
    BASE64ARR.push(BASE64STRING[i])
  }
})()

const BASE64DECODELOOKUP = new Uint8Array(256);
(function () {
  for (let i = 0, l = BASE64STRING.length; i < l; i++) {
    BASE64DECODELOOKUP[BASE64STRING[i].charCodeAt(0)] = i
  }
})()

/**
 * see docs in ub-pub main module
 *
 * @private
 * @param {string} base64
 * @returns {ArrayBuffer}
 */
module.exports.base64toArrayBuffer = function (base64) {
  let bufferLength = base64.length * 0.75
  const len = base64.length
  let p = 0
  let encoded1, encoded2, encoded3, encoded4

  if (base64[base64.length - 1] === '=') {
    bufferLength--
    if (base64[base64.length - 2] === '=') bufferLength--
  }

  const arrayBuffer = new ArrayBuffer(bufferLength)
  const bytes = new Uint8Array(arrayBuffer)

  for (let i = 0; i < len; i += 4) {
    encoded1 = BASE64DECODELOOKUP[base64.charCodeAt(i)]
    encoded2 = BASE64DECODELOOKUP[base64.charCodeAt(i + 1)]
    encoded3 = BASE64DECODELOOKUP[base64.charCodeAt(i + 2)]
    encoded4 = BASE64DECODELOOKUP[base64.charCodeAt(i + 3)]

    bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
    bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
    bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
  }

  return arrayBuffer
}

/**
 * UnityBase client-side exception.
 * Such exceptions will not be showed as unknown error in {@link UB#showErrorWindow UB.showErrorWindow}
 *
 * @example
// in adminUI will show message box with text:
// "Record was locked by other user. It\'s read-only for you now"
throw new UB.UBError('lockedBy')
 * @param {string} message Message can be either localized message or locale identifier - in this case UB#showErrorWindow translate message using {@link UB#i18n}
 * @param {string} [detail] Error details
 * @param {number} [code] Error code (for server-side errors)
 * @extends {Error}
 */
function UBError (message, detail, code) {
  this.name = 'UBError'
  this.detail = detail
  this.code = code
  this.message = message || 'UBError'
  if (Error.captureStackTrace) {
    Error.captureStackTrace(this, UBError)
  } else {
    this.stack = (new Error()).stack
  }
}

UBError.prototype = new Error()
UBError.prototype.constructor = UBError

module.exports.UBError = UBError

/**
 * Quiet exception. Global error handler does not show this exception for user. Use it for silently reject promise
 *
 * @param {string} [message] Message
 * @param {string} [detail] Error details
 * @extends {Error}
 */
function UBAbortError (message, detail) {
  this.name = 'UBAbortError'
  this.detail = detail
  this.code = 'UBAbortError'
  this.message = message || 'UBAbortError'
  if (Error.captureStackTrace) {
    Error.captureStackTrace(this, UBAbortError)
  } else {
    this.stack = (new Error()).stack
  }
}

UBAbortError.prototype = new Error()
UBAbortError.prototype.constructor = UBAbortError

module.exports.UBAbortError = UBAbortError

const TEST_ERROR_MESSAGE_RE = /<<<.*?>>>/
const PARSE_ERROR_MESSAGE_RE = /(?:^|")<<<(.*?)>>>(?:\|(\[[^\]]*]))?(?:$|")/
const SIMPLE_PARSE_ERROR_MESSAGE_RE = /<<<(.*)>>>/

function parseAndTranslateUBErrorMessage (errMsg) {
  // RegExp for fast detection of error message
  if (!TEST_ERROR_MESSAGE_RE.test(errMsg)) {
    return i18n(errMsg)
  }

  // RegExp for full detection of error message by patterns, using parts
  const match = errMsg.match(PARSE_ERROR_MESSAGE_RE)
  if (!match) {
    // Shall never happen
    return i18n(errMsg)
  }

  // Problem is that errMsg is JSON-encoded object, and we are looking for a string inside it.
  const [msgUnparsed, msgPart, argsPart] = match
  if (!argsPart) {
    // No parameters passed, just JSON.parse content inside <<<>>>
    return i18n(JSON.parse('"' + msgPart + '"'))
  }

  // JSON.parse the whole part in quotes, to decode JSON as a string
  const msgStr = JSON.parse(msgUnparsed[0] === '"' ? msgUnparsed : '"' + msgUnparsed + '"')

  const index = msgStr.indexOf('|')
  if (index === -1) {
    // Shall never happen
    return i18n(msgStr)
  }

  const msg = msgStr.substring(3, index - 3) // Use "substring" to get part inside <<<>>>, knowing index of |
  const argsStr = msgStr.substr(index + 1) // Get all the part after |
  const args = JSON.parse(argsStr) // Parse it.  If it fails - it fails, we expect server to return correct JSON
  if (Array.isArray(args)) {
    args.unshift(msg)
    return i18n.apply(null, args)
  } else {
    // Object or a single value
    return i18n(msg, args)
  }
}

module.exports.parseAndTranslateUBErrorMessage = parseAndTranslateUBErrorMessage

const SERVER_ERROR_CODES = {
  1: 'ubErrNotImplementedErrnum',
  2: 'ubErrRollbackedErrnum',
  3: 'ubErrNotExecutedErrnum',
  4: 'ubErrInvaliddataforrunmethod',
  5: 'ubErrInvaliddataforrunmethodlist',
  6: 'ubErrNoMethodParameter',
  7: 'ubErrMethodNotExist',
  8: 'ubErrElsAccessDeny',
  9: 'ubErrElsInvalidUserOrPwd',
  10: 'ubErrElsNeedAuth',
  11: 'ubErrNoEntityParameter',
  13: 'ubErrNoSuchRecord',
  14: 'ubErrInvalidDocpropFldContent',
  15: 'ubErrEntityNotExist',
  16: 'ubErrAttributeNotExist',
  17: 'ubErrNotexistEntitymethod',
  18: 'ubErrInvalidSetdocData',
  19: 'ubErrSoftlockExist',
  20: 'ubErrNoErrorDescription',
  21: 'ubErrUnknownStore',
  22: 'ubErrObjdatasrcempty',
  23: 'ubErrObjattrexprbodyempty',
  24: 'ubErrNecessaryfieldNotExist',
  25: 'ubErrRecordmodified',
  26: 'ubErrNotexistnecessparam',
  27: 'ubErrNotexistfieldlist',
  28: 'ubErrUpdaterecnotfound',
  29: 'ubErrNecessaryparamnotexist',
  30: 'ubErrInvalidstoredirs',
  31: 'ubErrNofileinstore',
  32: 'ubErrAppnotsupportconnection',
  33: 'ubErrAppnotsupportstore',
  34: 'ubErrDeleterecnotfound',
  35: 'ubErrNotfoundlinkentity',
  36: 'ubErrEntitynotcontainmixinaslink',
  37: 'ubErrEssnotinherfromessaslink',
  38: 'ubErrInstancedatanameisreadonly',
  39: 'ubErrManyrecordsforsoftlock',
  40: 'ubErrNotfoundidentfieldsl',
  41: 'ubErrInvalidlocktypevalue',
  42: 'ubErrLockedbyanotheruser',
  43: 'ubErrInvalidwherelistinparams',
  44: 'ubErrRecnotlocked',
  45: 'ubErrManyrecordsforchecksign',
  46: 'ubErrNotfoundparamnotrootlevel',
  47: 'ubErrCantcreatedirlogmsg',
  48: 'ubErrCantcreatedirclientmsg',
  49: 'ubErrConnectionNotExist',
  50: 'ubErrDirectUnityModification',
  51: 'ubErrCantdelrecthisvalueusedinassocrec',
  52: 'ubErrAssocattrnotfound',
  53: 'ubErrAttrassociationtoentityisempty',
  54: 'ubErrNotfoundconforentityinapp',
  55: 'ubErrNewversionrecnotfound',
  56: 'ubErrElsAccessDenyEntity',
  57: 'ubErrAlsAccessDenyEntityattr',
  58: 'ubErrDatastoreEmptyentity',
  // 59: "ubErrCustomerror"
  67: 'ubErrTheServerHasExceededMaximumNumberOfConnections',
  69: 'ubErrFtsForAppDisabled',
  72: 'ubErrElsPwdIsExpired',
  73: 'ELS_USER_NOT_FOUND',
  74: 'VALUE_MUST_ME_UNIQUE'
}
module.exports.SERVER_ERROR_CODES = SERVER_ERROR_CODES

function parseUBAuthError (rejectReason, authParams) {
  if (!rejectReason || (rejectReason instanceof Error)) {
    return rejectReason
  }
  // http error
  const errDescription = rejectReason.data || rejectReason // in case of server-side error we got a {data: {errMsg: ..}..}
  const errInfo = {
    errMsg: errDescription.errMsg,
    errCode: errDescription.errCode,
    errDetails: errDescription.errMsg
  }
  if (rejectReason.status === 403) {
    // in case exception text is wrapped in <<<>>> - use it, else replace to default "access deny"
    // for Access deny error on the auth stage transform it to Invalid user or pwd
    if (!TEST_ERROR_MESSAGE_RE.test(errInfo.errMsg) || (errInfo.errMsg === '<<<Access deny>>>')) {
      errInfo.errMsg = (authParams.authSchema === 'UB') ? 'msgInvalidUBAuth' : 'msgInvalidCertAuth'
    }
  } else if (rejectReason.status === 0) {
    errInfo.errMsg = 'serverIsBusy'
    errInfo.errDetails = 'network error'
  } else {
    if (!errInfo.errMsg) { errInfo.errMsg = 'unknownError' } // internalServerError
  }

  if (TEST_ERROR_MESSAGE_RE.test(errInfo.errMsg)) {
    errInfo.errMsg = JSON.parse('"' + errInfo.errMsg.match(SIMPLE_PARSE_ERROR_MESSAGE_RE)[1] + '"')
  }

  const codeMsg = SERVER_ERROR_CODES[errInfo.errCode]
  if (codeMsg) {
    errInfo.errDetails = codeMsg + ' ' + errInfo.errDetails
  }
  return new UBError(errInfo.errMsg, errInfo.errDetails, errInfo.errCode)
}
module.exports.parseUBAuthError = parseUBAuthError

/**
 * Parse error and translate message using {@link UB#i18n i18n}
 *
 * @param {string|object|Error|UBError} errMsg  message to show
 * @param {string} [errCode] (Optional) error code
 * @param {string} [entityCode] (Optional) entity code
 * @param {string} detail erro detail
 * @returns {{errMsg: string, errCode: *, entityCode: *, detail: *|string}}
 */
module.exports.parseUBError = function (errMsg, errCode, entityCode, detail) {
  let errDetails = detail || ''
  if (errMsg && errMsg instanceof UBError) {
    errCode = errMsg.code
    errDetails = errMsg.detail
    if (errMsg.stack) {
      errDetails += '<br/>stackTrace:' + errMsg.stack
    }
    errMsg = errMsg.message
  } else if (errMsg instanceof Error) {
    if (errMsg.stack) {
      errDetails += '<br/>stackTrace:' + errMsg.stack
    }
    errMsg = errMsg.toString()
  } else if (errMsg && (typeof errMsg === 'object')) {
    errCode = errMsg.errCode
    entityCode = errMsg.entity
    errMsg = errMsg.errMsg ? errMsg.errMsg : JSON.stringify(errMsg)
    errDetails = errMsg.detail || errDetails
  }
  return {
    errMsg: i18n(errMsg),
    errCode,
    entityCode,
    detail: errDetails
  }
}

/**
 * Log message to console (if console available)
 *
 * @function
 * @param {...*} msg
 */
module.exports.log = function log (msg) {
  if (console) console.log.apply(console, arguments)
}

/**
 * Log error message to console (if console available)
 *
 * @function
 * @param {...*} msg
 */
module.exports.logError = function logError (msg) {
  if (console) {
    console.error.apply(console, arguments)
  }
}

/**
 * Log warning message to console (if console available)
 *
 * @function
 * @param {...*} msg
 */
module.exports.logWarn = function logWarn (msg) {
  if (console) {
    console.warn.apply(console, arguments)
  }
}

/**
 * Log debug message to console.
 * Since it binds to console, can also be used to debug Promise resolving in this way
 *
 * @example
UB.get('timeStamp').then(UB.logDebug);
 * @function
 * @param {...*} msg
 */
module.exports.logDebug = console.info.bind(console)

const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent.toLowerCase() : 'nodeJS'
/** @type {string} */
module.exports.userAgent = userAgent.toLowerCase()
/** @type {boolean} */
module.exports.isChrome = /\bchrome\b/.test(userAgent)
/** @type {boolean} */
module.exports.isWebKit = /webkit/.test(userAgent)
/** @type {boolean} */
module.exports.isGecko = !/webkit/.test(userAgent) && /gecko/.test(userAgent)
/** @type {boolean} */
module.exports.isOpera = /opr|opera/.test(userAgent)
/** @type {boolean} */
module.exports.isMac = /macintosh|mac os x/.test(userAgent)
/** @type {boolean} */
module.exports.isSecureBrowser = /\belectron\b/.test(userAgent)
/** @type {boolean} */
module.exports.isReactNative = (typeof navigator !== 'undefined' && navigator.product === 'ReactNative')
/** @type {boolean} */
module.exports.isNodeJS = /nodeJS/.test(userAgent)

/**
 * localDataStorage keys used by @unitybase-ub-pub (in case of browser environment)
 */
module.exports.LDS_KEYS = {
  /**
   * Authentication schema used by user during last logon
   */
  LAST_AUTH_SCHEMA: 'lastAuthType',
  /**
   * In case stored value is 'true' then login using Negotiate without prompt
   */
  SILENCE_KERBEROS_LOGIN: 'silenceKerberosLogin',
  /**
   * Last logged-in username (login)
   */
  LAST_LOGIN: 'lastLogin',
  /**
   * In case stored value is 'true' then used call logout directly (i.e. press logout button)
   */
  USER_DID_LOGOUT: 'userDidLogout',
  /**
   * In case document url is set using URI Schema, for example `document.location.href="ms-word:ofv|u|http://...."`
   * window.onbeforeunload should skip call og App.logout(). Since we do not have access to the target URL inside onbeforeunload event
   * caller must set this flar in localstorage to `true` to prevent log out of current user
   */
  PREVENT_CALL_LOGOUT_ON_UNLOAD: 'preventLogoutOnUnload',
  /**
   * locale, preferred by user. Empty in case of first login
   */
  PREFERRED_LOCALE: 'preferredLocale',
  /**
   * User preferred uData keys, passed as prefUData URL param during `/auth` handshake (for example organization ID in case used assigned to several og them)
   * Server-side on('login') event handler MUST verify passed preferred keys and can apply it into uData
   */
  PREFFERED_UDATA_PREFIX: 'UDATA_PREFFERED_'
}