Task = require './task'

# Searches local files for lines matching a specified regex. Implements `.then()`
# so that it can be used with `Promise.all()`.
class DirectorySearch
  constructor: (rootPaths, regex, options) ->
    scanHandlerOptions =
      ignoreCase: regex.ignoreCase
      inclusions: options.inclusions
      includeHidden: options.includeHidden
      excludeVcsIgnores: options.excludeVcsIgnores
      globalExclusions: options.exclusions
      follow: options.follow
    searchOptions =
      leadingContextLineCount: options.leadingContextLineCount
      trailingContextLineCount: options.trailingContextLineCount
    @task = new Task(require.resolve('./scan-handler'))
    @task.on 'scan:result-found', options.didMatch
    @task.on 'scan:file-error', options.didError
    @task.on 'scan:paths-searched', options.didSearchPaths
    @promise = new Promise (resolve, reject) =>
      @task.on('task:cancelled', reject)
      @task.start rootPaths, regex.source, scanHandlerOptions, searchOptions, =>
        @task.terminate()
        resolve()

  then: (args...) ->
    @promise.then.apply(@promise, args)

  cancel: ->
    # This will cause @promise to reject.
    @task.cancel()
    null

# Default provider for the `atom.directory-searcher` service.
module.exports =
class DefaultDirectorySearcher
  # Determines whether this object supports search for a `Directory`.
  #
  # * `directory` {Directory} whose search needs might be supported by this object.
  #
  # Returns a `boolean` indicating whether this object can search this `Directory`.
  canSearchDirectory: (directory) -> true

  # Performs a text search for files in the specified `Directory`, subject to the
  # specified parameters.
  #
  # Results are streamed back to the caller by invoking methods on the specified `options`,
  # such as `didMatch` and `didError`.
  #
  # * `directories` {Array} of {Directory} objects to search, all of which have been accepted by
  # this searcher's `canSearchDirectory()` predicate.
  # * `regex` {RegExp} to search with.
  # * `options` {Object} with the following properties:
  #   * `didMatch` {Function} call with a search result structured as follows:
  #     * `searchResult` {Object} with the following keys:
  #       * `filePath` {String} absolute path to the matching file.
  #       * `matches` {Array} with object elements with the following keys:
  #         * `lineText` {String} The full text of the matching line (without a line terminator character).
  #         * `lineTextOffset` {Number} (This always seems to be 0?)
  #         * `matchText` {String} The text that matched the `regex` used for the search.
  #         * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.)
  #   * `didError` {Function} call with an Error if there is a problem during the search.
  #   * `didSearchPaths` {Function} periodically call with the number of paths searched thus far.
  #   * `inclusions` {Array} of glob patterns (as strings) to search within. Note that this
  #   array may be empty, indicating that all files should be searched.
  #
  #   Each item in the array is a file/directory pattern, e.g., `src` to search in the "src"
  #   directory or `*.js` to search all JavaScript files. In practice, this often comes from the
  #   comma-delimited list of patterns in the bottom text input of the ProjectFindView dialog.
  #   * `ignoreHidden` {boolean} whether to ignore hidden files.
  #   * `excludeVcsIgnores` {boolean} whether to exclude VCS ignored paths.
  #   * `exclusions` {Array} similar to inclusions
  #   * `follow` {boolean} whether symlinks should be followed.
  #
  # Returns a *thenable* `DirectorySearch` that includes a `cancel()` method. If `cancel()` is
  # invoked before the `DirectorySearch` is determined, it will resolve the `DirectorySearch`.
  search: (directories, regex, options) ->
    rootPaths = directories.map (directory) -> directory.getPath()
    isCancelled = false
    directorySearch = new DirectorySearch(rootPaths, regex, options)
    promise = new Promise (resolve, reject) ->
      directorySearch.then resolve, ->
        if isCancelled
          resolve()
        else
          reject()
    return {
      then: promise.then.bind(promise)
      catch: promise.catch.bind(promise)
      cancel: ->
        isCancelled = true
        directorySearch.cancel()
    }
