# thread safe logger
module Dunlop::NestedLogger

  class << self

    def formatter=(formatter)
      Thread.current[:nested_logger_formatter] = formatter
    end

    def options
      @options ||= {
        yaml_options: {}
      }
    end

    def formatter
      Thread.current[:nested_logger_formatter] ||= begin
        proc do |severity, datetime, progname, msg|
          "#{severity == :warn ? 'WARNING' : severity.upcase} - #{msg}"
        end
      end
    end

    def reset_formatter
      Thread.current[:nested_logger_formatter] = nil
    end

    def benchmark
      Thread.current[:nested_logger_benchmark] = true
      yield
    ensure
      Thread.current[:nested_logger_benchmark] = false
    end

    def benchmark?
      Thread.current[:nested_logger_benchmark] == true
    end

    def scope(label, options={})
      scope_stack << Scope.new(label)
      r0 = Time.now
      return_value = yield
      duration = Time.now - r0
      info("duration = #{duration}s") if options.fetch(:benchmark, benchmark?)

      if parent_scope?
        closing_scope = scope_stack.pop
        current_scope.add_scope(closing_scope) unless closing_scope.empty?
      end
      return_value
    end

    def scope_stack
      Thread.current[:nested_logger_scope_stack] ||= [BaseScope.new]
    end

    def empty_scope_stack
      Thread.current[:nested_logger_scope_stack] = nil
    end
    alias_method :reset, :empty_scope_stack

    def current_scope
      scope_stack.last
    end

    def parent_scope?
      scope_stack.size > 1
    end

    def merge_threads(threads)
      threads.map do |thread|
        if top_thread_scope = Array.wrap(thread[:nested_logger_scope_stack]).pop
          current_scope.entries += top_thread_scope.entries
        end
      end
    end

    def copy_scope_stack_for_merge
      Marshal.load(Marshal.dump(scope_stack))
    end

    def merge_scope_stack(stack)
      current_scope.entries += stack.pop.entries
    end

    def flush
      current_scope.pop_entries.map(&:to_entry) || []
    end

    def flush_as_yaml
      (f = flush.presence) ? YAML.dump(f, Dunlop::NestedLogger.options[:yaml_options]).sub("---\n", '') : ''
    end

    def log(options={})
      current_scope.log(options) if current_scope
    end

    [:debug, :info, :warn, :error].each do |severity|
      define_method(severity) do |msg|
        current_scope.log(msg: msg, severity: severity) if current_scope
      end
    end
  end

  class Scope
    attr_accessor :label, :entries
    def initialize(label, entries=[])
      @label = label.to_s
      @entries = entries
    end

    def empty?
      entries.empty?
    end

    def log(options={})
      entries << Entry.new(options)
    end

    def add_scope(scope)
      entries << scope
    end

    def to_entry
      { label => entries.map(&:to_entry).compact } unless empty?
    end

    def pop_entries
      entries.tap { self.entries = [] }
    end
  end

  class BaseScope < Scope
    def initialize
      @entries = []
    end
    def to_entry
      entries.map(&:to_entry).compact unless empty?
    end
  end

  class Entry
    attr_reader :severity, :datetime, :progname, :msg

    def initialize(options)
      @severity = options[:severity]
      @datetime = options[:datetime] || Time.current
      @progname = options[:progname]
      @msg = options[:msg]
    end

    def to_entry
      Dunlop::NestedLogger.formatter.call(severity, datetime, progname, msg)
    end
  end

end

