#
# Fluentd
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#

module Fluent
  module Config
    class ConfigureProxy
      attr_accessor :name, :final, :param_name, :init, :required, :multi, :alias, :configured_in_section
      attr_accessor :argument, :params, :defaults, :descriptions, :sections
      # config_param :desc, :string, default: '....'
      # config_set_default :buffer_type, :memory
      #
      # config_section :default, required: true, multi: false do
      #   config_argument :arg, :string
      #   config_param :required, :bool, default: false
      #   config_param :name, :string
      #   config_param :power, :integer
      # end
      #
      # config_section :child, param_name: 'children', required: false, multi: true, alias: 'node' do
      #   config_param :name, :string
      #   config_param :power, :integer, default: nil
      #   config_section :item do
      #     config_param :name
      #   end
      # end

      def initialize(name, root: false, param_name: nil, final: nil, init: nil, required: nil, multi: nil, alias: nil, type_lookup:)
        @name = name.to_sym
        @final = final

        # For ConfigureProxy of root section, "@name" should be a class name of plugins.
        # Otherwise (like subsections), "@name" should be a name of section, like "buffer", "store".
        # For subsections, name will be used as parameter names (unless param_name exists), so overriding proxy's name
        #   should override "@name".
        @root_section = root

        @param_name = param_name && param_name.to_sym
        @init = init
        @required = required
        @multi = multi
        @alias = binding.local_variable_get(:alias)
        @type_lookup = type_lookup

        raise "init and required are exclusive" if @init && @required

        # specify section name for viewpoint of owner(parent) plugin
        # for buffer plugins: all params are in <buffer> section of owner
        # others: <storage>, <format> (formatter/parser), ...
        @configured_in_section = nil

        @argument = nil # nil: ignore argument
        @params = {}
        @defaults = {}
        @descriptions = {}
        @sections = {}
        @current_description = nil
      end

      def variable_name
        @param_name || @name
      end

      def root?
        @root_section
      end

      def init?
        @init.nil? ? false : @init
      end

      def required?
        @required.nil? ? false : @required
      end

      def multi?
        @multi.nil? ? true : @multi
      end

      def final?
        !!@final
      end

      def merge(other) # self is base class, other is subclass
        return merge_for_finalized(other) if self.final?

        [:param_name, :required, :multi, :alias, :configured_in_section].each do |prohibited_name|
          if overwrite?(other, prohibited_name)
            raise ConfigError, "BUG: subclass cannot overwrite base class's config_section: #{prohibited_name}"
          end
        end

        options = {}
        # param_name affects instance variable name, which is just "internal" of each plugins.
        # so it must not be changed. base class's name (or param_name) is always used.
        options[:param_name] = @param_name

        # subclass cannot overwrite base class's definition
        options[:init] = @init.nil? ? other.init : self.init
        options[:required] = @required.nil? ? other.required : self.required
        options[:multi] = @multi.nil? ? other.multi : self.multi
        options[:alias] = @alias.nil? ? other.alias : self.alias
        options[:final] = @final || other.final
        options[:type_lookup] = @type_lookup

        merged = if self.root?
                   options[:root] = true
                   self.class.new(other.name, **options)
                 else
                   self.class.new(@name, **options)
                 end

        # configured_in MUST be kept
        merged.configured_in_section = self.configured_in_section || other.configured_in_section

        merged.argument = other.argument || self.argument
        merged.params = self.params.merge(other.params)
        merged.defaults = self.defaults.merge(other.defaults)
        merged.sections = {}
        (self.sections.keys + other.sections.keys).uniq.each do |section_key|
          self_section = self.sections[section_key]
          other_section = other.sections[section_key]
          merged_section = if self_section && other_section
                             self_section.merge(other_section)
                           elsif self_section || other_section
                             self_section || other_section
                           else
                             raise "BUG: both of self and other section are nil"
                           end
          merged.sections[section_key] = merged_section
        end

        merged
      end

      def merge_for_finalized(other)
        # list what subclass can do for finalized section
        #  * append params/defaults/sections which are missing in superclass
        #  * change default values of superclass
        #  * overwrite init to make it enable to instantiate section objects with added default values

        if other.final == false && overwrite?(other, :final)
          raise ConfigError, "BUG: subclass cannot overwrite finalized base class's config_section"
        end

        [:param_name, :required, :multi, :alias, :configured_in_section].each do |prohibited_name|
          if overwrite?(other, prohibited_name)
            raise ConfigError, "BUG: subclass cannot overwrite base class's config_section: #{prohibited_name}"
          end
        end

        options = {}
        options[:param_name] = @param_name
        options[:init] = @init || other.init
        options[:required] = @required.nil? ? other.required : self.required
        options[:multi] = @multi.nil? ? other.multi : self.multi
        options[:alias] = @alias.nil? ? other.alias : self.alias
        options[:final]  = true
        options[:type_lookup] = @type_lookup

        merged = if self.root?
                   options[:root] = true
                   self.class.new(other.name, **options)
                 else
                   self.class.new(@name, **options)
                 end

        merged.configured_in_section = self.configured_in_section || other.configured_in_section

        merged.argument = self.argument || other.argument
        merged.params = other.params.merge(self.params)
        merged.defaults = self.defaults.merge(other.defaults)
        merged.sections = {}
        (self.sections.keys + other.sections.keys).uniq.each do |section_key|
          self_section = self.sections[section_key]
          other_section = other.sections[section_key]
          merged_section = if self_section && other_section
                             other_section.merge(self_section)
                           elsif self_section || other_section
                             self_section || other_section
                           else
                             raise "BUG: both of self and other section are nil"
                           end
          merged.sections[section_key] = merged_section
        end

        merged
      end

      def overwrite_defaults(other) # other is owner plugin's corresponding proxy
        self.defaults = self.defaults.merge(other.defaults)
        self.sections.each_key do |section_key|
          if other.sections.has_key?(section_key)
            self.sections[section_key].overwrite_defaults(other.sections[section_key])
          end
        end
      end

      def option_value_type!(name, opts, key, klass=nil, type: nil)
        if opts.has_key?(key)
          if klass && !opts[key].is_a?(klass)
            raise ArgumentError, "#{name}: #{key} must be a #{klass}, but #{opts[key].class}"
          end
          case type
          when :boolean
            unless opts[key].is_a?(TrueClass) || opts[key].is_a?(FalseClass)
              raise ArgumentError, "#{name}: #{key} must be true or false, but #{opts[key].class}"
            end
          when nil
            # ignore
          else
            raise "unknown type: #{type} for option #{key}"
          end
        end
      end

      def config_parameter_option_validate!(name, type, **kwargs, &block)
        if type.nil? && !block
          type = :string
        end
        kwargs.each_key do |key|
          case key
          when :default, :alias, :secret, :skip_accessor, :deprecated, :obsoleted, :desc
            # valid for all types
          when :list
            raise ArgumentError, ":list is valid only for :enum type, but #{type}: #{name}" if type != :enum
          when :value_type
            raise ArgumentError, ":value_type is valid only for :hash and :array, but #{type}: #{name}" if type != :hash && type != :array
          when :symbolize_keys
            raise ArgumentError, ":symbolize_keys is valid only for :hash, but #{type}: #{name}" if type != :hash
          else
            raise ArgumentError, "unknown option '#{key}' for configuration parameter: #{name}"
          end
        end
      end

      def parameter_configuration(name, type = nil, **kwargs, &block)
        config_parameter_option_validate!(name, type, **kwargs, &block)

        name = name.to_sym

        if block && type
          raise ArgumentError, "#{name}: both of block and type cannot be specified"
        elsif !block && !type
          type = :string
        end
        opts = {}
        opts[:type] = type
        opts.merge!(kwargs)

        begin
          block ||= @type_lookup.call(type)
        rescue ConfigError
          # override error message
          raise ArgumentError, "#{name}: unknown config_argument type `#{type}'"
        end

        # options for config_param
        option_value_type!(name, opts, :desc, String)
        option_value_type!(name, opts, :alias, Symbol)
        option_value_type!(name, opts, :secret, type: :boolean)
        option_value_type!(name, opts, :deprecated, String)
        option_value_type!(name, opts, :obsoleted, String)
        if type == :enum
          if !opts.has_key?(:list) || !opts[:list].is_a?(Array) || opts[:list].empty? || !opts[:list].all?(Symbol)
            raise ArgumentError, "#{name}: enum parameter requires :list of Symbols"
          end
        end
        option_value_type!(name, opts, :symbolize_keys, type: :boolean)
        option_value_type!(name, opts, :value_type, Symbol) # hash, array
        option_value_type!(name, opts, :skip_accessor, type: :boolean)

        if opts.has_key?(:default)
          config_set_default(name, opts[:default])
        end

        if opts.has_key?(:desc)
          config_set_desc(name, opts[:desc])
        end

        if opts[:deprecated] && opts[:obsoleted]
          raise ArgumentError, "#{name}: both of deprecated and obsoleted cannot be specified at once"
        end

        [name, block, opts]
      end

      def configured_in(section_name)
        if @configured_in_section
          raise ArgumentError, "#{self.name}: configured_in called twice"
        end
        @configured_in_section = section_name.to_sym
      end

      def config_argument(name, type = nil, **kwargs, &block)
        if @argument
          raise ArgumentError, "#{self.name}: config_argument called twice"
        end
        name, block, opts = parameter_configuration(name, type, **kwargs, &block)

        @argument = [name, block, opts]
        name
      end

      def config_param(name, type = nil, **kwargs, &block)
        name, block, opts = parameter_configuration(name, type, **kwargs, &block)

        if @current_description
          config_set_desc(name, @current_description)
          @current_description = nil
        end

        @sections.delete(name)
        @params[name] = [block, opts]
        name
      end

      def config_set_default(name, defval)
        name = name.to_sym

        if @defaults.has_key?(name)
          raise ArgumentError, "#{self.name}: default value specified twice for #{name}"
        end

        @defaults[name] = defval
        nil
      end

      def config_set_desc(name, description)
        name = name.to_sym

        if @descriptions.has_key?(name)
          raise ArgumentError, "#{self.name}: description specified twice for #{name}"
        end

        @descriptions[name] = description
        nil
      end

      def desc(description)
        @current_description = description
      end

      def config_section(name, **kwargs, &block)
        unless block_given?
          raise ArgumentError, "#{name}: config_section requires block parameter"
        end
        name = name.to_sym

        sub_proxy = ConfigureProxy.new(name, type_lookup: @type_lookup, **kwargs)
        sub_proxy.instance_exec(&block)

        @params.delete(name)
        @sections[name] = sub_proxy

        name
      end

      def dump_config_definition
        dumped_config = {}
        if @argument
          argument_name, _block, options = @argument
          options[:required] = !@defaults.key?(argument_name)
          options[:argument] = true
          dumped_config[argument_name] = options
        end
        @params.each do |name, config|
          dumped_config[name] = config[1]
          dumped_config[name][:required] = !@defaults.key?(name)
          dumped_config[name][:default] = @defaults[name] if @defaults.key?(name)
          dumped_config[name][:desc] = @descriptions[name] if @descriptions.key?(name)
        end
        # Overwrite by config_set_default
        @defaults.each do |name, value|
          if @params.key?(name) || (@argument && @argument.first == name)
            dumped_config[name][:default] = value
          else
            dumped_config[name] = { default: value }
          end
        end
        # Overwrite by config_set_desc
        @descriptions.each do |name, value|
          if @params.key?(name)
            dumped_config[name][:desc] = value
          else
            dumped_config[name] = { desc: value }
          end
        end
        @sections.each do |section_name, sub_proxy|
          if dumped_config.key?(section_name)
            dumped_config[section_name].update(sub_proxy.dump_config_definition)
          else
            dumped_config[section_name] = sub_proxy.dump_config_definition
            dumped_config[section_name][:required] = sub_proxy.required?
            dumped_config[section_name][:multi] = sub_proxy.multi?
            dumped_config[section_name][:alias] = sub_proxy.alias
            dumped_config[section_name][:section] = true
          end
        end
        dumped_config
      end

      private

      def overwrite?(other, attribute_name)
        value = instance_variable_get("@#{attribute_name}")
        other_value = other.__send__(attribute_name)
        !value.nil? && !other_value.nil? && value != other_value
      end
    end
  end
end
