class Vernier::Output::Firefox::Thread

def allocations_table

def allocations_table
  return nil if !@allocations
  samples, weights, timestamps = @allocations.values_at(:samples, :weights, :timestamps)
  return nil if samples.size == 0
  size = samples.size
  timestamps = timestamps.map { _1 / 1_000_000.0 }
  ret = {
    "time": timestamps,
    "className": ["Object"]*size,
    "typeName": ["JSObject"]*size,
    "coarseType": ["Object"]*size,
    "weight": weights,
    "inNursery": [false] * size,
    "stack": samples,
    "length": size
  }
  ret
end

def data

def data
  started_at = (@started_at - 0) / 1_000_000.0
  stopped_at = (@stopped_at - 0) / 1_000_000.0 if @stopped_at
  {
    name: @name,
    isMainThread: @is_main,
    processStartupTime: started_at,
    processShutdownTime: stopped_at,
    registerTime: started_at,
    unregisterTime: stopped_at,
    pausedRanges: [],
    pid: profile.pid || Process.pid,
    tid: @tid,
    frameTable: frame_table,
    funcTable: func_table,
    nativeSymbols: {},
    samples: samples_table,
    jsAllocations: allocations_table,
    stackTable: stack_table,
    resourceTable: {
      length: 0,
      lib: [],
      name: [],
      host: [],
      type: []
    },
    markers: markers_table,
    stringArray: string_table
  }.compact
end

def filter_filenames(filenames)

def filter_filenames(filenames)
  filter = FilenameFilter.new
  filenames.map do |filename|
    filter.call(filename)
  end
end

def frame_table

def frame_table
  funcs = @stack_table_hash[:frame_table].fetch(:func)
  lines = @stack_table_hash[:frame_table].fetch(:line)
  raise unless lines.size == funcs.size
  size = funcs.size
  none = [nil] * size
  default = [0] * size
  unidentified = [-1] * size
  categories = @frame_categories.map(&:idx)
  subcategories = @frame_subcategories
  {
    address: unidentified,
    inlineDepth: default,
    category: categories,
    subcategory: subcategories,
    func: funcs,
    nativeSymbol: none,
    innerWindowID: none,
    implementation: @frame_implementations,
    line: lines,
    column: none,
    length: size
  }
end

def func_table

def func_table
  size = @func_names.size
  cfunc_idx = @strings["<cfunc>"]
  is_js = @filenames.map { |fn| fn != cfunc_idx }
  line_numbers = @stack_table_hash[:func_table].fetch(:first_line).map.with_index do |line, i|
    if is_js[i] || line != 0
      line
    else
      nil
    end
  end
  {
    name: @func_names,
    isJS: is_js,
    relevantForJS: is_js,
    resource: [-1] * size, # set to unidentified for now
    fileName: @filenames,
    lineNumber: line_numbers,
    columnNumber: [nil] * size,
    #columnNumber: functions.map { _1.column },
    length: size
  }
end

def gc_category

def gc_category
  @categorizer.get_category("GC")
end

def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil)

def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil)
  @ruby_thread_id = ruby_thread_id
  @profile = profile
  @categorizer = categorizer
  @tid = tid
  @name = name
  @is_main = is_main
  if is_main.nil?
    @is_main = @ruby_thread_id == ::Thread.main.object_id
  end
  @is_main = true if profile.threads.size == 1
  @is_start = is_start.nil? ? @is_main : is_start
  @stack_table = Vernier::StackTable.new
  samples = samples.map { |sample| @stack_table.convert(profile._stack_table, sample) }
  @samples = samples
  if allocations
    allocation_samples = allocations[:samples].dup
    allocation_samples.map! do |sample|
      @stack_table.convert(profile._stack_table, sample)
    end
    allocations = allocations.merge(samples: allocation_samples)
  end
  @allocations = allocations
  timestamps ||= [0] * samples.size
  @weights, @timestamps = weights, timestamps
  @sample_categories = sample_categories || ([0] * samples.size)
  @markers = markers.map do |marker|
    if stack_idx = marker[5]&.dig(:cause, :stack)
      marker = marker.dup
      new_idx = @stack_table.convert(profile._stack_table, stack_idx)
      marker[5] = marker[5].merge({ cause: { stack: new_idx }})
    end
    marker
  end
  @started_at, @stopped_at = started_at, stopped_at
  @stack_table_hash = @stack_table.to_h
  names = @stack_table_hash[:func_table].fetch(:name)
  filenames = @stack_table_hash[:func_table].fetch(:filename)
  stacks_size = @stack_table.stack_count
  @categorized_stacks = Hash.new do |h, k|
    h[k] = h.size + stacks_size
  end
  @strings = Hash.new { |h, k| h[k] = h.size }
  @func_names = names.map do |name|
    @strings[name]
  end
  @filenames = filter_filenames(filenames).map do |filename|
    @strings[filename]
  end
  func_implementations = filenames.map do |filename|
    # Must match strings in `src/profile-logic/profile-data.js`
    # inside the firefox profiler. See `getFriendlyStackTypeName`
    if filename == "<cfunc>"
      @strings["native"]
    else
      # nil means interpreter
      nil
    end
  end
  @frame_implementations = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx|
    func_implementations[func_idx]
  end
  cfunc_category = @categorizer.get_category("cfunc")
  ruby_category = @categorizer.get_category("Ruby")
  func_categories, func_subcategories = [], []
  filenames.each do |filename|
    if filename == "<cfunc>"
      func_categories << cfunc_category
      func_subcategories << 0
    else
      func_categories << ruby_category
      subcategory = ruby_category.subcategories.detect {|c| c.matches?(filename) }&.idx || 0
      func_subcategories << subcategory
    end
  end
  @frame_categories = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx|
    func_categories[func_idx]
  end
  @frame_subcategories = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx|
    func_subcategories[func_idx]
  end
end

def markers_table

def markers_table
  string_indexes = []
  start_times = []
  end_times = []
  phases = []
  categories = []
  data = []
  @markers.each_with_index do |(_, name, start, finish, phase, datum), i|
    string_indexes << @strings[name]
    start_times << (start / 1_000_000.0)
    # Please don't hate me. Divide by 1,000,000 only if finish is not nil
    end_times << (finish&./(1_000_000.0))
    phases << phase
    category =
      if name.start_with?("GC")
        gc_category.idx
      elsif name.start_with?("Thread")
        thread_category.idx
      else
        0
      end
    categories << category
    data << datum
  end
  {
    data: data,
    name: string_indexes,
    startTime: start_times,
    endTime: end_times,
    phase: phases,
    category: categories,
    length: start_times.size
  }
end

def samples_table

def samples_table
  samples = @samples
  weights = @weights
  categories = @sample_categories
  size = samples.size
  if categories.empty?
    categories = [0] * size
  end
  if @timestamps
    times = @timestamps.map { _1 / 1_000_000.0 }
  else
    # FIXME: record timestamps for memory samples
    times = (0...size).to_a
  end
  raise unless weights.size == size
  raise unless times.size == size
  samples = samples.zip(categories).map do |sample, category|
    if category == 0
      sample
    else
      @categorized_stacks[[sample, category]]
    end
  end
  {
    stack: samples,
    time: times,
    weight: weights,
    weightType: profile.meta[:mode] == :retained ? "bytes" : "samples",
    length: samples.length
  }
end

def stack_table

def stack_table
  frames =   @stack_table_hash[:stack_table].fetch(:frame).dup
  prefixes = @stack_table_hash[:stack_table].fetch(:parent).dup
  categories  = frames.map{|idx| @frame_categories[idx].idx }
  subcategories  = frames.map{|idx| @frame_subcategories[idx] }
  @categorized_stacks.each_key do |(stack, category)|
    frames << frames[stack]
    prefixes << prefixes[stack]
    categories << category
    subcategories << 0
  end
  size = frames.length
  raise unless frames.size == size
  raise unless prefixes.size == size
  {
    frame: frames,
    category: categories,
    subcategory: subcategories,
    prefix: prefixes,
    length: prefixes.length
  }
end

def string_table

def string_table
  @strings.keys.map do |string|
    if string.ascii_only?
      string
    elsif string.encoding == Encoding::UTF_8
      if string.valid_encoding?
        string
      else
        string.scrub
      end
    elsif string.encoding == Encoding::BINARY
      # TODO: We might want to guess UTF-8 and escape the binary more explicitly
      string.dup.force_encoding("UTF-8").scrub
    else
      # TODO: ideally we should attempt to properly re-encode here, but right now I think this is dead code
      string.dup.force_encoding("UTF-8").scrub
    end
  end
end

def thread_category

def thread_category
  @categorizer.get_category("Thread")
end