require 'active_support/inflector/inflections'

module Neo4j
  module ActiveNode
    module HasN
      class Association
        attr_reader :type, :name, :relationship, :direction

        def initialize(type, direction, name, options = {})
          check_valid_type_and_dir(type, direction)
          @type = type.to_sym
          @name = name
          @direction = direction.to_sym
          @target_class_name_from_name = name.to_s.classify

          set_vars_from_options(options)
        end

        def target_class_option(options)
          options[:model_class].nil? ? @target_class_name_from_name : options[:model_class]
        end

        # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition)
        def arrow_cypher(var = nil, properties = {}, create = false)
          validate_origin!
          relationship_type = relationship_type(create)
          relationship_name_cypher = ":`#{relationship_type}`" if relationship_type
          properties_string = get_properties_string(properties)
          relationship_cypher = get_relationship_cypher(var, relationship_name_cypher, properties_string)
          get_direction(relationship_cypher, create)
        end

        def target_class_name
          @target_class_option.to_s if @target_class_option
        end

        def target_class
          return @target_class if @target_class

          @target_class = target_class_name.constantize if target_class_name
        rescue NameError
          raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified"
        end

        def callback(type)
          @callbacks[type]
        end

        def perform_callback(caller, other_node, type)
          return if callback(type).nil?
          caller.send(callback(type), other_node)
        end

        def relationship_type(create = false)
          case
          when @relationship_class
            relationship_class_type
          when @relationship_type
            @relationship_type
          when @origin
            origin_type
          else
            (create || exceptional_target_class?) && "##{@name}"
          end
        end

        def relationship_class
          @relationship_class
        end

        def relationship_class_type
          @relationship_class = @relationship_class.constantize if @relationship_class.class == String || @relationship_class == Symbol
          @relationship_class._type
        end

        def relationship_class_name
          @relationship_class_name ||= @relationship_class.respond_to?(:constantize) ? @relationship_class : @relationship_class.name
        end

        def inject_classname(properties)
          properties[Neo4j::Config.class_name_property] = relationship_class_name if @relationship_class
          properties
        end

        private

        def get_direction(relationship_cypher, create)
          dir = (create && @direction == :both) ? :out : @direction
          case dir
          when :out
            "-#{relationship_cypher}->"
          when :in
            "<-#{relationship_cypher}-"
          when :both
            "-#{relationship_cypher}-"
          end
        end

        def get_relationship_cypher(var, relationship_name_cypher, properties_string)
          "[#{var}#{relationship_name_cypher}#{properties_string}]"
        end

        def get_properties_string(properties)
          p = properties.map do |key, value|
            "#{key}: #{value.inspect}"
          end.join(', ')
          p.size == 0 ? '' : " {#{p}}"
        end

        def origin_type
          target_class.associations[@origin].relationship_type
        end

        private

        def set_vars_from_options(options)
          validate_option_combinations(options)
          @target_class_option = target_class_option(options)
          @callbacks = {before: options[:before], after: options[:after]}
          @origin = options[:origin] && options[:origin].to_sym
          @relationship_class = options[:rel_class]
          @relationship_type  = options[:type] && options[:type].to_sym
        end

        # Return basic details about association as declared in the model
        # @example
        #   has_many :in, :bands
        def base_declaration
          "#{type} #{direction.inspect}, #{name.inspect}"
        end

        def check_valid_type_and_dir(type, direction)
          raise ArgumentError, "Invalid association type: #{type.inspect} (valid value: :has_many and :has_one)" if not [:has_many, :has_one].include?(type.to_sym)
          raise ArgumentError, "Invalid direction: #{direction.inspect} (valid value: :out, :in, and :both)" if not [:out, :in, :both].include?(direction.to_sym)
        end

        def validate_option_combinations(options)
          raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" if options[:type] && options[:origin]
          # raise ArgumentError, "Cannot specify both :type and :rel_class (#{base_declaration})" if options[:type] && options[:rel_class] see issue #494
          # raise ArgumentError, "Cannot specify both :origin and :rel_class (#{base_declaration}" if options[:origin] && options[:rel_class]
        end

        # Determine if model class as derived from the association name would be different than the one specified via the model_class key
        # @example
        #   has_many :friends                 # Would return false
        #   has_many :friends, model_class: Friend  # Would return false
        #   has_many :friends, model_class: Person  # Would return true
        def exceptional_target_class?
          # TODO: Exceptional if target_class.nil?? (when model_class false)

          target_class && target_class.name != @target_class_name_from_name
        end

        def validate_origin!
          if @origin
            if target_class
              if association = target_class.associations[@origin]
                if @direction == association.direction
                  raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)"
                end
              else
                raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})"
              end
            else
              raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)"
            end
          end
        end
      end
    end
  end
end