module Teacup
  module_function

  # Merges two Hashes.  This is similar to `Hash#merge`, except the values will
  # be merged recursively (aka deep merge) when both values for a key are
  # Hashes, and values for the *left* argument are preferred over values on the
  # right.
  #
  # If you pass in a third argument, it will be acted upon directly instead of
  # creating a new Hash.  Usually used with `merge_defaults!`, which merges values
  # from `right` into `left`.
  def merge_defaults(left, right, target={})
    if target != left
      left.each do |key, value|
        if target.has_key? key and value.is_a?(Hash) and target[key].is_a?(Hash)
          target[key] = Teacup::merge_defaults(target[key], value)
        else
          if value.is_a?(Hash)
            # make a copy of the Hash
            value = Teacup::merge_defaults!({}, value)
          end
          target[key] = value
        end
      end
    end

    right.each do |key, value|
      if not target.has_key? key
        target[key] = value
      elsif value.is_a?(Hash) and left[key].is_a?(Hash)
        target[key] = Teacup::merge_defaults(left[key], value, (left==target ? left[key] : {}))
      elsif key == :constraints
        left[key] = merge_constraints(left[key], value)
      end
    end
    target
  end

  # constraints are a special case because when we merge an array of constraints
  # we need to make sure not to add more than one constraint for a given attribute
  def merge_constraints(left, right)
    # Return early if there are no constraints to merge.
    return right unless left

    left = left.map do |constraint|
      convert_constraints(constraint)
    end.flatten
    right = right.map do |constraint|
      convert_constraints(constraint)
    end.flatten

    constrained_attributes = left.map do |constraint|
      constraint.attribute
    end
    additional_constraints = right.reject do |constraint|
      constrained_attributes.include?(constraint.attribute)
    end
    left + additional_constraints
  end

  def convert_constraints(constraints)
    if constraints.is_a? Array
      constraints.map do |constraint|
        convert_constraints(constraint)
      end
    elsif constraints.is_a? Hash
      constraints.map do |sym, relative_to|
        convert_constraints(Teacup::Constraint.from_sym(sym, relative_to))
      end
    elsif constraints.is_a?(Symbol)
      Teacup::Constraint.from_sym(constraints)
    elsif constraints.is_a?(Teacup::Constraint)
      constraints
    else
      raise "Unsupported constraint: #{constraints.inspect}"
    end
  end

  # modifies left by passing it in as the `target`.
  def merge_defaults!(left, right)
    Teacup::merge_defaults(left, right, left)
  end
end
