# encoding: utf-8
require "logstash/util/loggable"
require "fileutils"
require "logstash/util/byte_value"
require "logstash/util/environment_variables"
require "logstash/util/time_value"

module LogStash
  class Settings

    include LogStash::Util::EnvironmentVariables
    
    def initialize
      @settings = {}
      # Theses settings were loaded from the yaml file
      # but we didn't find any settings to validate them,
      # lets keep them around until we do `validate_all` at that
      # time universal plugins could have added new settings.
      @transient_settings = {}
    end

    def register(setting)
      if @settings.key?(setting.name)
        raise ArgumentError.new("Setting \"#{setting.name}\" has already been registered as #{setting.inspect}")
      else
        @settings[setting.name] = setting
      end
    end

    def get_setting(setting_name)
      setting = @settings[setting_name]
      raise ArgumentError.new("Setting \"#{setting_name}\" hasn't been registered") if setting.nil?
      setting
    end

    def get_subset(setting_regexp)
      regexp = setting_regexp.is_a?(Regexp) ? setting_regexp : Regexp.new(setting_regexp)
      settings = self.class.new
      @settings.each do |setting_name, setting|
        next unless setting_name.match(regexp)
        settings.register(setting.clone)
      end
      settings
    end

    def set?(setting_name)
      get_setting(setting_name).set?
    end

    def clone
      get_subset(".*")
    end

    def get_default(setting_name)
      get_setting(setting_name).default
    end

    def get_value(setting_name)
      get_setting(setting_name).value
    end
    alias_method :get, :get_value

    def set_value(setting_name, value, graceful = false)
      get_setting(setting_name).set(value)
    rescue ArgumentError => e
      if graceful
        @transient_settings[setting_name] = value
      else
        raise e
      end
    end
    alias_method :set, :set_value

    def to_hash
      hash = {}
      @settings.each do |name, setting|
        hash[name] = setting.value
      end
      hash
    end

    def merge(hash, graceful = false)
      hash.each {|key, value| set_value(key, value, graceful) }
      self
    end

    def format_settings
      output = []
      output << "-------- Logstash Settings (* means modified) ---------"
      @settings.each do |setting_name, setting|
        value = setting.value
        default_value = setting.default
        if default_value == value # print setting and its default value
          output << "#{setting_name}: #{value.inspect}" unless value.nil?
        elsif default_value.nil? # print setting and warn it has been set
          output << "*#{setting_name}: #{value.inspect}"
        elsif value.nil? # default setting not set by user
          output << "#{setting_name}: #{default_value.inspect}"
        else # print setting, warn it has been set, and show default value
          output << "*#{setting_name}: #{value.inspect} (default: #{default_value.inspect})"
        end
      end
      output << "--------------- Logstash Settings -------------------"
      output
    end

    def reset
      @settings.values.each(&:reset)
    end

    def from_yaml(yaml_path)
      settings = read_yaml(::File.join(yaml_path, "logstash.yml"))
      self.merge(
        deep_replace(flatten_hash(settings)),
        true
      )
      self
    end
    
    def post_process
      if @post_process_callbacks
        @post_process_callbacks.each do |callback|
          callback.call(self)
        end
      end
    end
    
    def on_post_process(&block)
      @post_process_callbacks ||= []
      @post_process_callbacks << block
    end

    def validate_all
      # lets merge the transient_settings again to see if new setting were added.
      self.merge(@transient_settings)

      @settings.each do |name, setting|
        setting.validate_value
      end
    end

    private
    def read_yaml(path)
      YAML.safe_load(IO.read(path)) || {}
    end

    def flatten_hash(h,f="",g={})
      return g.update({ f => h }) unless h.is_a? Hash
      if f.empty?
        h.each { |k,r| flatten_hash(r,k,g) }
      else
        h.each { |k,r| flatten_hash(r,"#{f}.#{k}",g) }
      end
      g
    end
  end

  class Setting
    include LogStash::Util::Loggable

    attr_reader :name, :default

    def initialize(name, klass, default=nil, strict=true, &validator_proc)
      @name = name
      unless klass.is_a?(Class)
        raise ArgumentError.new("Setting \"#{@name}\" must be initialized with a class (received #{klass})")
      end
      @klass = klass
      @validator_proc = validator_proc
      @value = nil
      @value_is_set = false
      @strict = strict

      validate(default) if @strict
      @default = default
    end

    def value
      @value_is_set ? @value : default
    end

    def set?
      @value_is_set
    end

    def strict?
      @strict
    end

    def set(value)
      validate(value) if @strict
      @value = value
      @value_is_set = true
      @value
    end

    def reset
      @value = nil
      @value_is_set = false
    end

    def to_hash
      {
        "name" => @name,
        "klass" => @klass,
        "value" => @value,
        "value_is_set" => @value_is_set,
        "default" => @default,
        # Proc#== will only return true if it's the same obj
        # so no there's no point in comparing it
        # also thereś no use case atm to return the proc
        # so let's not expose it
        #"validator_proc" => @validator_proc
      }
    end

    def ==(other)
      self.to_hash == other.to_hash
    end

    def validate_value
      validate(value)
    end

    protected
    def validate(input)
      if !input.is_a?(@klass)
        raise ArgumentError.new("Setting \"#{@name}\" must be a #{@klass}. Received: #{input} (#{input.class})")
      end

      if @validator_proc && !@validator_proc.call(input)
        raise ArgumentError.new("Failed to validate setting \"#{@name}\" with value: #{input}")
      end
    end

    class Coercible < Setting
      def initialize(name, klass, default=nil, strict=true, &validator_proc)
        @name = name
        unless klass.is_a?(Class)
          raise ArgumentError.new("Setting \"#{@name}\" must be initialized with a class (received #{klass})")
        end
        @klass = klass
        @validator_proc = validator_proc
        @value = nil
        @value_is_set = false

        if strict
          coerced_default = coerce(default)
          validate(coerced_default)
          @default = coerced_default
        else
          @default = default
        end
      end
      def set(value)
        coerced_value = coerce(value)
        validate(coerced_value)
        @value = coerce(coerced_value)
        @value_is_set = true
        @value
      end

      def coerce(value)
        raise NotImplementedError.new("Please implement #coerce for #{self.class}")
      end
    end
    ### Specific settings #####

    class Boolean < Coercible
      def initialize(name, default, strict=true, &validator_proc)
        super(name, Object, default, strict, &validator_proc)
      end

      def coerce(value)
        case value
        when TrueClass, "true"
          true
        when FalseClass, "false"
          false
        else
          raise ArgumentError.new("could not coerce #{value} into a boolean")
        end
      end
    end

    class Numeric < Coercible
      def initialize(name, default=nil, strict=true)
        super(name, ::Numeric, default, strict)
      end

      def coerce(v)
        return v if v.is_a?(::Numeric)

        # I hate these "exceptions as control flow" idioms
        # but Ruby's `"a".to_i => 0` makes it hard to do anything else.
        coerced_value = (Integer(v) rescue nil) || (Float(v) rescue nil)

        if coerced_value.nil?
          raise ArgumentError.new("Failed to coerce value to Numeric. Received #{v} (#{v.class})")
        else
          coerced_value
        end
      end
    end

    class Integer < Coercible
      def initialize(name, default=nil, strict=true)
        super(name, ::Integer, default, strict)
      end

      def coerce(value)
        return value unless value.is_a?(::String)

        coerced_value = Integer(value) rescue nil

        if coerced_value.nil?
          raise ArgumentError.new("Failed to coerce value to Integer. Received #{value} (#{value.class})")
        else
          coerced_value
        end
      end
    end

    class PositiveInteger < Integer
      def initialize(name, default=nil, strict=true)
        super(name, default, strict) do |v|
          if v > 0
            true
          else
            raise ArgumentError.new("Number must be bigger than 0. Received: #{v}")
          end
        end
      end
    end

    class Port < Integer
      VALID_PORT_RANGE = 1..65535

      def initialize(name, default=nil, strict=true)
        super(name, default, strict) { |value| valid?(value) }
      end

      def valid?(port)
        VALID_PORT_RANGE.cover?(port)
      end
    end

    class PortRange < Coercible
      PORT_SEPARATOR = "-"

      def initialize(name, default=nil, strict=true)
        super(name, ::Range, default, strict=true) { |value| valid?(value) }
      end

      def valid?(range)
        Port::VALID_PORT_RANGE.first <= range.first && Port::VALID_PORT_RANGE.last >= range.last
      end

      def coerce(value)
        case value
        when ::Range
          value
        when ::Fixnum
          value..value
        when ::String
          first, last = value.split(PORT_SEPARATOR)
          last = first if last.nil?
          begin
            (Integer(first))..(Integer(last))
          rescue ArgumentError # Trap and reraise a more human error
            raise ArgumentError.new("Could not coerce #{value} into a port range")
          end
        else
          raise ArgumentError.new("Could not coerce #{value} into a port range")
        end
      end

      def validate(value)
        unless valid?(value)
          raise ArgumentError.new("Invalid value \"#{value}, valid options are within the range of #{Port::VALID_PORT_RANGE.first}-#{Port::VALID_PORT_RANGE.last}")
        end
      end
    end

    class Validator < Setting
      def initialize(name, default=nil, strict=true, validator_class=nil)
        @validator_class = validator_class
        super(name, ::Object, default, strict)
      end

      def validate(value)
        @validator_class.validate(value)
      end
    end

    class String < Setting
      def initialize(name, default=nil, strict=true, possible_strings=[])
        @possible_strings = possible_strings
        super(name, ::String, default, strict)
      end

      def validate(value)
        super(value)
        unless @possible_strings.empty? || @possible_strings.include?(value)
          raise ArgumentError.new("Invalid value \"#{value}\". Options are: #{@possible_strings.inspect}")
        end
      end
    end

    class NullableString < String
      def validate(value)
        return if value.nil?
        super(value)
      end
    end

    class ExistingFilePath < Setting
      def initialize(name, default=nil, strict=true)
        super(name, ::String, default, strict) do |file_path|
          if !::File.exists?(file_path)
            raise ::ArgumentError.new("File \"#{file_path}\" must exist but was not found.")
          else
            true
          end
        end
      end
    end

    class WritableDirectory < Setting
      def initialize(name, default=nil, strict=false)
        super(name, ::String, default, strict)
      end
      
      def validate(path)
        super(path)

        if ::File.directory?(path)
          if !::File.writable?(path)
            raise ::ArgumentError.new("Path \"#{path}\" must be a writable directory. It is not writable.")
          end
        elsif ::File.symlink?(path)
          # TODO(sissel): I'm OK if we relax this restriction. My experience
          # is that it's usually easier and safer to just reject symlinks.
          raise ::ArgumentError.new("Path \"#{path}\" must be a writable directory. It cannot be a symlink.")
        elsif ::File.exist?(path)
          raise ::ArgumentError.new("Path \"#{path}\" must be a writable directory. It is not a directory.")
        else
          parent = ::File.dirname(path)
          if !::File.writable?(parent)
            raise ::ArgumentError.new("Path \"#{path}\" does not exist and I cannot create it because the parent path \"#{parent}\" is not writable.")
          end
        end

        # If we get here, the directory exists and is writable.
        true
      end

      def value
        super.tap do |path|
          if !::File.directory?(path)
            # Create the directory if it doesn't exist.
            begin
              logger.info("Creating directory", setting: name, path: path)
              ::FileUtils.mkdir_p(path)
            rescue => e
              # TODO(sissel): Catch only specific exceptions?
              raise ::ArgumentError.new("Path \"#{path}\" does not exist, and I failed trying to create it: #{e.class.name} - #{e}")
            end
          end
        end
      end
    end

    class Bytes < Coercible
      def initialize(name, default=nil, strict=true)
        super(name, ::Fixnum, default, strict=true) { |value| valid?(value) }
      end

      def valid?(value)
        value.is_a?(Fixnum) && value >= 0
      end

      def coerce(value)
        case value
        when ::Numeric
          value
        when ::String
          LogStash::Util::ByteValue.parse(value)
        else
          raise ArgumentError.new("Could not coerce '#{value}' into a bytes value")
        end
      end

      def validate(value)
        unless valid?(value)
          raise ArgumentError.new("Invalid byte value \"#{value}\".")
        end
      end
    end

    class TimeValue < Coercible
      def initialize(name, default, strict=true, &validator_proc)
        super(name, ::Fixnum, default, strict, &validator_proc)
      end

      def coerce(value)
        return value if value.is_a?(::Fixnum)
        Util::TimeValue.from_value(value).to_nanos
      end
    end

    class ArrayCoercible < Coercible
      def initialize(name, klass, default, strict=true, &validator_proc)
        @element_class = klass
        super(name, ::Array, default, strict, &validator_proc)
      end

      def coerce(value)
        Array(value)
      end

      protected
      def validate(input)
        if !input.is_a?(@klass)
          raise ArgumentError.new("Setting \"#{@name}\" must be a #{@klass}. Received: #{input} (#{input.class})")
        end

        unless input.all? {|el| el.kind_of?(@element_class) }
          raise ArgumentError.new("Values of setting \"#{@name}\" must be #{@element_class}. Received: #{input.map(&:class)}")
        end

        if @validator_proc && !@validator_proc.call(input)
          raise ArgumentError.new("Failed to validate setting \"#{@name}\" with value: #{input}")
        end
      end
    end
  end


  SETTINGS = Settings.new
end
