module Consolvix
  module TransactionAPI
    protected
    def transaction_started?
      not @transaction.nil?
    end

    def cleanup_transaction
      @transaction.destroy
      @transaction = nil
    end

    def load_transaction
      unless params[:tid].blank?
        t = ConsolvixTransaction.find_by_number(params[:tid])
        @transaction = t if t and not t.is_complete?
      end
      true # ensure this filter doesn't break the filter chain
    end

    def save_transaction
      @transaction.save! if transaction_started?
    end

    def start_transaction(description, action, controller=nil)
      controller ||= controller_name
      @transaction = ConsolvixTransaction.new(:controller  => controller,
                                              :action      => action.to_s,
                                              :user_id     => @current_user,
                                              :description => description)
    end

    def complete_transaction
      @transaction.complete = true if transaction_started?
      save_transaction
    end
  end

  class Application
    # here we basically add [load_transaction] as a [before_filter] to the
    # actions specified in args.
    # args is an Array of action names (Symbols, not Strings!) that
    # may use @transaction, the current [ConsolvixTransaction] object
    # it is NOT NECESSARY to declare methods here that are given to
    # [define_transaction_handler] for those actions are automagically
    # being taken care of.
    def self.load_transaction_for(*args)
      options = args.last.is_a?(Hash) ? args.pop : {}
      options.merge! :only => args
      unless args.empty?
        class_eval do
          before_filter :load_transaction, options
        end
      end
    end

    # This basically adds a [save_transaction] as an [after_filter]
    # to the action specified in args.
    # args is an Array of action names that potentially change the state
    # of @transaction, the currently running [ConsolvixTransaction] object.
    # it is NOT NECESSARY to declare methods here that are given to
    # [define_transaction_handler] for those actions are automagically
    # being taken care of.
    def self.save_transaction_for(*args)
      options = args.last.is_a?(Hash) ? args.pop : {}
      options.merge! :only => args
      unless args.empty?
        class_eval do
          after_filter :save_transaction, options
        end
      end
    end

    def self.transaction_handler(description, main_action, *actions)
      sub_actions     = actions.last.is_a?(Hash) ? actions.pop : {}
      worker_action   = sub_actions.delete(:worker)   || "do_#{main_action}".to_sym
      selector_action = sub_actions.delete(:selector) || "select_transact_#{main_action}".to_sym
      action_map      = []
      @transaction_descriptors ||= {}
      sub_actions.each do |number, data|
        action_map[number.to_i] = {:action => data[0], :prerequisite => data[1]}
      end
      @transaction_descriptors[main_action] = { :selector_action  => selector_action,
                                                :description      => description,
                                                :main_action      => main_action,
                                                :worker_action    => worker_action,
                                                :total_step_count => action_map.size - 1,
                                                :action_map       => action_map }

      create_main_action_handler(@transaction_descriptors[main_action])
      create_worker_action_handler(@transaction_descriptors[main_action])
      create_tid_selection_handler(@transaction_descriptors[main_action])
      create_transaction_handler_stubs(@transaction_descriptors[main_action])

# #      FIXME: does not work when called repeadedly! (i.e. for n>1 transaction handlers per controller)
      class_eval do
        load_transaction_for main_action,
                             worker_action,
                             selector_action,
                             :abort_transaction
        save_transaction_for main_action,
                             worker_action
      end
    end

    # Abort the currently running transaction (identified by params[:tid]);
    # Can be overridden in the implementing class for custom behaviour.
    # However, the implementation below should do fine in most cases.
    def abort_transaction
      if transaction_started?
        worker_action = transaction_descriptor[:worker_action]
        cleanup_transaction # here, @transaction will be completely destroyed
        flash[:notice] = 'Aborted, you can start again now.'
      else
        flash[:error] = 'No transaction ID provided, nothing aborted.'
        worker_action = 'index'
      end
      redirect_to :controller => controller_name,
                  :action     => worker_action
    end

    protected
    # returnd the meta information for the currently active
    # transaction that is stored as a singleton class variable
    # of the controller
    def transaction_descriptor
      if transaction_started?
        # retrun the descriptor for the currently running transaction
        main_action = @transaction.action.to_sym
        return self.class.instance_variable_get('@transaction_descriptors')[main_action]
      else
        # try to identify transaction metadata by finding out wether
        # the currently called action is the worker or selector action
        main_action, descriptor = self.class.instance_variable_get('@transaction_descriptors').find do |key, descr|
          descr[:worker_action] == action_name.to_sym
        end
        if descriptor
          return descriptor
        else
          raise "The called action #{action_name} does not belong to any Transaction!"
        end
      end
    end

    # define the main action for this transaction.
    # In most cases, this should do fine and neets not be overwritten in a child class.
    # However, if you do, remember to call [super] first before calling own operations!
    # ...Or just remember to call [start_transaction] with the right parametres and then
    # redirect to the worker action.
    def self.create_main_action_handler(descriptor)
      class_eval do
        public
        define_method descriptor[:main_action] do
#           load_transaction
          abort_transaction and return if request.delete?
          start_transaction(descriptor[:description],
                            descriptor[:main_action],
                            controller_name) if @transaction.nil?
          redirect_to(:controller => controller_name,
                      :action     => descriptor[:worker_action],
                      :id         => params[:id],
                      :step       => 1,
                      :tid        => @transaction.number)
#           save_transaction
        end
      end
    end

    # This defines the main worker action that basically just
    # acts as a proxy to [handle_transaction]
    def self.create_worker_action_handler(descriptor)
      class_eval do
        public
        define_method descriptor[:worker_action] do
#           load_transaction
          return send(:handle_transaction)
#           save_transaction
        end
      end
    end

    # This defines a stub for the transaction selection action.
    # It can be implemented in the child class, however it is sufficient
    # if the views folder for the active controller contains a
    # *.rhtml file named after the [selector_action] specified above.
    def self.create_tid_selection_handler(descriptor)
      class_eval do
        define_method descriptor[:selector_action] do
#           load_transaction
          @open_transactions = ConsolvixTransaction.find_all_by_controller_and_action(controller_name, descriptor[:main_action].to_s).collect {|t| ["#{t.description} (#{t.number})", t.number]}
          begin
            render :action => descriptor[:selector_action], :layout => !request.xhr?
          rescue
            render :text => "Here you should be seeing a selector for currently running transactions for #{descriptor[:main_action]}. However, you don`t seem to have implemented it yet!<br />
            It is sufficient to create /app/views/#{controller_name}/#{descriptor[:selector_action]}) with a form that sends a valid `tid` to <strong<#{descriptor[:worker_action]}</strong>. Just create a select_tag that takes its values from @open_transactions"
          end
        end
      end
    end

    def self.create_transaction_handler_stubs(descriptor)
      descriptor[:action_map].each do |a|
        next if a.nil?
        case a[:action]
        when descriptor[:main_action]
          raise "Found transaction name `#{a[:action]}' to be one of the steps for transaction `#{descriptor[:main_action]}' in `#{controller_name}'. However, this is not allowed!\nAll prerequisite actions of a transaction should be implemented as private or protected methods in order for them not to be callable directly."
        when descriptor[:worker_action]
          raise "Found transaction interface name `#{a[:action]}' to be one of the steps for transaction `#{descriptor[:main_action]}' in `#{controller_name}'. However, this is not allowed!\nAll substeps of a transaction should be implemented as private or protected methods in order for them not to be callable directly."
        end

        create_step_method_stub(a)
        create_prerequisite_method_stub(a) unless a[:prerequisite].nil?
      end
    end

    # Here we define empty stub actions for all transaction steps.
    # They must be implemented in the child class.
    def self.create_step_method_stub(map)
      class_eval do
        define_method map[:action] do
          render :text => "This is is an empty implementation of <strong>#{map[:action]}</strong>. Prerequisite <strong>#{map[:prerequisite]||'[NONE]'}</strong> is OK if you can read this. Please implement me now!" and return
        end
      end
    end

    # Let's just predefine all prerequisite actions.
    # They also have to be overwritten in the child class.
    # By default, they all return FALSE (i.e. prerequisite failed)
    def self.create_prerequisite_method_stub(map)
      class_eval do
        define_method map[:prerequisite] do
          return false
        end
      end
    end

    # returns the called step number or otherwise the highest
    # step number available in this transaction
    def requested_step_no
      if params[:step].to_i <= last_step_of_transaction and params[:step].to_i > 0
        return params[:step].to_i
      else
        return last_step_of_transaction
      end
    end

    def last_step_of_transaction
      transaction_descriptor[:total_step_count]
    end

    def redirect_to_next_if_step_done
      if transaction_started?
        prerequisite = transaction_descriptor[:action_map][requested_step_no+1][:prerequisite]
        if prerequisite and send prerequisite
          redirect_to :action => transaction_descriptor[:worker_action],
                      :step   => last_step_of_transaction,
                      :id     => params[:id],
                      :tid    => @transaction.number
        end
      end
    end

    def handle_transaction
      abort_transaction and return if request.delete?
      if transaction_started?
        # This incrementally checks the prerequisite for the step called.
        # If the prerequisite is OK, the requested step is called.
        # If not, forward to the highest step number whose prerequisite is fulfilled.
        1.upto requested_step_no do |i|
          action_map = transaction_descriptor[:action_map][i]
         if i == requested_step_no and (action_map[:prerequisite].nil? or send(action_map[:prerequisite]))
            return send(action_map[:action])
          elsif action_map[:prerequisite] and not send(action_map[:prerequisite])
            redirect_to :controller => controller_name,
                        :action     => transaction_descriptor[:worker_action],
                        :id         => params[:id],
                        :step       => i-1,
                        :tid        => @transaction.number and return
          else
            redirect_to :controller => controller_name,
                        :action     => transaction_descriptor[:worker_action],
                        :id         => params[:id],
                        :step       => 1,
                        :tid        => @transaction.number and return

          end
        end
      else
  #       FIXME:
  #       @open_transactions = ConsolvixTransaction.find_all_by_controller_and_action_and_user_id(controller_name, 'cha_do_create', @current_user.id) || []
        transaction_count = ConsolvixTransaction.count(:conditions => "controller='#{controller_name}' AND action='#{transaction_descriptor[:main_action]}'")
        if transaction_count == 0
          redirect_to :action => 'index' and return
        else
          # one or more transaction found, show selection
          redirect_to :action => transaction_descriptor[:selector_action] and return
        end
      end
    end
  end
end

