module Apipie

  # method parameter description
  #
  # name - method name (show)
  # desc - description
  # required - boolean if required
  # validator - Validator::BaseValidator subclass
  class ParamDescription

    attr_reader :method_description, :name, :desc, :allow_nil, :allow_blank, :validator, :options, :metadata, :show, :as, :validations, :response_only, :request_only
    attr_reader :additional_properties, :is_array
    attr_accessor :parent, :required

    alias response_only? response_only
    alias request_only? request_only
    alias is_array? is_array

    def self.from_dsl_data(method_description, args)
      param_name, validator, desc_or_options, options, block = args
      Apipie::ParamDescription.new(method_description,
                                   param_name,
                                   validator,
                                   desc_or_options,
                                   options,
                                   &block)
    end

    def to_s
      "ParamDescription: #{method_description.id}##{name}"
    end

    def ==(other)
      return false unless self.class == other.class
      if method_description == other.method_description && @options == other.options
        true
      else
        false
      end
    end

    def initialize(method_description, name, validator, desc_or_options = nil, options = {}, &block)

      if desc_or_options.is_a?(Hash)
        options = options.merge(desc_or_options)
      elsif desc_or_options.is_a?(String)
        options[:desc] = desc_or_options
      elsif !desc_or_options.nil?
        raise ArgumentError.new("param description: expected description or options as 3rd parameter")
      end

      options.symbolize_keys!

      # we save options to know what was passed in DSL
      @options = options
      if @options[:param_group]
        @from_concern = @options[:param_group][:from_concern]
      end

      if validator.is_a?(Hash)
        @options.merge!(validator.select{|k,v| k != :array_of })
      end

      @method_description = method_description
      @name = concern_subst(name)
      @as = options[:as] || @name
      @desc = preformat_text(@options[:desc])

      @parent = @options[:parent]
      @metadata = @options[:meta]

      @required = is_required?

      @response_only = (@options[:only_in] == :response)
      @request_only = (@options[:only_in] == :request)
      raise ArgumentError.new("'#{@options[:only_in]}' is not a valid value for :only_in") if (!@response_only && !@request_only) && @options[:only_in].present?

      @show = if @options.key? :show
        @options[:show]
      else
        true
      end

      @allow_nil = @options[:allow_nil] || false
      @allow_blank = @options[:allow_blank] || false

      action_awareness

      if validator
        if (validator != Hash) && (validator.is_a? Hash) && validator[:array_of]
          @is_array = true
          validator = validator[:array_of]
          raise "an ':array_of =>' validator is allowed exclusively on response-only fields" unless @response_only
        end
        @validator = Validator::BaseValidator.find(self, validator, @options, block)
        raise "Validator for #{validator} not found." unless @validator
      end

      @validations = Array(options[:validations]).map {|v| concern_subst(Apipie.markup_to_html(v)) }

      @additional_properties = @options[:additional_properties]
      @deprecated = @options[:deprecated] || false
    end

    def from_concern?
      method_description.from_concern? || @from_concern
    end

    def normalized_value(value)
      if value.is_a?(ActionController::Parameters) && !value.is_a?(Hash)
        value.to_unsafe_hash
      elsif value.is_a? Array
        value.map { |v| normalized_value (v) }
      else
        value
      end
    end

    def validate(value)
      return true if allow_nil && value.nil?
      return true if allow_blank && value.blank?
      value = normalized_value(value)
      if (!allow_nil && value.nil?) || (blank_forbidden? && value.blank?) || !validator.valid?(value)
        error = validator.error
        error = ParamError.new(error) unless error.is_a? StandardError
        raise error
      end
    end

    def blank_forbidden?
      !Apipie.configuration.ignore_allow_blank_false && !allow_blank && !validator.ignore_allow_blank?
    end

    def process_value(value)
      value = normalized_value(value)
      if @validator.respond_to?(:process_value)
        @validator.process_value(value)
      else
        value
      end
    end

    def full_name
      name_parts = parents_and_self.map{|p| p.name if p.show}.compact
      return name.to_s if name_parts.blank?
      return ([name_parts.first] + name_parts[1..-1].map { |n| "[#{n}]" }).join("")
    end

    # returns an array of all the parents: starting with the root parent
    # ending with itself
    def parents_and_self
      ret = []
      if self.parent
        ret.concat(self.parent.parents_and_self)
      end
      ret << self
      ret
    end

    def to_json(lang = nil)
      hash = {
        name: name.to_s,
        full_name: full_name,
        description: preformat_text(Apipie.app.translate(@options[:desc], lang)),
        required: required,
        allow_nil: allow_nil,
        allow_blank: allow_blank,
        validator: validator.to_s,
        expected_type: validator.expected_type,
        metadata: metadata,
        show: show,
        validations: validations,
        deprecated: deprecated?
      }

      if deprecation.present?
        hash[:deprecation] = deprecation.to_json
      end

      if sub_params = validator.params_ordered
        hash[:params] = sub_params.map { |p| p.to_json(lang)}
      end
      hash
    end

    def merge_with(other_param_desc)
      if self.validator && other_param_desc.validator
        self.validator.merge_with(other_param_desc.validator)
      else
        self.validator ||= other_param_desc.validator
      end
      self
    end

    # merge param descriptions. Allows defining hash params on more places
    # (e.g. in param_groups). For example:
    #
    #     def_param_group :user do
    #       param :user, Hash do
    #         param :name, String
    #       end
    #     end
    #
    #     param_group :user
    #     param :user, Hash do
    #       param :password, String
    #     end
    def self.unify(params)
      ordering = params.map(&:name)
      params.group_by(&:name).map do |name, param_descs|
        param_descs.reduce(&:merge_with)
      end.sort_by { |param| ordering.index(param.name) }
    end

    def self.merge(target_params, source_params)
      params_to_merge, params_to_add = source_params.partition do |source_param|
        target_params.any? { |target_param| source_param.name == target_param.name }
      end
      unify(target_params + params_to_merge)
      target_params.concat(params_to_add)
    end

    # action awareness is being inherited from ancestors (in terms of
    # nested params)
    def action_aware?
      if @options.key?(:action_aware)
        return @options[:action_aware]
      elsif @parent
        @parent.action_aware?
      else
        false
      end
    end

    def as_action
      if @options[:param_group] && @options[:param_group][:options] &&
          @options[:param_group][:options][:as]
        @options[:param_group][:options][:as].to_s
      elsif @parent
        @parent.as_action
      else
        @method_description.method
      end
    end

    # makes modification that are based on the action that the param
    # is defined for. Typical for required and allow_nil variations in
    # create/update actions.
    def action_awareness
      if action_aware?
        if !@options.key?(:allow_nil)
          if @required
            @allow_nil = false
          else
            @allow_nil = true
          end
        end
        if as_action != "create"
          @required = false
        end
      end
    end

    def concern_subst(string)
      return string if string.nil? or !from_concern?

      original = string
      string = ":#{original}" if original.is_a? Symbol

      replaced = method_description.resource.controller._apipie_perform_concern_subst(string)

      return original if replaced == string
      return replaced.to_sym if original.is_a? Symbol
      return replaced
    end

    def preformat_text(text)
      concern_subst(Apipie.markup_to_html(text || ''))
    end

    def is_required?
      if @options.key?(:required)
        if (@options[:required] == true) || (@options[:required] == false)
          @options[:required]
        else
          Array(@options[:required]).include?(@method_description.method.to_sym)
        end
      else
        Apipie.configuration.required_by_default?
      end
    end

    def deprecated?
      @deprecated.present?
    end

    def deprecation
      return if @deprecated.blank? || @deprecated == true

      case @deprecated
      when Hash
        Apipie::ParamDescription::Deprecation.new(
          info: @deprecated[:info],
          deprecated_in: @deprecated[:in],
          sunset_at: @deprecated[:sunset]
        )
      when String
        Apipie::ParamDescription::Deprecation.new(info: @deprecated)
      end
    end
  end

end
