// QueryString
// ---------------
// This module provides utilities for dealing with query strings.
//
// Thanks to:
//  - http://nodejs.org/docs/v0.4.7/api/querystring.html
//  - http://developer.yahoo.com/yui/3/api/QueryString.html
//  - https://github.com/lifesinger/dew/tree/master/lib/querystring

interface IQueryString {
  escape: typeof encodeURIComponent
  unescape: (val: string) => string
  stringify: (obj: object, sep?: string, eq?: string, arrayKey?: boolean) => string
  parse: (str: string, sep?: string, eq?: string) => any
}

const QueryString: IQueryString = {
  escape: encodeURIComponent,
  unescape: (str) => '',
  stringify: (obj: object, sep?: string, eq?: string, arrayKey?: boolean) => '',
  parse: (str: string, sep?: string, eq?: string) => {}
}

// The escape/unescape function used by stringify/parse, provided so that it
// could be overridden if necessary. This is important in cases where
// non-standard delimiters are used, if the delimiters would not normally be
// handled properly by the built-in (en|de)codeURIComponent functions.
QueryString.escape = encodeURIComponent

QueryString.unescape = function (s) {
  // The + character is interpreted as a space on the server side as well as
  // generated by forms with spaces in their fields.
  return decodeURIComponent(s.replace(/\+/g, ' '))
}

/**
 * Serialize an object to a query string. Optionally override the default
 * separator and assignment characters.
 *
 * stringify({foo: 'bar'})
 *   // returns 'foo=bar'
 *
 * stringify({foo: 'bar', baz: 'bob'}, '', ':')
 *   // returns 'foo:barbaz:bob'
 */
QueryString.stringify = function (obj, sep, eq, arrayKey) {
  if (!isPlainObject(obj)) return ''

  sep = sep || '&'
  eq = eq || '='
  arrayKey = arrayKey || false

  const buf: any[] = []
  let key: string
  let val
  const escape = QueryString.escape

  for (key in obj) {
    if (!hasOwnProperty.call(obj, key)) continue

    val = obj[key]
    key = QueryString.escape(key)

    // val is primitive value
    if (isPrimitive(val)) {
      buf.push(key, eq, escape(val + ''), sep)
    } else if (isArray(val) && val.length) {
      // val is not empty array
      for (let i = 0; i < val.length; i++) {
        if (isPrimitive(val[i])) {
          buf.push(
            key,
            (arrayKey ? escape('[]') : '') + eq,
            escape(val[i] + ''),
            sep)
        }
      }
    } else {
      // ignore other cases, including empty array, Function, RegExp, Date etc.
      buf.push(key, eq, sep)
    }
  }

  buf.pop()
  return buf.join('')
}

/**
 * Deserialize a query string to an object. Optionally override the default
 * separator and assignment characters.
 *
 * parse('a=b&c=d')
 *   // returns {a: 'b', c: 'c'}
 */
QueryString.parse = function (str, sep, eq) {
  if (typeof str === 'undefined') {
    str = document.location.search
  }
  const ret = {}

  if (typeof str !== 'string' || trim(str).length === 0) {
    return ret
  }

  // remove ^?
  str = str.replace(/^\?/, '')

  const pairs = str.split(sep || '&')
  eq = eq || '='
  const unescape = QueryString.unescape

  for (let i = 0; i < pairs.length; i++) {
    const pair = pairs[i].split(eq)
    let key = unescape(trim(pair[0]))
    const val = unescape(trim(pair.slice(1).join(eq)))

    const m = key.match(/^(\w+)\[\]$/)
    if (m && m[1]) {
      key = m[1]
    }

    if (hasOwnProperty.call(ret, key)) {
      if (!isArray(ret[key])) {
        ret[key] = [ret[key]]
      }
      ret[key].push(val)
    } else {
      ret[key] = m ? [val] : val
    }
  }

  return ret
}

// Helpers

const toString = Object.prototype.toString
const hasOwnProperty = Object.prototype.hasOwnProperty
const isArray = Array.isArray || function (val) {
  return toString.call(val) === '[object Array]'
}
const trim = String.prototype.trim
  ? function (str) {
    return (str === null)
      ? ''
      : String.prototype.trim.call(str)
  }
  : function (str) {
    return (str === null)
      ? ''
      : str.toString().replace(/^\s+/, '').replace(/\s+$/, '')
  }

/**
 * Checks to see if an object is a plain object (created using "{}" or
 * "new Object()" or "new FunctionClass()").
 */
function isPlainObject(o) {
  /**
   * NOTES:
   * isPlainObject(node = document.getElementById("xx")) -> false
   * toString.call(node):
   *   ie678 === '[object Object]', other === '[object HTMLElement]'
   * 'isPrototypeOf' in node:
   *   ie678 === false, other === true
   */
  return o &&
    toString.call(o) === '[object Object]' &&
    'isPrototypeOf' in o
}

/**
 * If the type of o is null, undefined, number, string, boolean,
 * return true.
 */
function isPrimitive(o) {
  return o !== Object(o)
}

export default QueryString
