module Celluloid
  OWNER_IVAR = :@celluloid_owner # reference to owning actor

  # Wrap the given subject with an Cell
  class Cell
    class ExitHandler
      def initialize(behavior, subject, method_name)
        @behavior = behavior
        @subject = subject
        @method_name = method_name
      end

      def call(event)
        @behavior.task(:exit_handler, @method_name) do
          @subject.send(@method_name, event.actor, event.reason)
        end
      end
    end

    def initialize(subject, options, actor_options)
      @actor                      = Actor.new(self, actor_options)
      @subject                    = subject
      @receiver_block_executions  = options[:receiver_block_executions]
      @exclusive_methods          = options[:exclusive_methods]
      @finalizer                  = options[:finalizer]

      @subject.instance_variable_set(OWNER_IVAR, @actor)

      if exit_handler_name = options[:exit_handler_name]
        @actor.exit_handler = ExitHandler.new(self, @subject, exit_handler_name)
      end

      @actor.handle(Call) do |message|
        invoke(message)
      end
      @actor.handle(Call::Block) do |message|
        task(:invoke_block) { message.dispatch }
      end
      @actor.handle(Internals::Response::Block, Internals::Response, &:dispatch)

      @actor.start
      @proxy = (options[:proxy_class] || Proxy::Cell).new(@actor.mailbox, @actor.proxy, @subject.class.to_s)
    end
    attr_reader :proxy, :subject

    def self.dispatch
      proc do |subject|
        subject[:call].dispatch(subject[:subject])
        subject[:call] = nil
        subject[:subject] = nil
      end
    end

    def invoke(call)
      meth = call.method
      meth = call.arguments.first if meth == :__send__
      if @receiver_block_executions && meth
        call.execute_block_on_receiver if @receiver_block_executions.include?(meth.to_sym)
      end

      task(:call, meth, { call: call, subject: @subject },
           dangerous_suspend: meth == :initialize, &Cell.dispatch)
    end

    def task(task_type, method_name = nil, subject = nil, meta = nil, &_block)
      meta ||= {}
      meta[:method_name] = method_name
      @actor.task(task_type, meta) do
        if @exclusive_methods && method_name && @exclusive_methods.include?(method_name.to_sym)
          Celluloid.exclusive { yield subject }
        else
          yield subject
        end
      end
    end

    def self.shutdown
      proc do |subject|
        begin
          subject[:subject].__send__(subject[:call])
        rescue => ex
          Internals::Logger.crash("#{subject[:subject].class} finalizer crashed!", ex)
        end
        subject[:call] = nil
        subject[:subject] = nil
      end
    end

    # Run the user-defined finalizer, if one is set
    def shutdown
      return unless @finalizer && @subject.respond_to?(@finalizer, true)

      task(:finalizer, @finalizer, { call: @finalizer, subject: @subject },
           dangerous_suspend: true, &Cell.shutdown)
    end
  end
end
