module Hanuman

  #
  # For stages that can be linked to directly
  # Including this means your stage has exactly one input (itself).
  #
  module IsOwnInputSlot
    extend  Gorillib::Concern
    include Inlinkable
    included do
      magic   :input,    Hanuman::Stage, :writer => false, :tester => true, :doc => 'stage/slot in graph that feeds into this one'
    end
    def inputs
      input?  ? [input] : []
    end
  end

  #
  # For stages that can be linked to directly
  # Including this means your stage has exactly one output (itself).
  #
  module IsOwnOutputSlot
    extend  Gorillib::Concern
    include Outlinkable
    included do
      magic  :output,   Hanuman::Stage, :writer => false, :tester => true, :doc => 'stage/slot in graph this one feeds into'
    end
    def outputs
      output? ? [output] : []
    end
  end

  #
  # For stages with named slots
  #
  # A named slot is a special kind of field: saying
  #
  #   consumes :brain
  #
  # gives your class
  #
  # * A normal attribute `brain_slot`
  # * methods `brain_slot`, `receive_brain_slot` to go with it
  # * method `brain`, returning the item (if any) connected to the brain slot
  # * method `brain=` (alias for `receive_brain`) that links the brain slot with the given item
  #
  # @note that at the moment you can't have an input and an output with the same name.
  #
  module Slottable
    extend Gorillib::Concern
    include Inlinkable
    include Outlinkable

    included do
      collection :outslots, Hanuman::OutputSlot, :key_method => :name
    end

    def inputs
      inslots.to_a.map{|slot| slot.input }.compact
    end

    def inslots
      self.class.inslot_fields.map{|_, slot_field| read_attribute(slot_field.name) }
    end

    def handle_extra_attributes(attrs)
      self.class.inslot_fields.each do |_, field|
        field_name = field.basename
        next unless attrs.has_key?(field_name)
        self.public_send(:"receive_#{field_name}", attrs.delete(field_name))
      end
      super(attrs)
    end

    module ClassMethods
      def consumes(name, options={})
        field name, Hanuman::Stage, {:field_type => InputSlotField}.merge(options)
      end
      def produces(name, options={})
        field name, Hanuman::Stage, {:field_type => OutputSlotField}.merge(options)
      end

      def define_slot_reader(field)
        meth_name  = field.basename
        slot_name  = field.name
        type       = field.type
        define_meta_module_method(meth_name, true) do ||
          begin
            slot = read_attribute(slot_name) or return nil
            slot.other
          rescue StandardError => err ; err.polish("#{self.class}.#{meth_name}") rescue nil ; raise ; end
        end
      end

      def define_inslot_receiver(field)
        meth_name  = field.basename
        slot_name  = field.name
        type       = field.type
        define_meta_module_method("receive_#{meth_name}", true) do |stage|
          begin
            slot = read_attribute(slot_name) or return nil
            slot.from(stage)
            self
          rescue StandardError => err ; err.polish("#{self.class} set slot #{meth_name} to #{stage}") rescue nil ; raise ; end
        end
        meta_module.module_eval do
          alias_method "#{meth_name}=", "receive_#{meth_name}"
        end
      end

      def define_outslot_receiver(field)
        meth_name  = field.basename
        slot_name  = field.name
        type       = field.type
        define_meta_module_method("receive_#{meth_name}", true) do |stage|
          begin
            slot = read_attribute(slot_name) or return nil
            slot.into(stage)
            self
          rescue StandardError => err ; err.polish("#{self.class} set slot #{meth_name} to #{stage}") rescue nil ; raise ; end
        end
        meta_module.module_eval do
          alias_method "#{meth_name}=", "receive_#{meth_name}"
        end
      end

      def inslot_fields
        fields.select{|_, field| field.is_a?(InputSlotField) }
      end

      def inslot_field?(field_name)
        fields[field_name].is_a?(InputSlotField)
      end
    end

    class SlotField < Gorillib::Model::Field
      self.visibilities = visibilities.merge(:reader => true, :writer => false, :tester => false)
      field :basename,   Symbol
      field :stage_type, Whatever, :doc => 'type for stages this slot accepts'
      class_attribute :slot_type

      def initialize(model, basename, type, options={})
        name = "#{basename}_slot"
        options[:stage_type] = type
        slot_type = self.slot_type
        options[:basename] = basename
        options[:default]  = ->{ slot_type.new(:name => basename, :stage => self) }
        super(model, name, slot_type, options)
      end
    end

    class InputSlotField < SlotField
      self.slot_type = Hanuman::InputSlot
      def inscribe_methods(model)
        model.__send__(:define_slot_reader, self)
        model.__send__(:define_inslot_receiver, self)
        super
      end
    end

    class OutputSlotField < SlotField
      self.slot_type = Hanuman::OutputSlot
      def inscribe_methods(model)
        model.__send__(:define_slot_reader, self)
        model.__send__(:define_outslot_receiver, self)
        super
      end
    end

  end # Slottable

  module SplatInputs
    extend  Gorillib::Concern
    include Slottable

    included do
      collection :splat_inslots, Hanuman::InputSlot, :key_method => :name
    end

    def set_input(stage)
      slot = Hanuman::InputSlot.new(:name => stage.name, :stage => self, :input => stage)
      self.splat_inslots << slot
      slot
    end

    def has_input?(slot_name)
      self.splat_inslots.keys.include?(slot_name)
    end

    def inslots
      super + splat_inslots.to_a
    end
  end

  module SplatOutputs
    extend  Gorillib::Concern
    include Slottable

    included do
      collection :splat_outslots, Hanuman::OutputSlot, :key_method => :name
    end

    def set_output(stage)
      slot = Hanuman::OutputSlot.new(
        :name => stage.name, :stage => self, :output => stage)
      self.outslots << slot
      slot
    end

    def outputs
      outslots.to_a.map{|slot| slot.output }
    end

    def into(*others)
      others.each{|other| super(other)}
      self
    end
  end

end