const binding = require('./binding')
const {Emitter, CompositeDisposable, Disposable} = require('event-kit')

const ACTIONS = new Map([
  [0, 'created'],
  [1, 'deleted'],
  [2, 'modified'],
  [3, 'renamed']
])

const ENTRIES = new Map([
  [0, 'file'],
  [1, 'directory'],
  [2, 'unknown']
])

// Private: Possible states of a {NativeWatcher}.
const STOPPED = Symbol('stopped')
const STARTING = Symbol('starting')
const RUNNING = Symbol('running')
const STOPPING = Symbol('stopping')

// Private: Interface with and normalize events from a native OS filesystem watcher.
class NativeWatcher {
  // Private: Initialize a native watcher on a path.
  //
  // Events will not be produced until {start()} is called.
  constructor (normalizedPath, options) {
    this.normalizedPath = normalizedPath
    this.options = options
    this.emitter = new Emitter()
    this.subs = new CompositeDisposable()

    this.channel = null
    this.state = STOPPED

    this.onEvents = this.onEvents.bind(this)
    this.onError = this.onError.bind(this)
  }

  // Private: Begin watching for filesystem events.
  //
  // Has no effect if the watcher has already been started.
  async start () {
    if (this.state === STARTING) {
      await new Promise(resolve => this.emitter.once('did-start', resolve))
      return
    }

    if (this.state !== STOPPED) {
      return
    }

    this.state = STARTING

    this.channel = await new Promise((resolve, reject) => {
      binding.watch(this.normalizedPath, this.options, (err, channel) => {
        if (err) {
          reject(err)
          return
        }

        resolve(channel)
      }, this.onEvents)
    })

    this.state = RUNNING
    this.emitter.emit('did-start')
  }

  // Private: Return true if the underlying watcher is actively listening for filesystem events.
  isRunning () {
    return this.state === RUNNING
  }

  // Private: Register a callback to be invoked when the filesystem watcher has been initialized.
  //
  // Returns: A {Disposable} to revoke the subscription.
  onDidStart (callback) {
    return this.emitter.on('did-start', callback)
  }

  // Private: Register a callback to be invoked with normalized filesystem events as they arrive. Starts the watcher
  // automatically if it is not already running. The watcher will be stopped automatically when all subscribers
  // dispose their subscriptions.
  //
  // Returns: A {Disposable} to revoke the subscription.
  onDidChange (callback) {
    this.start()

    const sub = this.emitter.on('did-change', callback)
    return new Disposable(() => {
      sub.dispose()
      if (this.emitter.listenerCountForEventName('did-change') === 0) {
        this.stop()
      }
    })
  }

  // Private: Register a callback to be invoked when a {PathWatcher} should attach to a different {NativeWatcher}.
  //
  // Returns: A {Disposable} to revoke the subscription.
  onShouldDetach (callback) {
    return this.emitter.on('should-detach', callback)
  }

  // Private: Register a callback to be invoked when a {NativeWatcher} is about to be stopped.
  //
  // Returns: A {Disposable} to revoke the subscription.
  onWillStop (callback) {
    return this.emitter.on('will-stop', callback)
  }

  // Private: Register a callback to be invoked when the filesystem watcher has been stopped.
  //
  // Returns: A {Disposable} to revoke the subscription.
  onDidStop (callback) {
    return this.emitter.on('did-stop', callback)
  }

  // Private: Register a callback to be invoked with any errors reported from the watcher.
  //
  // Returns: A {Disposable} to revoke the subscription.
  onDidError (callback) {
    return this.emitter.on('did-error', callback)
  }

  // Private: Broadcast an `onShouldDetach` event to prompt any {PathWatcher} instances bound here to attach to a new
  // {NativeWatcher} instead.
  //
  // * `replacement` the new {NativeWatcher} instance that a live {PathWatcher} instance should reattach to instead.
  // * `watchedPath` absolute path watched by the new {NativeWatcher}.
  reattachTo (replacement, watchedPath) {
    this.emitter.emit('should-detach', {replacement, watchedPath})
  }

  // Private: Stop the native watcher and release any operating system resources associated with it.
  //
  // Has no effect if the watcher is not running.
  async stop () {
    if (this.state === STOPPING) {
      await new Promise(resolve => this.emitter.once('did-stop', resolve))
      return
    }

    if (this.state !== RUNNING || !this.channel) {
      return
    }
    this.state = STOPPING
    this.emitter.emit('will-stop')

    await new Promise((resolve, reject) => {
      binding.unwatch(this.channel, err => (err ? reject(err) : resolve()))
    })
    this.channel = null
    this.state = STOPPED

    this.emitter.emit('did-stop')
  }

  // Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive.
  // Normalize and re-broadcast them to any subscribers.
  //
  // * `events` An Array of filesystem events.
  onEvents (err, events) {
    if (err) {
      return this.onError(err)
    }

    const translated = events.map(event => {
      const n = {
        action: ACTIONS.get(event.action),
        kind: ENTRIES.get(event.kind),
        path: event.path
      }

      if (event.oldPath !== '') n.oldPath = event.oldPath

      return n
    })

    this.emitter.emit('did-change', translated)
  }

  // Private: Callback function invoked by the native watcher when an error occurs.
  //
  // * `err` The native filesystem error.
  onError (err) {
    this.emitter.emit('did-error', err)
  }
}

module.exports = {NativeWatcher}

@@ -1322,17 +1322,43 @@
 ate 
-!
+=== RUNNING %7C%7C this.state =
 == STOPP
 ED) 
@@ -1353,18 +1353,19 @@
 == STOPP
-ED
+ING
 ) %7B%0A    
@@ -4474,59 +4474,256 @@
 ate 
-!== RUNNING %7C%7C !this.channel) %7B%0A      return
+=== STARTING) %7B%0A      await new Promise(resolve =%3E this.emitter.once('did-start', resolve))%0A    %7D%0A%0A    if (this.state === STOPPED) %7B%0A      return%0A    %7D%0A%0A    if (!this.channel) %7B%0A      throw new Error('Cannot stop a watcher with no channel')
 %0A    %7D%0A
+%0A
     
@@ -5806,16 +5806,178 @@
 err)%0A  %7D
+%0A%0A  // Extended: Report this watcher's path and state.%0A  toString () %7B%0A    return %60%5BNativeWatcher path=$%7Bthis.normalizedPath%7D state=$%7Bthis.state.toString()%7D%5D%60%0A  %7D
 %0A%7D%0A%0Amodu

Handle .start() and .stop() while the watcher is in any state