module Cucumber
  class Factory

    ATTRIBUTES_PATTERN = '( with the .+?)?( (?:which|who|that) is .+?)?'

    NAMED_RECORDS_VARIABLE = :'@named_cucumber_factory_records'

    CLEAR_NAMED_RECORDS_STEP_DESCRIPTOR = {
      :kind => :Before,
      :block => proc { instance_variable_set(NAMED_RECORDS_VARIABLE, {}) }
    }

    NAMED_CREATION_STEP_DESCRIPTOR = {
      :kind => :Given,
      :pattern => /^"([^\"]*)" is an? (.+?)( \(.+?\))?#{ATTRIBUTES_PATTERN}?$/,
      # we cannot use vararg blocks here in Ruby 1.8, as explained by Aslak: http://www.ruby-forum.com/topic/182927
      :block => lambda { |a1, a2, a3, a4, a5| Cucumber::Factory.send(:parse_named_creation, self, a1, a2, a3, a4, a5) }
    }

    CREATION_STEP_DESCRIPTOR = {
      :kind => :Given,
      :pattern => /^there is an? (.+?)( \(.+?\))?#{ATTRIBUTES_PATTERN}$/,
       # we cannot use vararg blocks here in Ruby 1.8, as explained by Aslak: http://www.ruby-forum.com/topic/182927
      :block => lambda { |a1, a2, a3, a4| Cucumber::Factory.send(:parse_creation, self, a1, a2, a3, a4) }
    }

    class << self

      attr_reader :step_definitions

      def add_steps(main)
        add_step(main, CREATION_STEP_DESCRIPTOR)
        add_step(main, NAMED_CREATION_STEP_DESCRIPTOR)
        add_step(main, CLEAR_NAMED_RECORDS_STEP_DESCRIPTOR)
      end

      private

      def add_step(main, descriptor)
        @step_definitions ||= []
        step_definition = main.instance_eval { send(descriptor[:kind], *[descriptor[:pattern]].compact, &descriptor[:block]) }
        @step_definitions << step_definition
      end

      def get_named_record(world, name)
        world.instance_variable_get(NAMED_RECORDS_VARIABLE)[name]
      end

      def set_named_record(world, name, record)
        world.instance_variable_get(NAMED_RECORDS_VARIABLE)[name] = record
      end
  
      def parse_named_creation(world, name, raw_model, raw_variant, raw_attributes, raw_boolean_attributes)
        record = parse_creation(world, raw_model, raw_variant, raw_attributes, raw_boolean_attributes)
        set_named_record(world, name, record)
      end
    
      def parse_creation(world, raw_model, raw_variant, raw_attributes, raw_boolean_attributes)
        model_class = model_class_from_prose(raw_model)
        attributes = {}
        if raw_attributes.try(:strip).present?
          raw_attributes.scan(/(?:the|and|with|but|,| )+(.*?) ("([^\"]*)"|above)/).each do |fragment|
            attribute = attribute_name_from_prose(fragment[0])
            value_type = fragment[1] # 'above' or a quoted string
            value = fragment[2] # the value string without quotes
            attributes[attribute] = attribute_value(world, model_class, attribute, value_type, value)
          end
        end
        if raw_boolean_attributes.try(:strip).present?
          raw_boolean_attributes.scan(/(?:which|who|that|is| )*(not )?(.+?)(?: and | but |,|$)+/).each do |fragment|
            flag = !fragment[0] # if the word 'not' didn't match above, this expression is true
            attribute = attribute_name_from_prose(fragment[1])
            attributes[attribute] = flag
          end
        end
        variant = raw_variant.present? && /\((.*?)\)/.match(raw_variant)[1].downcase.gsub(" ", "_")
        record = create_record(model_class, variant, attributes)
        remember_record_names(world, record, attributes)
        record
      end

      def attribute_value(world, model_class, attribute, value_type, value)
        association = model_class.respond_to?(:reflect_on_association) ? model_class.reflect_on_association(attribute) : nil
        if association.present?
          if value_type == "above"
            # Don't use class.last, in sqlite that is not always the last inserted element
            value = association.klass.find(:last, :order => "id") or raise "There is no last #{attribute}"
          else
            value = get_named_record(world, value)
          end
        else
          value = world.Transform(value)
        end
        value
      end

      def attribute_name_from_prose(prose)
        prose.downcase.gsub(" ", "_").to_sym
      end

      def model_class_from_prose(prose)
        # don't use \w which depends on the system locale
        prose.gsub(/[^A-Za-z0-9_\/]+/, "_").camelize.constantize
      end

      def factory_girl_factory_name(name)
        name.to_s.underscore.to_sym
      end
      
      def create_record(model_class, variant, attributes)
        fg_factory_name = factory_girl_factory_name(variant || model_class)
        if defined?(::FactoryGirl) && factory = ::FactoryGirl.factories[fg_factory_name]
          ::FactoryGirl.create(fg_factory_name, attributes)
        elsif model_class.respond_to?(:make) # Machinist blueprint
          if variant.present?
            model_class.make(variant.to_sym, attributes)
          else
            model_class.make(attributes)
          end
        elsif model_class.respond_to?(:create!) # Plain ActiveRecord
          model = model_class.new
          model.send(:attributes=, attributes, false) # ignore attr_accessible
          model.save!
          model
        else
          model_class.new(attributes)
        end
      end

      def remember_record_names(world, record, attributes)
        string_values = attributes.values.select { |v| v.is_a?(String) }
        for string_value in string_values
          set_named_record(world, string_value, record)
        end
      end

    end
  end
end