module L2meter
  class Emitter
    attr_reader :configuration

    def initialize(configuration: Configuration.new)
      @configuration = configuration
      @start_times = []
      @contexts = []
      @outputs = []
    end

    def log(*args)
      params = transform_log_args(*args)
      params = merge_contexts(params)

      if block_given?
        wrap params, &proc
      else
        write params
      end
    end

    def with_elapsed
      @start_times << Time.now
      yield
    ensure
      @start_times.pop
    end

    def silence
      silence!
      yield
    ensure
      unsilence!
    end

    def silence!
      @outputs.push NullObject.new
    end

    def unsilence!
      @outputs.pop
    end

    def measure(metric, value, unit: nil)
      log_with_prefix :measure, metric, value, unit: unit
    end

    def sample(metric, value, unit: nil)
      log_with_prefix :sample, metric, value, unit: unit
    end

    def count(metric, value=1)
      log_with_prefix :count, metric, value
    end

    def unique(metric, value)
      log_with_prefix :unique, metric, value
    end

    def context(hash_or_proc)
      @contexts.push hash_or_proc
      yield
    ensure
      @contexts.pop
    end

    def clone
      self.class.new(configuration: configuration)
    end

    private

    def transform_log_args(*args)
      params = Hash === args.last ? args.pop : {}
      args = args.compact.map { |key| [ key, true ] }.to_h
      args.merge(params)
    end

    def merge_contexts(params)
      params = current_context.merge(params)
      source = configuration.source
      params = { source: source }.merge(params) if source

      if start_time = @start_times.last
        elapsed = Time.now - start_time
        params = merge_elapsed(elapsed, params)
      end

      params
    end

    def merge_elapsed(elapsed, params)
      params.merge(elapsed: "%.4fs" % elapsed)
    end

    def current_context
      contexts_queue.inject({}) do |result, c|
        current = c.respond_to?(:call) ? c.call.to_h : c.clone
        result.merge(current)
      end.to_a.reverse.to_h
    end

    def format_value(value)
      configuration.value_formatter.call(value)
    end

    def format_key(key)
      configuration.key_formatter.call(key)
    end

    def format_keys(params)
      params.inject({}) do |normalized, (key, value)|
        normalized.tap { |n| n[format_key(key)] = value }
      end
    end

    def write(params)
      tokens = format_keys(params).map do |key, value|
        value == true ? key : [ key, format_value(value) ] * ?=
      end

      tokens.sort! if configuration.sort?

      output_queue.last.print tokens.join(" ") + "\n"
    end

    def log_with_prefix(method, key, value, unit: nil)
      key = [configuration.prefix, key, unit].compact * ?.
      log Hash["#{method}##{key}", value]
    end

    def wrap(params)
      write params.merge(at: :start)

      result, exception, elapsed = execute_with_elapsed(&proc)

      if exception
        status = { at: :exception, exception: exception.class.name, message: exception.message.strip }
      else
        status = { at: :finish }
      end

      status = merge_elapsed(elapsed, status)

      write params.merge(status)

      raise exception if exception

      result
    end

    def execute_with_elapsed
      time_at_start = Time.now
      [ yield, nil, Time.now - time_at_start ]
    rescue Exception => exception
      [ nil, exception, Time.now - time_at_start ]
    end

    def contexts_queue
      [ configuration.context, *@contexts ].compact
    end

    def output_queue
      [ configuration.output, *@outputs ].compact
    end
  end
end