try
  # FIXME: This is a hack for testing, otherwise instanceof doesn't work for TextMessage
  path = require('path')
  {User,Adapter,Robot,TextMessage,EnterMessage,LeaveMessage} = require path.join(module.parent.path, '..')
catch
  {User,Adapter,Robot,TextMessage,EnterMessage,LeaveMessage} = require 'hubot'


fs = require 'fs'
util = require 'util'
path = require 'path'
{EventEmitter} = require 'events'

global._ = require 'underscore'
_.str = require 'underscore.string'
{ client, xml } = require("@xmpp/client");
middleware = require("@xmpp/middleware");
debug = require("@xmpp/debug");

global.JID = require('@xmpp/client').jid
global.ltx = require('ltx')
global.uuid = require './lib/uuid'

config = require(path.join(process.cwd(), 'config'))

global.logError = => @robot.logger.error arguments...
global.log = => console.log arguments...
global.debug = => console.log arguments... if process.env.HUBOT_GENESYS_CLOUD_DEBUG;

class Realtime extends EventEmitter

    constructor: (client) ->
      super
      @client = client
    
    send: (stanza) ->
      debug 'stanza', 'out', stanza.toString()
      @client.send stanza
    
    jid: null
    
    debug: debug

    connected: false

    features: {}

class GenesysCloudBot extends Adapter

  options = null

  constructor: ->
    super
    @options = config

    [username, domain] = @options.username.split('@')
    @client = client({
      service: "xmpp://#{@options.host}:#{@options.port}",
      credentials: {
        authzid: @options.username,
        username,
        password: @options.password,
      },
    });
    # !!! HACK BELOW !!! All of thie is because it's
    # near impossible to override the host when the
    # connection is upgraded to TLS.
    @client.header = (args...) =>
      for el in args
        if el.attrs.to == @options.host
          el.attrs.to = domain
      # This comes from @xmpp/client-core
      @client.Transport.prototype.header(args...)

    debug(@client, true) if process.env.HUBOT_GENESYS_CLOUD_DEBUG

    @realtime = new Realtime @client

  reconnectTryCount: 0

  controllers: {}

  run: ->
    @robot.on 'error', (error) => console.error error

    @makeClient()

    @setupControllers()

    @realtime.on 'message', @_onMessage

  setupControllers: ->

    for fileName in fs.readdirSync __dirname + '/controllers'
      unless fileName.match /.*Controller(.coffee)?$/ then continue
      name = _.str.camelize(fileName.substring(0, fileName.indexOf('Controller'))).replace /^./, (m) -> m.toLowerCase()
      Controller = require "./controllers/#{fileName}"
      @controllers[name] = new Controller(@realtime)

      do (name, Controller) =>
        for funcName, func of Controller.expose
          debug 'handler', 'registering exposed controller method', funcName, 'in controller', name
          do (funcName, func) =>
            @realtime[funcName] = (args...) =>
              apply = =>
                func.apply @controllers[name], args
              unless @realtime.connected
                debug 'handle', 'deferring call to', funcName, @realtime.connected
                return @realtime.once 'connect', => 
                  debug 'handle', 'defer resolve', funcName
                  apply()
              else apply()

            null

        for event in Controller.exposeEvents
          do (name, event) =>
            @['on'+_.str.capitalize(event)] = (callback) => @on event, callback
            @controllers[name].on event, =>
              @realtime.emit event, arguments...

            null

  makeClient: ->
    @connected = false
    
    @client.on 'error', (error) => logError error
    
    @client.on 'online', @onConnect
    
    @client.on 'offline', =>
      log 'offline', arguments...
      @realtime.emit 'disconnect'
    
    @client.on 'stanza', @onStanza

    @client.on 'end', =>
      @robot.logger.info 'Connection closed, attempting to reconnect'
      @client.reconnect()

    if(@client.start)
      @client.start().catch(console.error);

  onConnect: () => 
    log '***************** online', @client.jid.toString(), @client.jid.bare().toString()
    @realtime.jid = @client.jid
    unless @realtime.connected
      @realtime.connected = true
      @emit 'connected'
    @realtime.emit 'connect'

  onStanza: (stanza) =>
    handled = false

    for name, controller of @controllers
      for func, test of controller.constructor.stanzas
        do (stanza) =>
          if test.apply(controller, [stanza])
            debug 'stanza', 'handled by', "#{controller.constructor.name}.#{func}"
            handled = true
            controller[func] stanza

    unless handled then debug 'stanza', 'unhandled'

    null

  _send: (envelope, messages, {message_fn, preferred_to_fn}) ->
    for message in messages
      unless message then continue
      if (!message_fn)
        message_fn = (msg) => msg
      transformedMessage = message_fn(message)
      @robot.logger.debug "Sending to #{envelope.room or envelope.user?.id}: #{transformedMessage}"

      if (!preferred_to_fn)
        to = envelope.room or envelope.user?.id
      else
        to = preferred_to_fn(envelope)

      @realtime.sendMessage to, transformedMessage

  send: (envelope, messages...) ->
    @_send(envelope, messages, {})

  emote: (envelope, messages...) ->
    @_send(envelope, messages, {
      message_fn: ((msg) => "/me #{msg}")
    })

  reply: (envelope, messages...) ->
    @_send(envelope, messages, {
      preferred_to_fn: ((envelope) => envelope.user?.id)
    })

  offline: =>
    @robot.logger.debug "Received offline event", @client.connect?
    @client.connect()
    clearInterval(@keepaliveInterval)
    @robot.logger.debug "Received offline event"
    @client.connect()

  leaveRoom: (res) =>
    to = res.envelope.room
    @robot.logger.debug 'Leaving room', to
    if to.match(/@conference/)
      @realtime.sendMessage to, 'Goodbye!', =>
        @realtime.leaveRoom to
        @realtime.setInactive to
    else
      @realtime.sendMessage res.envelope.user?.id, "Sorry, I can only leave chats!"

  _onMessage: (msg) =>
    if msg.from is @options.username.toLowerCase() then return
    if msg.body?.match /nsfw/ then return

    if msg.type is 'person'
      user = @robot.brain.userForId msg.from
      user.room = msg.from
    else 
      user = @robot.brain.userForId msg.from
      user.room = msg.to

    @receive new TextMessage(user, msg.body)


exports.use = (@robot) ->
  new GenesysCloudBot @robot
