module Bond
  # Occurs when a mission is incorrectly defined.
  class InvalidMissionError < StandardError; end
  # Occurs when a mission action is incorrectly defined.
  class InvalidMissionActionError < StandardError; end
  # Occurs when a mission or search action fails.
  class FailedExecutionError < StandardError; end
  # Namespace for subclasses of Bond::Mission.
  class Missions; end

  # A set of conditions and actions to take for a completion scenario or mission in Bond's mind.
  class Mission
    include Search

    class<<self
      # default search used across missions
      attr_accessor :default_search
      # Handles creation of proper Mission class depending on the options passed.
      def create(options)
        if options[:method]
          Missions::MethodMission.new(options)
        elsif options[:object]
          Missions::ObjectMission.new(options)
        else
          new(options)
        end
      end
      #:stopdoc:
      def action_object
        @action_object ||= Object.new.extend(Actions)
      end

      def current_eval(string, eval_binding=nil)
        eval_binding ||= default_eval_binding
        eval(string, eval_binding)
      end

      def default_eval_binding
        Object.const_defined?(:IRB) ? IRB.CurrentContext.workspace.binding : ::TOPLEVEL_BINDING
      end

      def default_search
        @default_search ||= :default
      end
      #:startdoc:
    end

    attr_reader :action, :condition, :place
    OPERATORS = ["%", "&", "*", "**", "+",  "-",  "/", "<", "<<", "<=", "<=>", "==", "===", "=~", ">", ">=", ">>", "[]", "[]=", "^"]

    # Options are almost the same as those explained at Bond.complete. The only difference is that the action is passed
    # as an :action option here.
    def initialize(options)
      raise InvalidMissionError unless (options[:action] || respond_to?(:default_action)) &&
        (options[:on] || is_a?(Missions::DefaultMission))
      raise InvalidMissionError if options[:on] && !options[:on].is_a?(Regexp)
      @action = options[:action].is_a?(Symbol) && self.class.action_object.respond_to?(options[:action]) ?
        self.class.action_object.method(options[:action]) : options[:action]
      raise InvalidMissionActionError if @action && !@action.respond_to?(:call)
      @condition = options[:on]
      @place = options[:place]
      @search = options.has_key?(:search) ? options[:search] : Mission.default_search
      @search = method("#{@search}_search") unless @search.is_a?(Proc) || @search == false
    end

    # Returns a boolean indicating if a mission matches the given input.
    def matches?(input)
      @matched = @input = @list_prefix = nil
      if (match = handle_valid_match(input))
        @input.instance_variable_set("@matched", @matched)
        @input.instance_eval("def self.matched; @matched ; end")
      end
      !!match
    end

    # Called when a mission has been chosen to autocomplete.
    def execute(input=@input)
      completions = @action.call(input)
      completions = (completions || []).map {|e| e.to_s }
      completions =  @search.call(input || '', completions) if @search
      if @completion_prefix
        @completion_prefix = @completion_prefix.split(Regexp.union(*Readline::DefaultBreakCharacters.split('')))[-1]
        completions = completions.map {|e| @completion_prefix + e }
      end
      completions
    rescue
      error_message = "Mission action failed to execute properly. Check your mission action with pattern #{@condition.inspect}.\n" +
        "Failed with error: #{$!.message}"
      raise FailedExecutionError, error_message
    end

    #:stopdoc:
    def unique_id
      @condition
    end

    def set_input(input, match)
      @input = input[/\S+$/]
    end

    def handle_valid_match(input)
      if (match = input.match(@condition))
        set_input(input, match)
        @matched ||= match
      end
      match
    end
    #:startdoc:
  end
end