# Robut plugins implement a simple interface to listen for messages
# and optionally respond to them. All plugins include the Robut::Plugin
# module.
module Robut::Plugin

  # Contains methods that will be added directly to a class including
  # Robut::Plugin.
  module ClassMethods

    # Sets up a 'matcher' that will try to match input being sent to
    # this plugin with a regular expression. If the expression
    # matches, +action+ will be performed. +action+ will be passed any
    # captured groups in the regular expression as parameters. For
    # example:
    #
    #     match /^say hello to (\w+)/ do |name| ...
    #
    # The action is run in the context of an instance of a class that
    # includes Robut::Plugin. Like +handle+, an action explicitly
    # returning +true+ will stop the plugin chain from matching any
    # further.
    #
    # Supported options:
    #   :sent_to_me - only try to match this regexp if it contains an @reply to robut.
    #                 This will also strip the @reply from the message we're trying
    #                 to match on, so ^ and $ will still do the right thing.
    def match(regexp, options = {}, &action)
      matchers << [regexp, options, action, @last_description]
      @last_description = nil
    end

    # Provides a description for the next matcher
    def desc(string)
      @last_description = string
    end

    # A list of regular expressions to apply to input being sent to
    # the plugin, along with blocks of actions to perform. Each
    # element is a [regexp, options, action, description] array.
    def matchers
      @matchers ||= []
    end

    # Allows you to define http methods that this plugin should respond to.
    # This is useful if you're want to accept generic post data for Robut to
    # display in all rooms (e.g. Heroku deploy hooks, GitHub post receive
    # hooks, etc).
    #
    #     http do
    #       post '/heroku' do
    #         payload = Hashie::Mash.new(params)
    #         say "#{payload.user} deployed #{payload.head} to #{payload.app}", nil
    #         halt 200
    #       end
    #     end
    #
    # The action is run in the context of a Sinatra app (+Robut::Web+). So
    # anything that Sinatra provides is available within the block.
    def http(&block)
      Robut::Web.class_eval &block
    end
  end

  class << self
    # A list of all available plugin classes. When you require a new
    # plugin class, you should add it to this list if you want it to
    # respond to messages.
    attr_accessor :plugins
  end

  self.plugins = []

  # A reference to the connection attached to this instance of the
  # plugin. This is mostly used to communicate back to the server.
  attr_accessor :connection

  # If we are handling a private message, holds a reference to the
  # sender of the message. +nil+ if the message was sent to the entire
  # room.
  attr_accessor :private_sender

  attr_accessor :reply_to

  # :nodoc:
  def self.included(klass)
    klass.send(:extend, ClassMethods)
  end

  # Creates a new instance of this plugin to reply to a particular
  # object over that object's connection
  def initialize(reply_to, private_sender = nil)
    self.reply_to = reply_to
    self.connection = reply_to.connection
    self.private_sender = private_sender
  end

  # Send +message+ back to the HipChat server. If +to+ == +:room+,
  # replies to the room. If +to+ == nil, responds in the manner the
  # original message was sent. Otherwise, PMs the message to +to+.
  def reply(message, to = nil)
    if to == :room
      reply_to.reply(message, nil)
    else
      reply_to.reply(message, to || private_sender)
    end
  end

  # An ordered list of all words in the message with any reference to
  # the bot's nick stripped out. If +command+ is passed in, it is also
  # stripped out. This is useful to separate the 'parameters' from the
  # 'commands' in a message.
  def words(message, command = nil)
    reply = at_nick.downcase
    command = command.downcase if command
    message.split.reject {|word| word.downcase == reply || word.downcase == command }
  end

  # Removes the first word in message if it is a reference to the bot's nick
  # Given "@robut do this thing", Returns "do this thing"
  def without_nick(message)
    possible_nick, command = message.split(' ', 2)
    if possible_nick.casecmp(at_nick) == 0
      command
    else
      message
    end
  end

  # The bot's nickname, for @-replies.
  def nick
    connection.config.mention_name || connection.config.nick.gsub(/[^0-9a-z]/i, '')
  end

  # #nick with the @-symbol prepended
  def at_nick
    "@#{nick}"
  end

  # Was +message+ sent to Robut as an @reply?
  def sent_to_me?(message)
    message =~ /(^|\s)@#{nick}(\s|$)/i
  end

  # Do whatever you need to do to handle this message.
  # If you want to stop the plugin execution chain, return +true+ from this
  # method.  Plugins are handled in the order that they appear in
  # Robut::Plugin.plugins
  def handle(time, sender_nick, message)
    if matchers.empty?
      raise NotImplementedError, "Implement me in #{self.class.name}!"
    else
      find_match(message)
    end
  end

  # Returns a list of messages describing the commands this plugin
  # handles.
  def usage
    matchers.map do |regexp, options, action, description|
      next unless description
      if options[:sent_to_me]
        at_nick + " " + description
      else
        description
      end
    end.compact
  end

  def fake_message(time, sender_nick, msg)
    # TODO: ensure this connection is threadsafe
    plugins = Robut::Plugin.plugins.map { |p| p.new(reply_to, private_sender) }
    reply_to.handle_message(plugins, time, sender_nick, msg)
  end

  # Accessor for the store instance
  def store
    connection.store
  end

  private

  # Find and run all the actions associated with matchers that match
  # +message+.
  def find_match(message)
    matchers.each do |regexp, options, action, description|
      if options[:sent_to_me] && !sent_to_me?(message)
        next
      end

      if match_data = without_nick(message).match(regexp)
        # Return true explicitly if this matcher explicitly returned true
        break true if instance_exec(*match_data[1..-1], &action) == true
      end
    end
  end

  # The matchers defined by this plugin's class
  def matchers
    self.class.matchers
  end
end
