class app.Searcher
  $.extend @prototype, Events

  CHUNK_SIZE = 10000
  SEPARATOR = '.'

  DEFAULTS =
    max_results: app.config.max_results
    fuzzy_min_length: 3

  constructor: (options = {}) ->
    @options = $.extend {}, DEFAULTS, options

  find: (data, attr, query) ->
    @kill()

    @data = data
    @attr = attr
    @query = query
    @setup()

    if @isValid() then @match() else @end()
    return

  setup: ->
    @query = @normalizeQuery @query
    @queryLength = @query.length
    @dataLength = @data.length
    @matchers = ['exactMatch']
    @totalResults = 0
    @setupFuzzy()
    return

  setupFuzzy: ->
    if @queryLength >= @options.fuzzy_min_length
      @fuzzyRegexp = @queryToFuzzyRegexp @query
      @matchers.push 'fuzzyMatch'
    return

  isValid: ->
    @queryLength > 0

  end: ->
    @triggerResults [] unless @totalResults
    @free()
    return

  kill: ->
    if @timeout
      clearTimeout @timeout
      @free()
    return

  free: ->
    @data = @attr = @query = @queryLength = @dataLength =
    @fuzzyRegexp = @matchers = @totalResults = @scoreMap =
    @cursor = @matcher = @timeout = null
    return

  match: =>
    if not @foundEnough() and @matcher = @matchers.shift()
      @setupMatcher()
      @matchChunks()
    else
      @end()
    return

  setupMatcher: ->
    @cursor = 0
    @scoreMap = new Array(101)
    return

  matchChunks: =>
    @matchChunk()

    if @cursor is @dataLength or @scoredEnough()
      @delay @match
      @sendResults()
    else
      @delay @matchChunks
    return

  matchChunk: ->
    for [0...@chunkSize()]
      if score = @[@matcher](@data[@cursor][@attr])
        @addResult @data[@cursor], score
      @cursor++
    return

  chunkSize: ->
    if @cursor + CHUNK_SIZE > @dataLength
      @dataLength % CHUNK_SIZE
    else
      CHUNK_SIZE

  scoredEnough: ->
     @scoreMap[100]?.length >= @options.max_results

  foundEnough: ->
    @totalResults >= @options.max_results

  addResult: (object, score) ->
    (@scoreMap[Math.round(score)] or= []).push(object)
    @totalResults++
    return

  getResults: ->
    results = []
    for objects in @scoreMap by -1 when objects
      results.push.apply results, objects
    results[0...@options.max_results]

  sendResults: ->
    results = @getResults()
    @triggerResults results if results.length
    return

  triggerResults: (results) ->
    @trigger 'results', results
    return

  delay: (fn) ->
    @timeout = setTimeout(fn, 1)

  normalizeQuery: (string) ->
    string.replace(/\s/g, '').toLowerCase()

  queryToFuzzyRegexp: (string) ->
    chars = string.split ''
    chars[i] = $.escapeRegexp(char) for char, i in chars
    new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/

  #
  # Match functions
  #

  index =      # position of the query in the string being matched
  match =      # regexp match data
  score =      # score for the current match
  separators = # counter
  i = null     # cursor

  exactMatch: (value) ->
    index = value.indexOf @query
    return unless index >= 0

    # Remove one point for each unmatched character.
    score = 100 - (value.length - @queryLength)

    if index > 0
      # If the character preceding the query is a dot, assign the same score
      # as if the query was found at the beginning of the string, minus one.
      if value[index - 1] is SEPARATOR
        score += index - 1
      # Don't match a single-character query unless it's found at the beginning
      # of the string or is preceded by a dot.
      else if @queryLength is 1
        return
      # (1) Remove one point for each unmatched character up to the nearest
      #     preceding dot or the beginning of the string.
      # (2) Remove one point for each unmatched character following the query.
      else
        i = index - 2
        i-- while i >= 0 and value[i] isnt SEPARATOR
        score -= (index - i) +                         # (1)
                 (value.length - @queryLength - index) # (2)

      # Remove one point for each dot preceding the query, except for the one
      # immediately before the query.
      separators = 0
      i = index - 2
      while i >= 0
        separators++ if value[i] is SEPARATOR
        i--
      score -= separators

    # Remove five points for each dot following the query.
    separators = 0
    i = value.length - @queryLength - index - 1
    while i >= 0
      separators++ if value[index + @queryLength + i] is SEPARATOR
      i--
    score -= separators * 5

    Math.max 1, score

  fuzzyMatch: (value) ->
    return if value.length <= @queryLength or value.indexOf(@query) >= 0
    return unless match = @fuzzyRegexp.exec(value)

    # When the match is at the beginning of the string or preceded by a dot.
    if match.index is 0 or value[match.index - 1] is SEPARATOR
      Math.max 66, 100 - match[0].length
    # When the match is at the end of the string.
    else if match.index + match[0].length is value.length
      Math.max 33, 67 - match[0].length
    # When the match is in the middle of the string.
    else
      Math.max 1, 34 - match[0].length

class app.SynchronousSearcher extends app.Searcher
  match: =>
    if @matcher
      @allResults or= []
      @allResults.push.apply @allResults, @getResults()
    super

  free: ->
    @allResults = null
    super

  end: ->
    @sendResults true
    super

  sendResults: (end) ->
    if end and @allResults?.length
      @triggerResults @allResults

  delay: (fn) ->
    fn()