util = require 'util'
validUrl = require 'valid-url'
isImage = require 'is-image'
normalizeNewline = require 'normalize-newline'
try
  {Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, User} = require 'hubot'
catch
  prequire = require 'parent-require' 
  {Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, User} = prequire 'hubot'
  
HangupsClient = require 'hangupsjs'
Promise = require "bluebird"
 
  # An adapter is a specific interface to a chat source for robots.
class HangupsJS extends Adapter

  # robot - A Robot instance.
  constructor: (@robot) ->

  # Public: Raw method for invoking the bot to run. Extend this.
  #
  # Returns nothing.
  run: ->
    @robot.logger.debug 'Run'
    
    @client = new HangupsClient()
    
    #TODO: 7 = DEBUG, 6 = INFO, ... 4 = WARNING, 3 = ERROR
    #@client.loglevel @robot.logger.level
    
    @initUserList = []
    @robot.brain.on 'loaded', @.brainLoaded
    
    @client.on 'connecting', @.connecting
    @client.on 'connected', @.connected
    @client.on 'connect_failed', @.connect_failed

    @client.on 'chat_message', @.chat_message
    @client.on 'membership_change', @.membership_change
    @client.on 'conversation_rename', @.conversation_rename
    
    @creds = -> auth: () -> process.env.HUBOT_GOOGLE_AUTH_TOKEN || HangupsClient.authStdin()
    @client.connect @creds

  brainLoaded: =>
    @robot.logger.info 'Brain Loaded...updating user list'
    @updateUsers @initUserList

  connecting: =>
    @robot.logger.info 'Client Connecting'

  connected: =>
    @robot.logger.info 'Client Connected'
    @client.setpresence(true)
    @client.getselfinfo().then( (self) =>
      @robot.logger.debug self
      @self = self
    ).then( () => 
      @emit "connected" # Tell Hubot we're connected so it can load scripts
    ).then( () =>
      #reach into init object for all conversations
      chatIds = []
      for cs in @client.init.conv_states
        chatIds = chatIds.concat(cs.conversation.current_participant.map((o) -> o.chat_id))
      chatIds = chatIds.filter( (val, index, self) -> index == self.indexOf(val) )

      @getUsers(chatIds).then( (users) => 
        @initUserList = users #stash users for delayed brain load
        @updateUsers(users) #update users for synchronous brain
      )
    ).catch (error) =>
      @robot.logger.error error

    
  connect_failed: (err) =>
    @robot.logger.info "Client Connect Failed #{err}"
    Promise.delay(3000).then(() => @client.connect @creds)

  chat_message: (msg) =>
    @robot.logger.debug 'chat_message'
    @robot.logger.debug msg

    return if msg.sender_id.chat_id == @self.self_entity.id.chat_id
    return if not msg.chat_message.message_content.segment #currently don't support attachments

    Promise.join @getSender(msg), msg, (users, msg) =>
      user = users[0]
      msgSegment = msg.chat_message.message_content.segment
      @robot.logger.debug msgSegment

      #msgAttachment = msg.chat_message.message_content.attachment
      #@robot.logger.debug msgAttachment
      #@robot.logger.debug msgAttachment[0].embed_item.type_
      #@robot.logger.debug msgAttachment[0].embed_item.data

      text = msgSegment.map( (m) -> m.text)
        .filter((i) -> i)
        .join('')

      @robot.logger.debug "Chat message: #{text}"
    
      user.room = msg.conversation_id.id
      @receive new TextMessage(user, text, msg.event_id)
      @updateWatermark msg
    .catch (error) =>
      @robot.logger.error error

  membership_change: (msg) =>
    @robot.logger.debug 'membership_change'
    @robot.logger.info msg
    
    for participant in msg.membership_change.participant_ids
      #TODO: batch participants
      Promise.join @getUsers(participant.chat_id), (userList) =>
        user = userList[0]
        if msg.membership_change.type == "JOIN"
          @updateUsers userList
          user.room = msg.conversation_id.id # try to keep room out of brain
          m = new EnterMessage(user, null, msg.event_id)
        else if msg.membership_change.type == "LEAVE"
          user.room = msg.conversation_id.id
          m = new LeaveMessage(user, null, msg.event_id)

        @receive m
        @updateWatermark msg
      .catch (error) =>
        @robot.logger.error error

  conversation_rename: (msg) =>
    @robot.logger.debug 'conversation_rename'
    @robot.logger.info msg

    Promise.join @getSender(msg), msg, (user, msg) =>
      newName = msg.conversation_rename.new_name
      @robot.logger.debug "Conversation Rename: #{newName}"
      user.room = msg.conversation_id.id
      @receive new TopicMessage(user, newName, msg.event_id)
      @updateWatermark msg
    .catch (error) =>
      @robot.logger.error error


  getSender: (msg) =>
    @getUsers(msg.sender_id.chat_id)

  getUsers: (userIds) =>
    @robot.logger.debug 'getUsers'

    @client.getentitybyid([].concat(userIds)).then (res) => 
      for entity in res.entities
        @robot.logger.debug entity.id.chat_id
        @robot.logger.debug entity.properties
        @robot.logger.debug entity.properties.emails
        userProps = entity.properties
        new User(entity.id.chat_id, {first_name: userProps.first_name, name: userProps.display_name
        display_name: userProps.display_name, photo_url: userProps.photo_url, emails: userProps.emails })

  updateUsers: (users) =>
    @robot.logger.debug 'updateUsers'

    for user in [].concat(users)
      if user.id of @robot.brain.data.users
        for key, value of @robot.brain.data.users[user.id]
          unless key of user
            user[key] = value
      delete @robot.brain.data.users[user.id]
      @robot.brain.userForId user.id, user

  updateWatermark: (msg) =>
    @client.updatewatermark(msg.conversation_id.id, msg.timestamp / 1000)#.then( (res) => @robot.logger.debug res)
    @client.setfocus(msg.conversation_id.id)#.then( (res) => @robot.logger.debug res)

  # Public: Raw method for sending data back to the chat source. Extend this.
  #
  # envelope - A Object with message, room and user details.
  # strings  - One or more Strings for each message to send.
  #
  # Returns nothing.
  send: (envelope, strings...) ->
    @robot.logger.debug 'Send'
    builder = new HangupsClient.MessageBuilder()
    for msg in strings
      if isImage(msg) # todo, handle images better
        builder.link(msg,msg)
      else if validUrl.isUri(msg)
        builder.link(msg,msg)
      else
        msgArray = normalizeNewline(msg).split("\n")
        for m, i in msgArray    
          builder.text m
          if i != msgArray.length - 1 then builder.linebreak()

    @client.sendchatmessage envelope.room, builder.toSegments()
    
  # Public: Raw method for sending emote data back to the chat source.
  # Defaults as an alias for send
  #
  # envelope - A Object with message, room and user details.
  # strings  - One or more Strings for each message to send.
  #
  # Returns nothing.
  emote: (envelope, strings...) ->
    @robot.logger.debug 'Emote'
    @send envelope, strings...

  # Public: Raw method for building a reply and sending it back to the chat
  # source. Extend this.
  #
  # envelope - A Object with message, room and user details.
  # strings  - One or more Strings for each reply to send.
  #
  # Returns nothing.
  reply: (envelope, strings...) ->
    @robot.logger.debug 'Reply'
    @send envelope, strings...

  # Public: Raw method for setting a topic on the chat source. Extend this.
  #
  # envelope - A Object with message, room and user details.
  # strings  - One more more Strings to set as the topic.
  #
  # Returns nothing.
  topic: (envelope, strings...) ->
    @robot.logger.debug 'Topic'
    @client.renameconversation envelope.room, strings.join "\n"

  # Public: Raw method for playing a sound in the chat source. Extend this.
  #
  # envelope - A Object with message, room and user details.
  # strings  - One or more strings for each play message to send.
  #
  # Returns nothing
  play: (envelope, strings...) ->
    @robot.logger.debug 'Play'

  # Public: Raw method for shutting the bot down. Extend this.
  #
  # Returns nothing.
  close: ->
    @robot.logger.debug 'Close'
    @client.setpresence(false).then( (res) => @robot.logger.debug res)
    @client.disconnect().then( (res) => @robot.logger.debug res)

  # Public: Dispatch a received message to the robot.
  #
  # Returns nothing.
  receive: (message) ->
    @robot.logger.debug 'Receive'
    @robot.logger.debug message
    @robot.receive message

module.exports = HangupsJS