module Culinaryrobot
  module Internal # :nodoc: all
    # Rudimentary tracing and visualization utilities to iron out concurrency
    # issues with ffi in cruby
    module Waterfall
      # Convenience class to gather set_trace_func data, divided up by
      # thread.
      #
      #   t = Tracer.new
      #   t.trace do
      #     # code using threads
      #   end
      #   t.events # -> [trace for thread 1, thread 2, ...]
      #
      class Tracer
        attr_reader :events

        def initialize
          @thread_ids = {}
          @thread_hash_mutex = Mutex.new
          @events = []
        end

        # Run the given block, tracing the method calls it makes
        def trace
          set_trace_func method(:trace_func).to_proc
          yield
          self
        ensure
          set_trace_func nil
        end

        private

        def trace_func(event, _file, _line, id, _binding, classname)
          time = Time.now.to_f
          return if event == "line"
          tid = thread_id(Thread.current)
          @events[tid] << [time, event, classname, id]
        end

        # Map a Thread object to a integer thread ID, starting at 0 for the
        # first Thread object seen.
        def thread_id(thread)
          @thread_ids[thread] || @thread_hash_mutex.synchronize do
            index = @thread_ids.size
            @events[index] = []
            @thread_ids[thread] = index
          end
        end
      end

      # Coalesces raw thread traces from Tracer, matching call/return method
      # pairs.
      class ThreadTrace
        attr_reader :coalesced

        def initialize(events)
          @events = events
          compute
        end

        def start_time
          @events[0][0]
        end

        def max_depth
          @max_depth ||= @coalesced.map { |e| e[2] } .max
        end

        private

        # Sets #coalesced to [[start_time, duration, depth, class, method], ...]
        def compute
          @coalesced = []

          stack = []
          @events.each do |e|
            time, what, cls, meth = e

            next if meth == :set_trace_func

            case what
            when "c-call", "call"
              stack.push e
            when "return", "c-return"
              expected = [
                  case what
                  when "return"
                    "call"
                  when "c-return"
                    "c-call"
                  end,
                  cls, meth]

              if stack.empty?
                raise "Stack empty, no match for #{e}"
              end
              top = stack.pop

              if top[1..-1] != expected
                raise "Expected #{expected} for #{e}, got #{top}"
              end

              top_time = top[0]
              @coalesced << [top_time, time - top_time, stack.size,
                  cls, meth]
            else
              raise "Unknown event #{what.inspect} in #{e}"
            end
          end
          unless stack.empty?
            raise "Stack not empty at end of trace! Remaining: #{stack}"
          end
          @coalesced.sort!
        end
      end

      # HTML output for traces gathered by Tracer. This isn’t anything fancy,
      # or even really readable; it was just enough to diagnose a serious
      # performance problem.
      class TraceFormatter
        def initialize(events)
          @traces = events.map { |e| ThreadTrace.new(e) }
        end

        def scale(x)
          # FIXME: scale by total time trace
          ret = (x * 5000).to_i
          if ret < 10
            ret = 10
          end
          ret
        end

        def to_html
          global_start_time = @traces.map(&:start_time).min

          pieces = []

          @traces.each do |trace|
            pieces << "<div style='position: relative;" \
            " border: 1px solid green;" \
            " height: #{(trace.max_depth + 1) * 24}px'>"

            trace.coalesced.each do |item|
              start, duration, depth, cls, meth = item

              l = scale(start - global_start_time)
              w = scale(duration)
              h = 24
              t = 24 * depth

              pieces << "<div style='background-color: #DDD;" \
            " position: absolute;" \
            " top: #{t}px; left: #{l}px; width: #{w}px; height: #{h}px"\
            "'>#{cls}::#{meth}</div>"
            end

            pieces << "</div>"
          end
          pieces.join("\n")
        end
      end
    end
  end
end

# Test driver
if __FILE__ == $PROGRAM_NAME
  include Culinaryrobot::Internal::Waterfall

  # Dummy class
  class Foo
    def a
      b
    end

    def b; end
  end

  events = Tracer.new.trace do
    1.upto(2).map do
      Thread.new { Foo.new.a }
    end.map(&:join)
  end.events

  html = TraceFormatter.new(events).to_html
  puts html
  IO.write('test.html', html)
end
