var util = require('util')

var Promise = require('bluebird')
var syrup = require('stf-syrup')
var split = require('split')
var EventEmitter = require('eventemitter3').EventEmitter
var adbkit = require('adbkit')
var Parser = require('adbkit/lib/adb/parser')

var wire = require('../../../../wire')
var logger = require('../../../../util/logger')
var lifecycle = require('../../../../util/lifecycle')
var SeqQueue = require('../../../../wire/seqqueue')
var StateQueue = require('../../../../util/statequeue')
var RiskyStream = require('../../../../util/riskystream')
var FailCounter = require('../../../../util/failcounter')

module.exports = syrup.serial()
  .dependency(require('../../support/adb'))
  .dependency(require('../../support/router'))
  .dependency(require('../../resources/minitouch'))
  .dependency(require('../flags'))
  .define(function(options, adb, router, minitouch, flags) {
    var log = logger.createLogger('device:plugins:touch')

    function TouchConsumer(config) {
      EventEmitter.call(this)
      this.actionQueue = []
      this.runningState = TouchConsumer.STATE_STOPPED
      this.desiredState = new StateQueue()
      this.output = null
      this.socket = null
      this.banner = null
      this.touchConfig = config
      this.starter = Promise.resolve(true)
      this.failCounter = new FailCounter(3, 10000)
      this.failCounter.on('exceedLimit', this._failLimitExceeded.bind(this))
      this.failed = false
      this.readableListener = this._readableListener.bind(this)
      this.writeQueue = []
    }

    util.inherits(TouchConsumer, EventEmitter)

    TouchConsumer.STATE_STOPPED = 1
    TouchConsumer.STATE_STARTING = 2
    TouchConsumer.STATE_STARTED = 3
    TouchConsumer.STATE_STOPPING = 4

    TouchConsumer.prototype._queueWrite = function(writer) {
      switch (this.runningState) {
      case TouchConsumer.STATE_STARTED:
        writer.call(this)
        break
      default:
        this.writeQueue.push(writer)
        break
      }
    }

    TouchConsumer.prototype.touchDown = function(point) {
      this._queueWrite(function() {
        return this._write(util.format(
          'd %s %s %s %s\n'
        , point.contact
        , Math.floor(this.touchConfig.origin.x(point) * this.banner.maxX)
        , Math.floor(this.touchConfig.origin.y(point) * this.banner.maxY)
        , Math.floor((point.pressure || 0.5) * this.banner.maxPressure)
        ))
      })
    }

    TouchConsumer.prototype.touchMove = function(point) {
      this._queueWrite(function() {
        return this._write(util.format(
          'm %s %s %s %s\n'
        , point.contact
        , Math.floor(this.touchConfig.origin.x(point) * this.banner.maxX)
        , Math.floor(this.touchConfig.origin.y(point) * this.banner.maxY)
        , Math.floor((point.pressure || 0.5) * this.banner.maxPressure)
        ))
      })
    }

    TouchConsumer.prototype.touchUp = function(point) {
      this._queueWrite(function() {
        return this._write(util.format(
          'u %s\n'
        , point.contact
        ))
      })
    }

    TouchConsumer.prototype.touchCommit = function() {
      this._queueWrite(function() {
        return this._write('c\n')
      })
    }

    TouchConsumer.prototype.touchReset = function() {
      this._queueWrite(function() {
        return this._write('r\n')
      })
    }

    TouchConsumer.prototype.tap = function(point) {
      this.touchDown(point)
      this.touchCommit()
      this.touchUp(point)
      this.touchCommit()
    }

    TouchConsumer.prototype._ensureState = function() {
      if (this.desiredState.empty()) {
        return
      }

      if (this.failed) {
        log.warn('Will not apply desired state due to too many failures')
        return
      }

      switch (this.runningState) {
      case TouchConsumer.STATE_STARTING:
      case TouchConsumer.STATE_STOPPING:
        // Just wait.
        break
      case TouchConsumer.STATE_STOPPED:
        if (this.desiredState.next() === TouchConsumer.STATE_STARTED) {
          this.runningState = TouchConsumer.STATE_STARTING
          this.starter = this._startService().bind(this)
            .then(function(out) {
              this.output = new RiskyStream(out)
                .on('unexpectedEnd', this._outputEnded.bind(this))
              return this._readOutput(this.output.stream)
            })
            .then(function() {
              return this._connectService()
            })
            .then(function(socket) {
              this.socket = new RiskyStream(socket)
                .on('unexpectedEnd', this._socketEnded.bind(this))
              return this._readBanner(this.socket.stream)
            })
            .then(function(banner) {
              this.banner = banner
              return this._readUnexpected(this.socket.stream)
            })
            .then(function() {
              this._processWriteQueue()
            })
            .then(function() {
              this.runningState = TouchConsumer.STATE_STARTED
              this.emit('start')
            })
            .catch(Promise.CancellationError, function() {
              return this._stop()
            })
            .catch(function(err) {
              return this._stop().finally(function() {
                this.failCounter.inc()
                this.emit('error', err)
              })
            })
            .finally(function() {
              this._ensureState()
            })
        }
        else {
          setImmediate(this._ensureState.bind(this))
        }
        break
      case TouchConsumer.STATE_STARTED:
        if (this.desiredState.next() === TouchConsumer.STATE_STOPPED) {
          this.runningState = TouchConsumer.STATE_STOPPING
          this._stop().finally(function() {
            this._ensureState()
          })
        }
        else {
          setImmediate(this._ensureState.bind(this))
        }
        break
      }
    }

    TouchConsumer.prototype.start = function() {
      log.info('Requesting touch consumer to start')
      this.desiredState.push(TouchConsumer.STATE_STARTED)
      this._ensureState()
    }

    TouchConsumer.prototype.stop = function() {
      log.info('Requesting touch consumer to stop')
      this.desiredState.push(TouchConsumer.STATE_STOPPED)
      this._ensureState()
    }

    TouchConsumer.prototype.restart = function() {
      switch (this.runningState) {
      case TouchConsumer.STATE_STARTED:
      case TouchConsumer.STATE_STARTING:
        this.starter.cancel()
        this.desiredState.push(TouchConsumer.STATE_STOPPED)
        this.desiredState.push(TouchConsumer.STATE_STARTED)
        this._ensureState()
        break
      }
    }

    TouchConsumer.prototype._configChanged = function() {
      this.restart()
    }

    TouchConsumer.prototype._socketEnded = function() {
      log.warn('Connection to minitouch ended unexpectedly')
      this.failCounter.inc()
      this.restart()
    }

    TouchConsumer.prototype._outputEnded = function() {
      log.warn('Shell keeping minitouch running ended unexpectedly')
      this.failCounter.inc()
      this.restart()
    }

    TouchConsumer.prototype._failLimitExceeded = function(limit, time) {
      this._stop()
      this.failed = true
      this.emit('error', new Error(util.format(
        'Failed more than %d times in %dms'
      , limit
      , time
      )))
    }

    TouchConsumer.prototype._startService = function() {
      log.info('Launching screen service')
      return minitouch.run()
        .timeout(10000)
    }

    TouchConsumer.prototype._readOutput = function(out) {
      out.pipe(split()).on('data', function(line) {
        var trimmed = line.toString().trim()

        if (trimmed === '') {
          return
        }

        if (/ERROR/.test(line)) {
          log.fatal('minitouch error: "%s"', line)
          return lifecycle.fatal()
        }

        log.info('minitouch says: "%s"', line)
      })
    }

    TouchConsumer.prototype._connectService = function() {
      function tryConnect(times, delay) {
        return adb.openLocal(options.serial, 'localabstract:minitouch')
          .timeout(10000)
          .then(function(out) {
            return out
          })
          .catch(function(err) {
            if (/closed/.test(err.message) && times > 1) {
              return Promise.delay(delay)
                .then(function() {
                  return tryConnect(--times, delay * 2)
                })
            }
            return Promise.reject(err)
          })
      }
      log.info('Connecting to minitouch service')
      // SH-03G can be very slow to start sometimes. Make sure we try long
      // enough.
      return tryConnect(5, 100)
    }

    TouchConsumer.prototype._stop = function() {
      return this._disconnectService(this.socket).bind(this)
        .timeout(2000)
        .then(function() {
          return this._stopService(this.output).timeout(10000)
        })
        .then(function() {
          this.runningState = TouchConsumer.STATE_STOPPED
          this.emit('stop')
        })
        .catch(function(err) {
          // In practice we _should_ never get here due to _stopService()
          // being quite aggressive. But if we do, well... assume it
          // stopped anyway for now.
          this.runningState = TouchConsumer.STATE_STOPPED
          this.emit('error', err)
          this.emit('stop')
        })
        .finally(function() {
          this.output = null
          this.socket = null
          this.banner = null
        })
    }

    TouchConsumer.prototype._disconnectService = function(socket) {
      log.info('Disconnecting from minitouch service')

      if (!socket || socket.ended) {
        return Promise.resolve(true)
      }

      socket.stream.removeListener('readable', this.readableListener)

      var endListener
      return new Promise(function(resolve/*, reject*/) {
          socket.on('end', endListener = function() {
            resolve(true)
          })

          socket.stream.resume()
          socket.end()
        })
        .finally(function() {
          socket.removeListener('end', endListener)
        })
    }

    TouchConsumer.prototype._stopService = function(output) {
      log.info('Stopping minitouch service')

      if (!output || output.ended) {
        return Promise.resolve(true)
      }

      var pid = this.banner ? this.banner.pid : -1

      function waitForEnd() {
        var endListener
        return new Promise(function(resolve/*, reject*/) {
            output.expectEnd().on('end', endListener = function() {
              resolve(true)
            })
          })
          .finally(function() {
            output.removeListener('end', endListener)
          })
      }

      function kill(signal) {
        if (pid <= 0) {
          return Promise.reject(new Error('Minitouch service pid is unknown'))
        }

        log.info('Sending SIGTERM to minitouch')
        return Promise.all([
          waitForEnd()
        , adb.shell(options.serial, ['kill', signal, pid])
            .then(adbkit.util.readAll)
            .timeout(2000)
            .return(true)
        ])
      }

      function kindKill() {
        return kill('-15')
      }

      function forceKill() {
        return kill('-9')
      }

      function forceEnd() {
        log.info('Ending minitouch I/O as a last resort')
        output.end()
        return Promise.resolve(true)
      }

      return kindKill()
        .catch(Promise.TimeoutError, forceKill)
        .catch(forceEnd)
    }

    TouchConsumer.prototype._readBanner = function(socket) {
      log.info('Reading minitouch banner')

      var parser = new Parser(socket)
      var banner = {
        pid: -1 // @todo
      , version: 0
      , maxContacts: 0
      , maxX: 0
      , maxY: 0
      , maxPressure: 0
      }

      function readVersion() {
        return parser.readLine()
          .then(function(chunk) {
            var args = chunk.toString().split(/ /g)
            switch (args[0]) {
              case 'v':
                banner.version = +args[1]
                break
              default:
                throw new Error(util.format(
                  'Unexpected output "%s", expecting version line'
                , chunk
                ))
            }
          })
      }

      function readLimits() {
        return parser.readLine()
          .then(function(chunk) {
            var args = chunk.toString().split(/ /g)
            switch (args[0]) {
              case '^':
                banner.maxContacts = args[1]
                banner.maxX = args[2]
                banner.maxY = args[3]
                banner.maxPressure = args[4]
                break
              default:
                throw new Error(util.format(
                  'Unknown output "%s", expecting limits line'
                , chunk
                ))
            }
          })
      }

      function readPid() {
        return parser.readLine()
          .then(function(chunk) {
            var args = chunk.toString().split(/ /g)
            switch (args[0]) {
              case '$':
                banner.pid = +args[1]
                break
              default:
                throw new Error(util.format(
                  'Unexpected output "%s", expecting pid line'
                , chunk
                ))
            }
          })
      }

      return readVersion()
        .then(readLimits)
        .then(readPid)
        .return(banner)
        .timeout(2000)
    }

    TouchConsumer.prototype._readUnexpected = function(socket) {
      socket.on('readable', this.readableListener)

      // We may already have data pending.
      this.readableListener()
    }

    TouchConsumer.prototype._readableListener = function() {
      var chunk

      while ((chunk = this.socket.stream.read())) {
        log.warn('Unexpected output from minitouch socket', chunk)
      }
    }

    TouchConsumer.prototype._processWriteQueue = function() {
      for (var i = 0, l = this.writeQueue.length; i < l; ++i) {
        this.writeQueue[i].call(this)
      }

      this.writeQueue = []
    }

    TouchConsumer.prototype._write = function(chunk) {
      this.socket.stream.write(chunk)
    }

    function startConsumer() {
      var touchConsumer = new TouchConsumer({
        // Usually the touch origin is the same as the display's origin,
        // but sometimes it might not be.
        origin: (function(origin) {
          log.info('Touch origin is %s', origin)
          return {
            'top left': {
              x: function(point) {
                return point.x
              }
            , y: function(point) {
                return point.y
              }
            }
            // So far the only device we've seen exhibiting this behavior
            // is Yoga Tablet 8.
          , 'bottom left': {
              x: function(point) {
                return 1 - point.y
              }
            , y: function(point) {
                return point.x
              }
            }
          }[origin]
        })(flags.get('forceTouchOrigin', 'top left'))
      })

      var startListener, errorListener

      return new Promise(function(resolve, reject) {
        touchConsumer.on('start', startListener = function() {
          resolve(touchConsumer)
        })

        touchConsumer.on('error', errorListener = reject)

        touchConsumer.start()
      })
      .finally(function() {
        touchConsumer.removeListener('start', startListener)
        touchConsumer.removeListener('error', errorListener)
      })
    }

    return startConsumer()
      .then(function(touchConsumer) {
        var queue = new SeqQueue(100, 4)

        touchConsumer.on('error', function(err) {
          log.fatal('Touch consumer had an error', err.stack)
          lifecycle.fatal()
        })

        router
          .on(wire.GestureStartMessage, function(channel, message) {
            queue.start(message.seq)
          })
          .on(wire.GestureStopMessage, function(channel, message) {
            queue.push(message.seq, function() {
              queue.stop()
            })
          })
          .on(wire.TouchDownMessage, function(channel, message) {
            queue.push(message.seq, function() {
              touchConsumer.touchDown(message)
            })
          })
          .on(wire.TouchMoveMessage, function(channel, message) {
            queue.push(message.seq, function() {
              touchConsumer.touchMove(message)
            })
          })
          .on(wire.TouchUpMessage, function(channel, message) {
            queue.push(message.seq, function() {
              touchConsumer.touchUp(message)
            })
          })
          .on(wire.TouchCommitMessage, function(channel, message) {
            queue.push(message.seq, function() {
              touchConsumer.touchCommit()
            })
          })
          .on(wire.TouchResetMessage, function(channel, message) {
            queue.push(message.seq, function() {
              touchConsumer.touchReset()
            })
          })

        return touchConsumer
      })
  })
