
Inflector = require 'inflector'

module.exports =

  idCounter : 0

  extend:(target, sources...)->
    for source in sources
      target[key] = val  for key, val of source

    return target

  dict: Object.create.bind null, null, (Object.create null)

  getNearestElementByTagName: (el, tagName) ->
    el = el.parentNode  until not el? or @elementHasTag el, tagName
    return el

  elementShow: (el) ->
    el?.classList.remove "hidden"

  elementHide: (el) ->
    el?.classList.add "hidden"

  elementHasTag: (el, tagName) ->
    Boolean(el.tagName?.toLowerCase() is tagName.toLowerCase())

  elementIsVisible: (el) ->
    return false  if el.offsetWidth <= 0 or el.offsetHeight <= 0
    height = document.documentElement.clientHeight
    rects = el.getClientRects()
    onTop = (r) ->
      x = (r.left + r.right) / 2
      y = (r.top + r.bottom) / 2
      document.elementFromPoint(x, y) is el

    i = 0
    l = rects.length

    while i < l
      r = rects[i]
      inViewport = (if r.top > 0 then r.top <= height else (r.bottom > 0 and r.bottom <= height))
      return true  if inViewport and onTop(r)
      i++

    return false

  formatPlural:(count, noun, showCount = yes)->
    """
    #{
      if showCount
      then "#{count or 0} "
      else ''
    }#{
      if count is 1
      then noun
      else Inflector.pluralize noun
    }
    """

  formatIndefiniteArticle: (noun) ->
    return "an #{noun}"  if noun[0].toLowerCase() in ['a','e','i','o','u']
    return "a #{noun}"

  getSelection:->
    return  window.getSelection()

  getSelectionRange:->
    selection = @getSelection()
    return  selection.getRangeAt 0 if selection.type isnt "None"

  getCursorNode:->
    return  @getSelectionRange().commonAncestorContainer

  addRange:(range)->
    selection = window.getSelection()
    selection.removeAllRanges()
    selection.addRange range

  selectText:(element, start, end = start)->
    if document.body.createTextRange
      range = document.body.createTextRange()
      range.moveToElementText element
      range.select()
    else if window.getSelection
      selection = window.getSelection()
      range     = document.createRange()

      range.selectNodeContents element
      range.setStart           element, start if start?
      range.setEnd             element, end   if end?

      selection.removeAllRanges()
      selection.addRange range

  selectEnd:(element, range)->
    range   or= document.createRange()
    element or= @getSelection().focusNode

    return  unless element

    range.setStartAfter element
    range.collapse no
    @addRange range

  replaceRange:(node, replacement, start, end = start, appendTrailingSpace = yes)->
    trailingSpace = document.createTextNode "\u00a0"

    range = new Range()

    if start?
      range.setStart    node, start
      range.setEnd      node, end
    else
      range.selectNode  node

    range.deleteContents()

    range.insertNode replacement
    @selectEnd replacement, range

    if appendTrailingSpace
      range.insertNode trailingSpace
      @selectEnd trailingSpace, range

  getCallerChain:(args, depth)->
    {callee:{caller}} = args
    chain = [caller]
    while depth-- and caller = caller?.caller
      chain.push caller
    chain

  createCounter: createCounter = (i = 0) -> -> i++

  getUniqueId: do (inc = createCounter()) -> -> "kd-#{do inc}"

  getRandomNumber :(range=1e6, min=0)->
    res = Math.floor Math.random()*range+1
    return if res > min then res else res + min

  uniqueId : (prefix)->
    id = @idCounter++
    if prefix? then "#{prefix}#{id}" else id

  getRandomRGB :->
    fn = @getRandomNumber
    return "rgb(#{fn 255},#{fn 255},#{fn 255})"

  getRandomHex : ->
    # hex = (Math.random()*0xFFFFFF<<0).toString(16)
    hex = (Math.random()*0x999999<<0).toString(16)
    while hex.length < 6
      hex += "0"
    "##{hex}"

  curry:(obligatory, optional)->
    obligatory + if optional then ' ' + optional else ''

  parseQuery:do->
    params  = /([^&=]+)=?([^&]*)/g    # for chunking the key-val pairs
    plusses = /\+/g                   # for converting plus signs to spaces
    decode  = (str)-> decodeURIComponent str.replace plusses, " "
    parseQuery = (queryString = location.search.substring 1)->
      result = {}
      result[decode m[1]] = decode m[2]  while m = params.exec queryString
      result

  stringifyQuery:do->
    spaces = /\s/g
    encode =(str)-> encodeURIComponent str.replace spaces, "+"
    stringifyQuery = (obj)->
      Object.keys(obj).map((key)-> "#{encode key}=#{encode obj[key]}").join('&').trim()

  capAndRemovePeriods:(path)->
    newPath = for arg in path.split "."
      arg.capitalize()
    newPath.join ""

  slugify:(title = "")->
    url = String(title)
      .toLowerCase()                # change everything to lowercase
      .replace(/^\s+|\s+$/g, "")    # trim leading and trailing spaces
      .replace(/[_|\s]+/g, "-")     # change all spaces and underscores to a hyphen
      .replace(/[^a-z0-9-]+/g, "")  # remove all non-alphanumeric characters except the hyphen
      .replace(/[-]+/g, "-")        # replace multiple instances of the hyphen with a single instance
      .replace(/^-+|-+$/g, "")      # trim leading and trailing hyphens

  stripTags:(value)->
    value.replace /<(?:.|\n)*?>/gm, ''

  decimalToAnother:(n, radix) ->
    hex = []
    for i in [0..10]
      hex[i+1] = i

    s = ''
    a = n
    while a >= radix
      b = a % radix
      a = Math.floor a / radix
      s += hex[b + 1]

    s += hex[a + 1]
    n = s.length
    t = ''
    for i in [0...n]
      t = t + s.substring n - i - 1, n - i
    s = t
    s


  enterFullscreen: do ->

    # Find the right method, call on correct element
    launchFullscreen = (element) ->

      if element.requestFullscreen            then element.requestFullscreen()
      else if element.mozRequestFullScreen    then element.mozRequestFullScreen()
      else if element.webkitRequestFullscreen then element.webkitRequestFullscreen()
      else if element.msRequestFullscreen     then element.msRequestFullscreen()

    (element = document.documentElement) ->
      # Launch fullscreen for browsers that support it!
      launchFullscreen element

  exitFullscreen: ->

    if document.exitFullscreen            then document.exitFullscreen()
    else if document.mozCancelFullScreen  then document.mozCancelFullScreen()
    else if document.webkitExitFullscreen then document.webkitExitFullscreen()

  isFullscreen: ->

    return document.fullscreenElement or document.mozFullScreenElement or document.webkitIsFullScreen


  createExternalLink: (href) ->
    tag = document.createElement "a"
    tag.href = if href.indexOf("http") > -1 then href else "http://#{href}"
    tag.target = "_blank"
    document.body.appendChild tag
    tag.click()
    document.body.removeChild tag

  wait: (duration, fn)->
    if "function" is typeof duration
      fn = duration
      duration = 0
    return setTimeout fn, duration

  killWait:(id)->
    clearTimeout id if id
    return null

  repeat: (duration, fn)->
    if "function" is typeof duration
      fn = duration
      duration = 500
    setInterval fn, duration

  killRepeat:(id)-> clearInterval id

  defer:do (queue = []) ->
    # this was ported from browserify's implementation of "process.nextTick"
    if window?.postMessage and window.addEventListener
      window.addEventListener "message", ((ev) ->
        if ev.source is window and ev.data is "kd-tick"
          ev.stopPropagation()
          do queue.shift()  if queue.length > 0
      ), yes
      (fn) -> queue.push fn; window.postMessage "kd-tick", "*"
    else
      (fn) -> setTimeout fn, 1

  getCancellableCallback:(callback)->
    cancelled = no
    kallback = (rest...)-> callback rest...  unless cancelled
    kallback.cancel = -> cancelled = yes
    kallback

  # ~ GG
  # Returns a new callback which calls the failcallback if
  # first callback not finish its job in given timeout (default is 5000ms)
  #
  # Usage:
  #
  # Let assume that you have this:
  #
  #   asyncFunc (data)->
  #     doSomethingWith data
  #
  # To set a timeout for it eg. 500ms:
  #
  #   asyncFunc getTimedOutCallBack (data)->
  #     doSomethingWith data
  #   , ->
  #     console.log "asyncFunc is not responded in 500ms."
  #   , 500
  #
  getTimedOutCallback:(callback, failcallback, timeout=5000)->
    cancelled = no
    kallback  = (rest...)->
      clearTimeout fallbackTimer
      callback rest...  unless cancelled

    fallback = (rest...)->
      failcallback rest...  unless cancelled
      cancelled = yes

    fallbackTimer = setTimeout fallback, timeout
    kallback

  # Returns a new callback which calls the failcallback if
  # first callback doesn't finish its job within timeout.
  #
  # Also, keeps track of start and end times.
  #
  # Let's assume that you have this:
  #
  #   asyncFunc (data)-> doSomethingWith data
  #
  # To set a timeout for 500ms:
  #
  #   asyncFunc ,\
  #     KD.utils.getTimedOutCallbackOne
  #       name     :"asyncFunc" // optional, logs to KD.utils.timers
  #       timeout  : 500        // defaults to 5000
  #       onSucess : (data)->
  #       onTimeout: ->
  #       onResult : ->         // called when result comes after timeout
  getTimedOutCallbackOne: (options={})->
    timerName = options.name      or "undefined"
    timeout   = options.timeout   or 10000
    onSuccess = options.onSuccess or ->
    onTimeout = options.onTimeout or ->
    onResult  = options.onResult  or ->

    timedOut = no
    kallback = (rest...)=>
      clearTimeout fallbackTimer
      @updateLogTimer timerName, fallbackTimer, Date.now()

      if timedOut then onResult rest... else onSuccess rest...

    fallback = (rest...)=>
      timedOut = yes
      @updateLogTimer timerName, fallbackTimer

      onTimeout rest...

    fallbackTimer = setTimeout fallback, timeout
    @logTimer timerName, fallbackTimer, Date.now()

    kallback.cancel =-> clearTimeout fallbackTimer
    kallback

  logTimer:(timerName, timerNumber, startTime)->
    log "logTimer name:#{timerName}"

    @timers[timerName] ||= {}
    @timers[timerName][timerNumber] =
      start  : startTime
      status : "started"

  updateLogTimer:(timerName, timerNumber, endTime)->
    timer     = @timers[timerName][timerNumber]
    status    = if endTime then "ended" else "failed"
    startTime = timer.start
    elapsed   = endTime-startTime
    timer     =
      start   : startTime
      end     : endTime
      status  : status
      elapsed : elapsed

    @timers[timerName][timerNumber] = timer

    log "updateLogTimer name:#{timerName}, status:#{status} elapsed:#{elapsed}"

  timers: {}

  stopDOMEvent :(event)->
    return no  unless event
    event.preventDefault()
    event.stopPropagation()
    return no

  utf8Encode:(string)->
    string = string.replace(/\r\n/g, "\n")
    utftext = ""
    n = 0

    while n < string.length
      c = string.charCodeAt(n)
      if c < 128
        utftext += String.fromCharCode(c)
      else if (c > 127) and (c < 2048)
        utftext += String.fromCharCode((c >> 6) | 192)
        utftext += String.fromCharCode((c & 63) | 128)
      else
        utftext += String.fromCharCode((c >> 12) | 224)
        utftext += String.fromCharCode(((c >> 6) & 63) | 128)
        utftext += String.fromCharCode((c & 63) | 128)
      n++
    utftext

  utf8Decode:(utftext)->
    string = ""
    i = 0
    c = c1 = c2 = 0
    while i < utftext.length
      c = utftext.charCodeAt(i)
      if c < 128
        string += String.fromCharCode(c)
        i++
      else if (c > 191) and (c < 224)
        c2 = utftext.charCodeAt(i + 1)
        string += String.fromCharCode(((c & 31) << 6) | (c2 & 63))
        i += 2
      else
        c2 = utftext.charCodeAt(i + 1)
        c3 = utftext.charCodeAt(i + 2)
        string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
        i += 3
    string

  # Return true x% of time based on argument.
  #
  # Example:
  #   runXpercent(10) => returns true 10% of the time
  runXpercent: (percent)->
    chance = Math.floor(Math.random() * 100)
    chance <= percent

  shortenUrl: (url, callback)->
    request = $.ajax "https://www.googleapis.com/urlshortener/v1/url",
      type        : "POST"
      contentType : "application/json"
      data        : JSON.stringify {longUrl: url}
      timeout     : 4000
      dataType    : "json"

    request.done (data)=>
      callback data?.id or url, data

    request.error ({status, statusText, responseText})->
      error "URL shorten error, returning self as fallback.", status, statusText, responseText
      callback url

  formatBytesToHumanReadable: (bytes, fixedAmout = 2) ->
    minus = ''
    if bytes < 0
      minus  = '-'
      bytes *= -1
    thresh    = 1024
    units     = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
    unitIndex = -1
    return "#{bytes} B"  if bytes < thresh
    loop
      bytes /= thresh
      ++unitIndex
      break unless bytes >= thresh

    return "#{minus}#{bytes.toFixed fixedAmout} #{units[unitIndex]}"

  splitTrim: (str, delim = ',', filterEmpty = yes) ->
    arr = (str?.split(delim).map (part) -> do part.trim) ? []
    arr = arr.filter Boolean  if filterEmpty
    return arr

  arrayToObject: (list, key) ->
    dict = {}
    dict[obj[key]] = obj for obj in list when obj[key]?
    dict

  # The partition function takes a list and predicate fn and returns the pair of lists
  # of elements which do and do not satisfy the predicate, respectively.
  # (stolen from CoffeeScriptRedux)
  partition: (list, fn) ->
    result = [[], []]
    result[+!fn item].push item for item in list
    result
  ###
  //     Underscore.js 1.3.1
  //     (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
  //     Underscore is freely distributable under the MIT license.
  //     Portions of Underscore are inspired or borrowed from Prototype,
  //     Oliver Steele's Functional, and John Resig's Micro-Templating.
  //     For all details and documentation:
  //     http://documentcloud.github.com/underscore
  ###

  throttle : (wait, func, options) ->

    [wait, func] = [func, wait]  if (typeof func) is 'number'

    context  = null
    args     = null
    result   = null
    timeout  = null
    previous = 0
    options ?= {}

    later = ->
      previous = (if options.leading is false then 0 else Date.now)
      timeout  = null
      result   = func.apply context, args
      context  = args = null  unless timeout

    ->
      now       = Date.now
      previous  = now  if not previous and options.leading is false
      remaining = wait - (now - previous)
      context   = this
      args      = arguments

      if remaining <= 0 or remaining > wait
        if timeout
          clearTimeout timeout
          timeout = null
        previous = now
        result = func.apply context, args
        context = args = null  unless timeout
      else if not timeout and options.trailing isnt false
        timeout = setTimeout later, remaining

      return result


  debounce : (wait, func, immediate) ->

    [wait, func] = [func, wait]  if (typeof func) is 'number'

    timeout   = null
    args      = null
    context   = null
    timestamp = null
    result    = null
    later     = ->
      last = Date.now - timestamp
      if last < wait and last >= 0
        timeout = setTimeout(later, wait - last)
      else
        timeout = null
        unless immediate
          result  = func.apply(context, args)
          context = args = null  unless timeout

    ->
      context   = this
      args      = arguments
      timestamp = Date.now
      callNow   = immediate and not timeout
      timeout  ?= setTimeout later, wait

      if callNow
        result  = func.apply(context, args)
        context = args = null

      return result



  relativeOffset: (child, parent) ->
    x = 0; y = 0
    node = child
    while node
      x += node.offsetLeft
      y += node.offsetTop
      break if node is parent
      node = node.parentNode
      throw new Error "Not a descendant!" unless node?
    [x, y]
