#!/usr/bin/ruby.ruby3.4 
# frozen_string_literal: true

require "getoptlong"
require "ostruct"
require "histogram/array"
require "innodb"

class String
  def squish!
    gsub!(/[[:space:]]+/, " ")
    strip!
    self
  end

  def squish
    dup.squish!
  end
end

# Convert a floating point RGB array into an ANSI color number approximating it.
def rgb_to_ansi(rgb)
  rgb_n = rgb.map { |c| (c * 5.0).round }
  16 + (rgb_n[0] * 36) + (rgb_n[1] * 6) + rgb_n[2]
end

# Interpolate intermediate float-arrays between two float-arrays. Do not
# include the points a and b in the result.
def interpolate(ary_a, ary_b, count)
  deltas = ary_a.each_index.map { |i| ary_b[i] - ary_a[i] }
  steps = ary_a.each_index.map { |i| deltas[i].to_f / (count.to_f + 1) }

  count.times.to_a.map { |i| ary_a.each_index.map { |j| ary_a[j] + ((i + 1).to_f * steps[j]) } }
end

# Interpolate intermediate float-arrays between each step in a sequence of
# float-arrays. Include each step in the sequence.
def interpolate_sequence(sequence, count)
  result = []
  result << sequence.first
  (sequence.size - 1).times.map { |n| [sequence[n], sequence[n + 1]] }.each do |from, to|
    interpolate(from, to, count).each do |step|
      result << step
    end
    result << to
  end

  result
end

# The RGB values of the typical heatmap progression.
HEATMAP_PROGRESSION = [
  [0.0, 0.0, 0.0], # Black
  [0.0, 0.0, 1.0], # Blue
  [0.0, 1.0, 1.0], # Cyan
  [0.0, 1.0, 0.0], # Green
  [1.0, 1.0, 0.0], # Yellow
  [1.0, 0.0, 0.0], # Red
  [1.0, 0.0, 1.0], # Purple
].freeze

# Typical heatmap color progression.
ANSI_COLORS_HEATMAP = interpolate_sequence(HEATMAP_PROGRESSION, 6).map { |rgb| rgb_to_ansi(rgb) }

# The 24-step grayscale progression.
ANSI_COLORS_GRAYSCALE = (0xe8..0xff).to_a

# Return the text supplied with ANSI 256-color coloring applied.
def ansi_color(color, text)
  "\x1b[38;5;#{color}m#{text}\x1b[0m"
end

# Zero and 1/8 through 8/8 illustrations.
BLOCK_CHARS_V = "░▁▂▃▄▅▆▇█".chars.freeze
BLOCK_CHARS_H = "░▏▎▍▌▋▊▉█".chars.freeze

# A reasonably large prime number to multiply identifiers by in order to
# space out the colors used for similar identifiers.
COLOR_SPACING_PRIME = 999_983

# Return a string with a possibly colored filled block for use in printing
# to an ANSI-capable Unicode-enabled terminal.
def filled_block(fraction, identifier = nil, block_chars = BLOCK_CHARS_V)
  if fraction.zero?
    block = block_chars[0]
  else
    parts = (fraction.to_f * (block_chars.size.to_f - 1)).floor
    block = block_chars[[block_chars.size - 1, parts + 1].min]
  end
  if identifier
    # ANSI 256-color mode, color palette starts at 10 and contains 216 colors.
    color = 16 + ((identifier * COLOR_SPACING_PRIME) % 216)
    ansi_color(color, block)
  else
    block
  end
end

def center(text, width)
  return text if text.size >= width

  spaces = (width - text.size) / 2
  (" " * spaces) + text + (" " * spaces)
end

# Print metadata about each list in an array of InnoDB::List objects.
def print_lists(lists)
  puts "%-20s%-12s%-12s%-12s%-12s%-12s" % %w[
    name
    length
    f_page
    f_offset
    l_page
    l_offset
  ]

  lists.each do |name, list|
    puts "%-20s%-12i%-12i%-12i%-12i%-12i" % [
      name,
      list.base[:length],
      list.base[:first] && list.base[:first][:page]   || 0,
      list.base[:first] && list.base[:first][:offset] || 0,
      list.base[:last]  && list.base[:last][:page]    || 0,
      list.base[:last]  && list.base[:last][:offset]  || 0,
    ]
  end
end

# Print a page usage bitmap for each extent descriptor in an array of
# Innodb::XdesEntry objects.
def print_xdes_list(space, list)
  puts "%-12s%-64s" % %w[
    start_page
    page_used_bitmap
  ]

  list.each do |entry|
    display = entry.each_page_status.inject("") do |bitmap, (page_number, page_status)|
      if page_number < space.pages
        bitmap += page_status[:free] ? "." : "#"
      end
      bitmap
    end
    puts "%-12i%-64s" % [entry.xdes[:start_page], display]
  end
end

# Print a summary of page usage for all pages in an index.
def print_index_page_summary(pages)
  puts "%-12s%-8s%-8s%-8s%-8s%-8s" % %w[
    page
    index
    level
    data
    free
    records
  ]

  pages.each do |page_number, page|
    case page.type
    when :INDEX
      puts "%-12i%-8i%-8i%-8i%-8i%-8i" % [
        page_number,
        page.page_header[:index_id],
        page.level,
        page.record_space,
        page.free_space,
        page.records,
      ]
    when :ALLOCATED
      puts "%-12i%-8i%-8i%-8i%-8i%-8i" % [page_number, 0, 0, 0, page.size, 0]
    end
  end
end

# Print a summary of all spaces in the InnoDB system.
def system_spaces(innodb_system)
  puts "%-32s%-12s%-12s" % %w[
    name
    pages
    indexes
  ]

  print_space_information = lambda do |name, space|
    puts "%-32s%-12i%-12i" % [
      name,
      space.pages,
      space.each_index.to_a.size,
    ]
  end

  print_space_information.call("(system)", innodb_system.system_space)

  innodb_system.each_table_name do |table_name|
    space = innodb_system.space_by_table_name(table_name)
    next unless space

    print_space_information.call(table_name, space)
  end

  innodb_system.each_orphan do |table_name|
    puts "%-43s (orphan/tmp)" % table_name
  end
end

# Print the contents of the SYS_TABLES data dictionary table.
def data_dictionary_tables(innodb_system)
  puts "%-32s%-12s%-12s%-12s%-12s%-12s%-15s%-12s" % %w[
    name
    id
    n_cols
    type
    mix_id
    mix_len
    cluster_name
    space
  ]

  innodb_system.data_dictionary.each_table do |record|
    puts "%-32s%-12i%-12i%-12i%-12i%-12i%-15s%-12i" % [
      record["NAME"],
      record["ID"],
      record["N_COLS"],
      record["TYPE"],
      record["MIX_ID"],
      record["MIX_LEN"],
      record["CLUSTER_NAME"],
      record["SPACE"],
    ]
  end
end

# Print the contents of the SYS_COLUMNS data dictionary table.
def data_dictionary_columns(innodb_system)
  puts "%-12s%-6s%-32s%-12s%-12s%-6s%-6s" % %w[
    table_id
    pos
    name
    mtype
    prtype
    len
    prec
  ]

  innodb_system.data_dictionary.each_column do |record|
    puts "%-12i%-6i%-32s%-12i%-12i%-6i%-6i" % [
      record["TABLE_ID"],
      record["POS"],
      record["NAME"],
      record["MTYPE"],
      record["PRTYPE"],
      record["LEN"],
      record["PREC"],
    ]
  end
end

# Print the contents of the SYS_INDEXES data dictionary table.
def data_dictionary_indexes(innodb_system)
  puts "%-12s%-12s%-32s%-10s%-6s%-12s%-12s" % %w[
    table_id
    id
    name
    n_fields
    type
    space
    page_no
  ]

  innodb_system.data_dictionary.each_index do |record|
    puts "%-12i%-12i%-32s%-10i%-6i%-12i%-12i" % [
      record["TABLE_ID"],
      record["ID"],
      record["NAME"],
      record["N_FIELDS"],
      record["TYPE"],
      record["SPACE"],
      record["PAGE_NO"],
    ]
  end
end

# Print the contents of the SYS_FIELDS data dictionary table.
def data_dictionary_fields(innodb_system)
  puts "%-12s%-12s%-32s" % %w[
    index_id
    pos
    col_name
  ]

  innodb_system.data_dictionary.each_field do |record|
    puts "%-12i%-12i%-32s" % [
      record["INDEX_ID"],
      record["POS"],
      record["COL_NAME"],
    ]
  end
end

def space_summary(space, start_page)
  puts "%-12s%-20s%-12s%-12s%-20s" % %w[
    page
    type
    prev
    next
    lsn
  ]

  space.each_page(start_page) do |page_number, page|
    puts "%-12i%-20s%-12i%-12i%-20i" % [
      page_number,
      page.type,
      page.prev || 0,
      page.next || 0,
      page.lsn  || 0,
    ]
  end
end

def space_index_pages_summary(space, start_page)
  print_index_page_summary(space.each_page(start_page))
end

def space_index_fseg_pages_summary(space, fseg_id)
  print_index_page_summary(space.inode(fseg_id).each_page)
end

def space_page_type_regions(space, start_page)
  puts "%-12s%-12s%-12s%-20s" % %w[
    start
    end
    count
    type
  ]

  space.each_page_type_region(start_page) do |region|
    puts "%-12i%-12i%-12i%-20s" % [
      region[:start],
      region[:end],
      region[:count],
      region[:type],
    ]
  end
end

def space_page_type_summary(space, start_page)
  # Count of pages encountered; Shouldn't be space.pages since we may skip
  # some pages due to the start_page parameter.
  page_count = 0
  # A Hash of page type => count.
  page_type = Hash.new(0)
  space.each_page(start_page) do |_page_number, page|
    page_count += 1
    page_type[page.type] += 1
  end

  puts "%-20s%-12s%-12s%-20s" % %w[
    type
    count
    percent
    description
  ]

  # Sort the page type Hash by count, descending.
  page_type.sort { |a, b| b[1] <=> a[1] }.each do |type, type_count|
    puts "%-20s%-12i%-12.2f%-20s" % [
      type,
      type_count,
      100.0 * (type_count.to_f / page_count),
      Innodb::Page::PAGE_TYPE[type][:description],
    ]
  end
end

def space_lists(space)
  print_lists(space.page(0).each_list)
end

def space_list_iterate(space, list_name)
  fsp = space.page(0).fsp_header

  raise "List '#{list_name}' doesn't exist" unless fsp[list_name].is_a?(Innodb::List)

  case fsp[list_name]
  when Innodb::List::Xdes
    print_xdes_list(space, fsp[list_name])
  when Innodb::List::Inode
    puts "%-12s" % %w[
      page
    ]
    fsp[list_name].each do |page|
      puts "%-12i" % [
        page.offset,
      ]
    end
  end
end

def space_indexes(innodb_system, space)
  puts "%-12s%-32s%-12s%-12s%-12s%-12s%-12s%-12s" % %w[
    id
    name
    root
    fseg
    fseg_id
    used
    allocated
    fill_factor
  ]

  space.each_index do |index|
    index.each_fseg do |fseg_name, fseg|
      puts "%-12i%-32s%-12i%-12s%-12i%-12i%-12i%-12s" % [
        index.id,
        innodb_system ? innodb_system.index_name_by_id(index.id) : "",
        index.root.offset,
        fseg_name,
        fseg.fseg_id,
        fseg.used_pages,
        fseg.total_pages,
        "%.2f%%" % fseg.fill_factor,
      ]
    end
  end
end

def space_index_pages_free_plot(space, start_page)
  raise "Could not load gnuplot. Is it installed?" unless require "gnuplot"

  index_data = { 0 => { x: [], y: [] } }

  space.each_page(start_page) do |page_number, page|
    case page.type
    when :INDEX
      data = (index_data[page.page_header[:index_id]] ||= { x: [], y: [] })
      data[:x] << page_number
      data[:y] << page.free_space
    when :ALLOCATED
      index_data[0][:x] << page_number
      index_data[0][:y] << page.size
    end
  end

  image_name = space.name.sub(".ibd", "").gsub(/[^a-zA-Z0-9_]/, "_").sub(/\A_+/, "")
  image_file = "#{image_name}_free.png"

  # Aim for one horizontal pixel per extent, but min 1k and max 10k width.
  image_width = [10_000, [1_000, space.pages / space.pages_per_extent].max].min

  Gnuplot.open do |gp|
    Gnuplot::Plot.new(gp) do |plot|
      plot.terminal "png size #{image_width}, 800"
      plot.output image_file
      plot.title image_name.gsub("_", " ")
      plot.key "reverse left top box horizontal Left textcolor variable"
      plot.ylabel "free space per page"
      plot.xlabel "page number"
      plot.yrange "[-100:18000]"
      plot.xtics "border"

      index_data.sort.each do |id, data|
        plot.data << Gnuplot::DataSet.new([data[:x], data[:y]]) do |ds|
          ds.with = "dots"
          ds.title = id.zero? ? "Unallocated" : "Index #{id}"
        end
      end

      puts "Wrote #{image_file}"
    end
  end
end

def space_extents(space)
  print_xdes_list(space, space.each_xdes)
end

# rubocop:disable Metrics/BlockNesting
def space_extents_illustrate_page_status(space, entry, count_by_identifier, identifiers)
  entry.each_page_status.each_with_object("".dup) do |(page_number, page_status), bitmap|
    if page_number >= space.pages
      bitmap << " "
      next
    end

    used_fraction = 1.0
    identifier = nil
    if page_status[:free]
      used_fraction = 0.0
    else
      page = space.page(page_number)
      used_fraction = page.used_space.to_f / page.size if page.respond_to?(:used_space)
      if page.respond_to?(:index_id)
        identifier = page.index_id
        unless identifiers[identifier]
          identifiers[identifier] = page.ibuf_index? ? "Insert Buffer Index" : "Index #{page.index_id}"
          if space.innodb_system
            table, index = space.innodb_system.table_and_index_name_by_id(page.index_id)
            identifiers[identifier] += " (%s.%s)" % [table, index] if table && index
          end
        end
      end
    end

    bitmap << filled_block(used_fraction, identifier)

    if used_fraction.zero?
      count_by_identifier[:free] += 1
    else
      count_by_identifier[identifier] += 1
    end
  end
end
# rubocop:enable Metrics/BlockNesting

# Illustrate the space by printing each extent and for each page, printing a
# filled block colored based on the index the page is part of. Print a legend
# for the colors used afterwards.
def space_extents_illustrate(space)
  width = space.pages_per_extent
  puts
  puts "%12s  %-#{width}s " % ["", center(space.name, width)]
  puts "%12s ╭%-#{width}s╮" % ["Start Page", "─" * width]

  identifiers = {}
  count_by_identifier = Hash.new(0)

  space.each_xdes do |entry|
    puts "%12i │%-#{width}s│" % [
      entry.xdes[:start_page],
      space_extents_illustrate_page_status(space, entry, count_by_identifier, identifiers),
    ]
  end
  total_pages = count_by_identifier.values.reduce(:+)

  puts "%12s ╰%-#{width}s╯" % ["", "─" * width]

  puts
  puts "Legend (%s = 1 page):" % [filled_block(1.0, nil)]
  puts "  %-62s %8s %8s" % [
    "Page Type",
    "Pages",
    "Ratio",
  ]
  puts "  %s %-60s %8i %7.2f%%" % [
    filled_block(1.0, nil),
    "System",
    count_by_identifier[nil],
    100.0 * (count_by_identifier[nil].to_f / total_pages),
  ]
  identifiers.sort.each do |identifier, description|
    puts "  %s %-60s %8i %7.2f%%" % [
      filled_block(1.0, identifier),
      description,
      count_by_identifier[identifier],
      100.0 * (count_by_identifier[identifier].to_f / total_pages),
    ]
  end
  puts "  %s %-60s %8i %7.2f%%" % [
    filled_block(0.0, nil),
    "Free space",
    count_by_identifier[:free],
    100.0 * (count_by_identifier[:free].to_f / total_pages),
  ]
  puts
end

def space_lsn_age_illustrate(space)
  colors = ANSI_COLORS_HEATMAP
  width = @options.illustration_line_width

  # Calculate the minimum and maximum LSN in the space. This is pretty
  # inefficient as we end up scanning all pages twice.
  page_lsn = Array.new(space.pages)

  lsn_min = lsn_max = space.page(0).lsn
  space.each_page do |page_number, page|
    next if page.lsn.zero?

    page_lsn[page_number] = page.lsn
    lsn_min = page.lsn < lsn_min ? page.lsn : lsn_min
    lsn_max = page.lsn > lsn_max ? page.lsn : lsn_max
  end
  lsn_delta = lsn_max - lsn_min

  puts
  puts "%12s  %-#{width}s " % ["", center(space.name, width)]
  puts "%12s ╭%-#{width}s╮" % ["Start Page", "─" * width]

  start_page = 0
  page_lsn.each_slice(width) do |slice|
    puts "%12i │%-#{width}s│" % [
      start_page,
      slice.inject("") do |line, lsn|
        if lsn
          age_ratio = (lsn - lsn_min).to_f / lsn_delta
          color = colors[(age_ratio * colors.size.to_f).floor]
          line += ansi_color(color, filled_block(1.0, nil))
        else
          line += " "
        end
        line
      end,
    ]
    start_page += width
  end

  puts "%12s ╰%-#{width}s╯" % ["", "─" * width]

  _, lsn_freq = page_lsn.reject(&:nil?).histogram(colors.size, min: lsn_min, max: lsn_max)
  lsn_freq_delta = lsn_freq.max - lsn_freq.min

  lsn_age_histogram = "".dup
  lsn_freq.each do |freq|
    freq_norm = freq / lsn_freq_delta
    lsn_age_histogram << (freq_norm > 0.0 ? filled_block(freq_norm) : " ")
  end

  puts
  puts "LSN Age Histogram (%s = ~%d pages):" % [
    filled_block(1.0, nil),
    (space.pages.to_f / colors.size).round,
  ]
  puts "  %12s %s %-12s" % [
    "Min LSN",
    lsn_age_histogram,
    "Max LSN",
  ]
  puts "  %12i %s %-12i" % [
    lsn_min,
    colors.map { |c| ansi_color(c, filled_block(1.0, nil)) }.join,
    lsn_max,
  ]
end

def print_inode_summary(inode)
  puts "INODE fseg_id=%d, pages=%d, frag=%d, full=%d, not_full=%d, free=%d" % [
    inode.fseg_id,
    inode.total_pages,
    inode.frag_array_n_used,
    inode.full.length,
    inode.not_full.length,
    inode.free.length,
  ]
end

def print_inode_detail(inode)
  # rubocop:disable Layout/LineLength
  puts "INODE fseg_id=%d, pages=%d, frag=%d pages (%s), full=%d extents (%s), not_full=%d extents (%s) (%d/%d pages used), free=%d extents (%s)" % [
    inode.fseg_id,
    inode.total_pages,
    inode.frag_array_n_used,
    inode.frag_array_pages.join(", "),
    inode.full.length,
    inode.full.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(", "),
    inode.not_full.length,
    inode.not_full.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(", "),
    inode.not_full_n_used,
    inode.not_full.length * inode.space.pages_per_extent,
    inode.free.length,
    inode.free.each.to_a.map { |x| "#{x.start_page}-#{x.end_page}" }.join(", "),
  ]
  # rubocop:enable Layout/LineLength
end

def space_inodes_fseg_id(space)
  space.each_inode do |inode|
    puts inode.fseg_id
  end
end

def space_inodes_summary(space)
  space.each_inode do |inode|
    print_inode_summary(inode)
  end
end

def space_inodes_detail(space)
  space.each_inode do |inode|
    print_inode_detail(inode)
  end
end

def page_account(innodb_system, space, page_number)
  puts "Accounting for page #{page_number}:"

  if page_number > space.pages
    puts "  Page does not exist."
    return
  end

  page = space.page(page_number)
  page_type = Innodb::Page::PAGE_TYPE[page.type]
  puts "  Page type is %s (%s, %s)." % [
    page.type,
    page_type[:description],
    page_type[:usage],
  ]

  xdes = space.xdes_for_page(page_number)
  puts "  Extent descriptor for pages %d-%d is at page %d, offset %d." % [
    xdes.start_page,
    xdes.end_page,
    xdes.this[:page],
    xdes.this[:offset],
  ]

  if xdes.allocated_to_fseg?
    puts "  Extent is fully allocated to fseg #{xdes.fseg_id}."
  else
    puts "  Extent is not fully allocated to an fseg; may be a fragment extent."
  end

  xdes_status = xdes.page_status(page_number)
  puts "  Page is marked as %s in extent descriptor." % [
    xdes_status[:free] ? "free" : "used",
  ]

  space.each_xdes_list do |name, list|
    puts "  Extent is in #{name} list of space." if list.include?(xdes)
  end

  page_inode = nil
  space.each_inode do |inode|
    inode.each_list do |name, list|
      if list.include?(xdes)
        page_inode = inode
        puts "  Extent is in #{name} list of fseg #{inode.fseg_id}."
      end
    end

    if inode.frag_array.include?(page_number) # rubocop:disable Style/Next
      page_inode = inode
      puts "  Page is in fragment array of fseg %d." % [
        inode.fseg_id,
      ]
    end
  end

  space.each_index do |index|
    index.each_fseg do |fseg_name, fseg|
      next unless page_inode == fseg

      puts "  Fseg is in #{fseg_name} fseg of index #{index.id}."
      puts "  Index root is page #{index.root.offset}."
      if innodb_system
        table_name, index_name = innodb_system.table_and_index_name_by_id(index.id)
        puts "  Index is #{table_name}.#{index_name}." if table_name && index_name
      end
    end
  end

  if space.system_space? # rubocop:disable Style/GuardClause
    puts "  Fseg is trx_sys." if page_inode == space.trx_sys.fseg
    puts "  Fseg is doublewrite buffer." if page_inode == space.trx_sys.doublewrite[:fseg]

    innodb_system.data_dictionary&.each_data_dictionary_index do |table_name, index_name, index|
      index.each_fseg do |_fseg_name, fseg|
        puts "  Index is #{table_name}.#{index_name} of data dictionary." if page_inode == fseg
      end
    end

    space.trx_sys.rsegs.each_with_index do |rseg_slot, index|
      if page.fil_header.space_id == rseg_slot.space_id && page.fil_header.offset == rseg_slot.page_number
        puts "  Page is a rollback segment in slot #{index}."
      end
    end
  end
end

def page_validate_index(page)
  page_is_valid = true

  print "Parsing all records in page... "
  records = page.each_record.to_a
  puts "done."

  directory_offsets = page.each_directory_offset.to_a
  record_offsets = records.map(&:offset)

  invalid_directory_entries = directory_offsets.reject { |n| record_offsets.include?(n) }

  unless invalid_directory_entries.empty?
    page_is_valid = false
    puts "Invalid page directory entries (offsets not to valid records):"
    invalid_directory_entries.each do |offset|
      puts "  slot %d, offset %d" % [
        page.offset_is_directory_slot?(offset),
        offset,
      ]
    end
  end

  # Read all records corresponding to valid directory entries.
  directory_records = directory_offsets.reject { |o| invalid_directory_entries.include?(o) }.map { |o| page.record(o) }

  misordered_directory_entries = []
  prev = nil
  directory_records.each do |rec|
    unless prev
      prev = rec
      next
    end
    if rec.compare_key(prev.key.map { |v| v[:value] }) == 1
      page_is_valid = false
      misordered_directory_entries << {
        slot: page.offset_is_directory_slot?(rec.offset),
        offset: rec.offset,
        key: rec.key_string,
        prev_key: prev.key_string,
      }
    end
    prev = rec
  end
  unless misordered_directory_entries.empty?
    puts "Misordered page directory entries (key < prev key):"
    misordered_directory_entries.each do |entry|
      puts "  slot %d, offset %d, key %s, prev key %s" % [
        entry[:slot],
        entry[:offset],
        entry[:key],
        entry[:prev_key],
      ]
    end
  end

  misordered_records = []
  prev = nil
  page.each_record do |rec|
    unless prev
      prev = rec
      next
    end
    if rec.compare_key(prev.key.map { |v| v[:value] }) == 1
      page_is_valid = false
      misordered_records << {
        offset: rec.offset,
        key: rec.key_string,
        prev_key: prev.key_string,
      }
    end
    prev = rec
  end
  unless misordered_records.empty?
    puts "Misordered records in record list (key < prev key):"
    misordered_records.each do |entry|
      puts "  offset %d, key %s, prev key %s" % [
        entry[:offset],
        entry[:key],
        entry[:prev_key],
      ]
    end
  end

  page_is_valid
end

def page_validate(_innodb_system, space, page_number)
  page_is_valid = true
  puts "Validating page %d..." % [page_number]

  print "Parsing page... "
  page = space.page(page_number)
  puts "done."

  if page.corrupt?
    page_is_valid = false
    puts "Page appears to be corrupt:"
    puts "  Stored checksums:"
    puts "    header  %10d (0x%08x), type %s" % [
      page.checksum,
      page.checksum,
      page.checksum_type || "unknown",
    ]
    puts "    trailer %10d (0x%08x)" % [
      page.fil_trailer.checksum,
      page.fil_trailer.checksum,
    ]
    puts "  Calculated checksums:"
    puts "    crc32  %10d (0x%08x)" % [
      page.checksum_crc32,
      page.checksum_crc32,
    ]
    puts "    innodb %10d (0x%08x)" % [
      page.checksum_innodb,
      page.checksum_innodb,
    ]
  end

  if page.torn?
    page_is_valid = false
    puts "Page appears to be torn:"
    puts "  Full LSN:"
    puts "    header  %d (0x%016x)" % [
      page.lsn,
      page.lsn,
    ]
    puts "  Low 32 bits of LSN:"
    puts "    header  %10d (0x%08x)" % [
      page.fil_header.lsn_low32,
      page.fil_header.lsn_low32,
    ]
    puts "    trailer %10d (0x%08x)" % [
      page.fil_trailer.lsn_low32,
      page.fil_trailer.lsn_low32,
    ]
  end

  if page.misplaced?
    page_is_valid = false
    puts "Page appears to be misplaced:"
    if page.misplaced_offset?
      puts "  Requested page %d but offset stored in page is %d." % [
        page_number,
        page.offset,
      ]
    end
    if page.misplaced_space?
      puts "  Space ID %d does not match page stored space ID %d." % [
        page.space.space_id,
        page.space_id,
      ]
    end
  end

  page_is_valid = false if page.type == :INDEX && !page_validate_index(page)

  puts "Page %d appears to be %s!" % [
    page_number,
    page_is_valid ? "valid" : "corrupted",
  ]
end

def page_directory_summary(_space, page)
  usage(1, "Page must be an index page") if page.type != :INDEX

  puts "%-8s%-8s%-14s%-8s%s" % %w[
    slot
    offset
    type
    owned
    key
  ]

  page.directory.each_with_index do |offset, slot|
    record = page.record(offset)
    key = %i[conventional node_pointer].include?(record.header[:type]) ? "(%s)" % record.key_string : ""

    puts "%-8i%-8i%-14s%-8i%s" % [
      slot,
      offset,
      record.header[:type],
      record.header[:n_owned],
      key,
    ]
  end
end

def page_records(_space, page)
  page.each_record do |record|
    puts "Record %i: %s" % [
      record.offset,
      record.string,
    ]
  end
end

def page_illustrate(page)
  width = 64
  unknown_page_content = page.type == :INDEX && page.record_describer.nil?
  blocks = Array.new(page.size, unknown_page_content ? "▞" : " ")
  identifiers = {}
  identifier_sort = 0
  count_by_identifier = Hash.new(0)

  page.each_region.sort_by(&:offset).each do |region|
    region.length.times do |n|
      identifier = nil
      fraction = 0.0
      if region.name != :garbage
        fraction = n == region.length - 1 ? 0.5 : 1.0
        identifier = region.name.hash.abs
        unless identifiers[identifier]
          # Prefix an integer <0123> on each name so that the legend can be
          # sorted by the appearance of each region in the page.
          identifiers[identifier] = "<%04i>%s" % [
            identifier_sort,
            region.info,
          ]
          identifier_sort += 1
        end
      end
      blocks[region.offset + n] = filled_block(fraction, identifier, BLOCK_CHARS_H)
      count_by_identifier[identifier] += 1
    end
  end

  puts
  puts "%12s  %-#{width}s " % ["", center("Page #{page.offset} (#{page.type})", width)]
  puts "%12s ╭%-#{width}s╮" % ["Offset", "─" * width]
  offset = 0
  skipped_lines = 0
  blocks.each_slice(width) do |slice|
    if slice.any? { |s| s != " " }
      if skipped_lines.positive?
        puts "%12s │%-#{width}s│" % ["...", ""]
        skipped_lines = 0
      end
      puts "%12i │%-s│" % [offset, slice.join]
    else
      skipped_lines += 1
    end
    offset += width
  end
  puts "%12s ╰%-#{width}s╯" % ["", "─" * width]

  puts
  puts "Legend (%s = 1 byte):" % [filled_block(1.0, nil)]
  puts "  %-32s %8s %8s" % [
    "Region Type",
    "Bytes",
    "Ratio",
  ]
  identifiers.sort { |a, b| a[1] <=> b[1] }.each do |identifier, description|
    puts "  %s %-30s %8i %7.2f%%" % [
      filled_block(1.0, identifier),
      description.gsub(/^<\d+>/, ""),
      count_by_identifier[identifier],
      100.0 * (count_by_identifier[identifier].to_f / page.size),
    ]
  end
  puts "  %s %-30s %8i %7.2f%%" % [
    filled_block(0.0, nil),
    "Garbage",
    count_by_identifier[nil],
    100.0 * (count_by_identifier[nil].to_f / page.size),
  ]
  free_space = page.size - count_by_identifier.inject(0) { |sum, (_k, v)| sum + v }
  puts "  %s %-30s %8i %7.2f%%" % [
    unknown_page_content ? "▞" : " ",
    unknown_page_content ? "Unknown (no data dictionary)" : "Free",
    free_space,
    100.0 * (free_space.to_f / page.size),
  ]

  if unknown_page_content
    puts
    puts "Note:"
    puts "  Records could not be parsed because no data dictionary or record describer"
    puts "  was available. Use -s instead of -f, or provide a record describer class."
  end

  puts
end

def record_dump(page, record_offset)
  record = page.record(record_offset)
  raise "Record at offset #{record_offset} not found" unless record

  record.dump
rescue IOError
  raise "Record could not be read at offset #{record_offset}; is it a valid record offset?"
end

def record_history(page, record_offset)
  raise "Record is not located on a leaf page; no history available" unless page.leaf?

  record = page.record(record_offset)
  raise "Record at offset #{record_offset} not found" unless record

  puts "%-14s%-20s%s" % [
    "Transaction",
    "Type",
    "Undo record",
  ]

  record.each_undo_record do |undo|
    puts "%-14s%-20s%s" % [
      undo.trx_id || "(n/a)",
      undo.header[:type],
      undo.string,
    ]
  end
end

def index_fseg_lists(index, fseg_name)
  raise "File segment '#{fseg_name}' doesn't exist" unless index.fseg(fseg_name)

  print_lists(index.each_fseg_list(index.fseg(fseg_name)))
end

def index_fseg_list_iterate(index, fseg_name, list_name)
  fseg = index.fseg(fseg_name)
  raise "File segment '#{fseg_name}' doesn't exist" unless fseg

  list = fseg.list(list_name)
  raise "List '#{list_name}' doesn't exist" unless list

  print_xdes_list(index.space, list)
end

def index_fseg_frag_pages(index, fseg_name)
  raise "File segment '#{fseg_name}' doesn't exist" unless index.fseg(fseg_name)

  print_index_page_summary(index.each_fseg_frag_page(index.fseg(fseg_name)))
end

def index_recurse(index)
  index.recurse(
    lambda do |page, depth|
      puts "%s%s NODE #%i: %i records, %i bytes" % [
        "  " * depth,
        index.node_type(page).to_s.upcase,
        page.offset,
        page.records,
        page.record_space,
      ]
      if page.level.zero?
        page.each_record do |record|
          puts "%sRECORD: (%s) → (%s)" % [
            "  " * (depth + 1),
            record.key_string,
            record.row_string,
          ]
        end
      end
    end,
    lambda do |_parent_page, child_page, child_min_key, depth|
      puts "%sNODE POINTER RECORD ≥ (%s) → #%i" % [
        "  " * depth,
        child_min_key.map { |r| "%s=%s" % [r[:name], r[:value].inspect] }.join(", "),
        child_page.offset,
      ]
    end
  )
end

def index_record_offsets(index)
  puts "%-20s%-20s" % %w[
    page_offset
    record_offset
  ]
  index.recurse(
    lambda do |page, _depth|
      if page.level.zero?
        page.each_record do |record|
          puts "%-20i%-20i" % [
            page.offset,
            record.offset,
          ]
        end
      end
    end,
    ->(*_) {}
  )
end

def index_digraph(index)
  puts "digraph btree {"
  puts "  rankdir = LR;"
  puts "  ranksep = 2.0;"
  index.recurse(
    lambda do |page, depth|
      label = "<page>Page %i|(%i records)" % [
        page.offset,
        page.records,
      ]
      page.each_child_page do |child_page_number, child_key|
        label += "|<dir_%i>(%s)" % [
          child_page_number,
          child_key.join(", "),
        ]
      end
      puts "  %spage_%i [ shape = 'record'; label = '%s'; ];" % [
        "  " * depth,
        page.offset,
        label,
      ]
    end,
    lambda do |parent_page, child_page, _child_key, depth|
      puts "  %spage_%i:dir_%i → page_%i:page:nw;" % [
        "  " * depth,
        parent_page.offset,
        child_page.offset,
        child_page.offset,
      ]
    end
  )
  puts "}"
end

def index_level_summary(index, level)
  puts "%-8s%-8s%-8s%-8s%-8s%-8s%-8s" % %w[
    page
    index
    level
    data
    free
    records
    min_key
  ]

  index.each_page_at_level(level) do |page|
    puts "%-8i%-8i%-8i%-8i%-8i%-8i%s" % [
      page.offset,
      page.page_header[:index_id],
      page.level,
      page.record_space,
      page.free_space,
      page.records,
      page.min_record.key_string,
    ]
  end
end

def undo_history_summary(innodb_system)
  history_list = innodb_system.history.each_history_list.reject { |h| h.list.empty? }

  puts "%-8s%-8s%-14s%-20s%s" % %w[
    Page
    Offset
    Transaction
    Type
    Table
  ]

  history_list.each do |history|
    history.each_undo_record do |undo|
      table_name = innodb_system.table_name_by_id(undo.table_id)
      puts "%-8s%-8s%-14s%-20s%s" % [
        undo.page,
        undo.offset,
        undo.trx_id,
        undo.type,
        table_name,
      ]
    end
  end
end

def undo_record_dump(innodb_system, page, record_offset)
  undo_record = Innodb::UndoRecord.new(page, record_offset)
  index = innodb_system.clustered_index_by_table_id(undo_record.table_id)
  undo_record.index_page = index.root
  undo_record.new_subordinate(page, record_offset).dump
end

def usage(exit_code, message = nil)
  if message
    puts "Error: #{message}; see --help for usage information\n\n"
    exit exit_code
  end

  # rubocop:disable Layout/HeredocIndentation
  print <<'END_OF_USAGE'

Usage: innodb_space <options> <mode>

Invocation examples:

  innodb_space -s ibdata1 [-T table-name [-I index-name [-R record-offset]]] [options] <mode>
    Use ibdata1 as the system tablespace and load the table-name table (and
    the index-name index for modes that require it) from data located in the
    system tablespace data dictionary. This will automatically generate a
    record describer for any indexes using the data dictionary.

  innodb_space -f file-name.ibd [-r ./describer.rb -d DescriberClass] [options] <mode>
    Use the file-name.ibd tablespace file (and the DescriberClass describer
    where required) to read the tablespace structures or indexes.

The following options are supported:

  --help, -?
    Print this usage text.

  --trace, -t
    Enable tracing of all data read. Specify twice to enable even more
    tracing (including reads during opening of the tablespace) which can
    be quite noisy.

  --system-space-file, -s <arg>
    Load the system tablespace file or files <arg>: Either a single file e.g.
    'ibdata1', a comma-delimited list of files e.g. 'ibdata1,ibdata1', or a
    directory name. If a directory name is provided, it will be scanned for all
    files named 'ibdata?' which will then be sorted alphabetically and used to
    load the system tablespace.

  If using the --system-space-file option, the following options may also
  be used:

    --table-name, -T <name>
      Use the table name <name>.

    --index-name, -I <name>
      Use the index name <name>.

    --system-space-tables, -x
      Allow opening tables from the system space to support system spaces with
      tables created without innodb-file-per-table enabled.

    --data-directory, -D <directory>
      Open per-table tablespace files from <directory> rather than from the
      directory where the system-space-file is located.

  --space-file, -f <file>
    Load the tablespace file <file>.

  --page, -p <page>
    Operate on the page <page>.

  --record, -R <offset>
    Operate on the record located at <offset> within the index page.

  --level, -l <level>
    Operate on the level <level>.

  --list, -L <list>
    Operate on the list <list>.

  --fseg-id, -F <fseg_id>
      Operate on the file segment (fseg) <fseg_id>.

  --require, -r <file>
    Use Ruby's 'require' to load the file <file>. This is useful for loading
    classes with record describers.

  --describer, -d <describer>
    Use the named record describer to parse records in index pages.

The following modes are supported:

  system-spaces
    Print a summary of all spaces in the system.

  data-dictionary-tables
    Print all records in the SYS_TABLES data dictionary table.

  data-dictionary-columns
    Print all records in the SYS_COLUMNS data dictionary table.

  data-dictionary-indexes
    Print all records in the SYS_INDEXES data dictionary table.

  data-dictionary-fields
    Print all records in the SYS_FIELDS data dictionary table.

  space-summary
    Summarize all pages within a tablespace. A starting page number can be
    provided with the --page/-p argument.

  space-index-pages-summary
    Summarize all 'INDEX' pages within a tablespace. This is useful to analyze
    page fill rates and record counts per page. In addition to 'INDEX' pages,
    'ALLOCATED' pages are also printed and assumed to be completely empty.
    A starting page number can be provided with the --page/-p argument.

  space-index-fseg-pages-summary
    The same as space-index-pages-summary but only iterate one fseg, provided
    with the --fseg-id/-F argument.

  space-index-pages-free-plot
    Use Ruby's gnuplot module to produce a scatterplot of page free space for
    all 'INDEX' and 'ALLOCATED' pages in a tablespace. More aesthetically
    pleasing plots can be produced with space-index-pages-summary output,
    but this is a quick and easy way to produce a passable plot. A starting
    page number can be provided with the --page/-p argument.

  space-page-type-regions
    Summarize all contiguous regions of the same page type. This is useful to
    provide an overall view of the space and allocations within it. A starting
    page number can be provided with the --page/-p argument.

  space-page-type-summary
    Summarize all pages by type. A starting page number can be provided with
    the --page/-p argument.

  space-indexes
    Summarize all indexes (actually each segment of the indexes) to show
    the number of pages used and allocated, and the segment fill factor.

  space-lists
    Print a summary of all lists in a space.

  space-list-iterate
    Iterate through the contents of a space list.

  space-extents
    Iterate through all extents, printing the extent descriptor bitmap.

  space-extents-illustrate
    Iterate through all extents, illustrating the extent usage using ANSI
    color and Unicode box drawing characters to show page usage throughout
    the space.

  space-lsn-age-illustrate
    Iterate through all pages, producing a heat map colored by the page LSN
    using ANSI color and Unicode box drawing characters, allowing the user to
    get an overview of page modification recency.

  space-inodes-fseg-id
    Iterate through all inodes, printing only the FSEG ID.

  space-inodes-summary
    Iterate through all inodes, printing a short summary of each FSEG.

  space-inodes-detail
    Iterate through all inodes, printing a detailed report of each FSEG.

  index-recurse
    Recurse an index, starting at the root (which must be provided in the first
    --page/-p argument), printing the node pages, node pointers (links), leaf
    pages. A record describer must be provided with the --describer/-d argument
    to recurse indexes (in order to parse node pages).

  index-record-offsets
    Recurse an index as index-recurse does, but print the offsets of each
    record within the page.

  index-digraph
    Recurse an index as index-recurse does, but print a dot-compatible digraph
    instead of a human-readable summary.

  index-level-summary
    Print a summary of all pages at a given level (provided with the --level/-l
    argument) in an index.

  index-fseg-internal-lists
  index-fseg-leaf-lists
    Print a summary of all lists in an index file segment. Index root page must
    be provided with --page/-p.

  index-fseg-internal-list-iterate
  index-fseg-leaf-list-iterate
    Iterate the file segment list (whose name is provided in the first --list/-L
    argument) for internal or leaf pages for a given index (whose root page
    is provided in the first --page/-p argument). The lists used for each
    index are 'full', 'not_full', and 'free'.

  index-fseg-internal-frag-pages
  index-fseg-leaf-frag-pages
    Print a summary of all fragment pages in an index file segment. Index root
    page must be provided with --page/-p.

  page-dump
    Dump the contents of a page, using the Ruby pp ('pretty-print') module.

  page-account
    Account for a page's usage in FSEGs.

  page-validate
    Validate the contents of a page.

  page-directory-summary
    Summarize the record contents of the page directory in a page. If a record
    describer is available, the key of each record will be printed.

  page-records
    Summarize all records within a page.

  page-illustrate
    Produce an illustration of the contents of a page.

  record-dump
    Dump a detailed description of a record and the data it contains. A record
    offset must be provided with -R/--record.

  record-history
    Summarize the history (undo logs) for a record. A record offset must be
    provided with -R/--record.

  undo-history-summary
    Summarize all records in the history list (undo logs).

  undo-record-dump
    Dump a detailed description of an undo record and the data it contains.
    A record offset must be provided with -R/--record.

END_OF_USAGE
  # rubocop:enable Layout/HeredocIndentation

  exit exit_code
end

%w[INT PIPE].each do |name|
  Signal.trap(name) { exit } if Signal.list.include?(name)
end

@options = OpenStruct.new
@options.trace                    = 0
@options.system_space_file        = nil
@options.system_space_tables      = false
@options.data_directory           = nil
@options.space_file               = nil
@options.table_name               = nil
@options.index_name               = nil
@options.page                     = nil
@options.record                   = nil
@options.level                    = nil
@options.list                     = nil
@options.fseg_id                  = nil
@options.describer                = nil
@options.illustration_line_width  = 64
@options.illustration_block_size  = 8

# rubocop:disable Layout/SpaceInsideArrayLiteralBrackets
getopt_options = [
  [ "--help",                     "-?",     GetoptLong::NO_ARGUMENT ],
  [ "--trace",                    "-t",     GetoptLong::NO_ARGUMENT ],
  [ "--system-space-file",        "-s",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--system-space-tables",      "-x",     GetoptLong::NO_ARGUMENT ],
  [ "--data-directory",           "-D",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--space-file",               "-f",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--table-name",               "-T",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--index-name",               "-I",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--page",                     "-p",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--record",                   "-R",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--level",                    "-l",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--list",                     "-L",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--fseg-id",                  "-F",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--require",                  "-r",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--describer",                "-d",     GetoptLong::REQUIRED_ARGUMENT ],
  [ "--illustration-line-width",            GetoptLong::REQUIRED_ARGUMENT ],
  [ "--illustration-block-size",            GetoptLong::REQUIRED_ARGUMENT ],
]
# rubocop:enable Layout/SpaceInsideArrayLiteralBrackets

getopt = GetoptLong.new(*getopt_options)

getopt.each do |opt, arg|
  case opt
  when "--help"
    usage 0
  when "--trace"
    @options.trace += 1
  when "--system-space-file"
    @options.system_space_file = arg.split(",")
  when "--system-space-tables"
    @options.system_space_tables = true
  when "--data-directory"
    @options.data_directory = arg
  when "--space-file"
    @options.space_file = arg.split(",")
  when "--table-name"
    @options.table_name = arg
  when "--index-name"
    @options.index_name = arg
  when "--page"
    @options.page = arg.to_i
  when "--record"
    @options.record = arg.to_i
  when "--level"
    @options.level = arg.to_i
  when "--list"
    @options.list = arg.to_sym
  when "--fseg-id"
    @options.fseg_id = arg.to_i
  when "--require"
    require File.expand_path(arg)
  when "--describer"
    @options.describer = arg
  when "--illustration-line-width"
    @options.illustration_line_width = arg.to_i
  when "--illustration-block-size"
    @options.illustration_block_size = arg.to_i
  end
end

# rubocop:disable Style/IfUnlessModifier

unless @options.system_space_file || @options.space_file
  usage(1, "Either the --system-space-file (-s) or --space-file (-f) must be specified")
end

if @options.system_space_file && @options.space_file
  usage(1, "Only one of --system-space-file (-s) or --space-file (-f) may be specified")
end

system_space_options =
  @options.table_name || @options.index_name || @options.system_space_tables || @options.data_directory

if !@options.system_space_file && system_space_options
  usage(
    1,
    %{
      The --table-name (-T), --index-name (-I), --system-space-tables (-x), and --data-directory (-D)
      options can only be used with the --system-space-file (-s) option
    }.squish
  )
end

BufferCursor.trace! if @options.trace > 1

# A few globals that we'll try to populate from the command-line arguments.
innodb_system = nil
space = nil
index = nil
page = nil

if @options.system_space_file
  innodb_system = Innodb::System.new(@options.system_space_file, data_directory: @options.data_directory)
end

if innodb_system && @options.table_name
  table_tablespace = innodb_system.space_by_table_name(@options.table_name)
  if table_tablespace
    space = table_tablespace
  elsif @options.system_space_tables
    space = innodb_system.system_space
  else
    raise "Tablespace file not found and --system-space-tables (-x) is not enabled"
  end
elsif @options.space_file
  space = Innodb::Space.new(@options.space_file)
else
  space = innodb_system.system_space
end

if @options.describer
  describer = eval(@options.describer) # rubocop:disable Security/Eval
  describer ||= Innodb::RecordDescriber.const_get(@options.describer)
  space.record_describer = describer.new
end

if innodb_system && @options.table_name && @options.index_name
  index = innodb_system.index_by_name(@options.table_name, @options.index_name)
  page = @options.page ? space.page(@options.page) : index.root
elsif @options.page
  page = space.page(@options.page)
  index = space.index(@options.page) if page&.type == :INDEX && page&.root?
end

# The non-option argument on the command line is the mode (usually the last,
# but not required).
mode = ARGV.shift

unless mode
  usage(1, "At least one mode must be provided")
end

if /^(system-|data-dictionary-)/.match(mode) && !innodb_system
  usage(1, "System tablespace must be specified using --system-space-file (-s)")
end

if /^space-/.match(mode) && !space
  usage(
    1,
    %{
      Tablespace must be specified using either --space-file (-f)
      or a combination of --system-space-file (-s) and --table (-T)
    }.squish
  )
end

if /^index-/.match(mode) && !index
  usage(
    1,
    %{
      Index must be specified using a combination of either --space-file (-f) and --page (-p)
      or --system-space-file (-s), --table-name (-T), and --index-name (-I)
    }.squish
  )
end

if /^page-/.match(mode) && !page
  usage(1, "Page number must be specified using --page (-p)")
end

if /^record-/.match(mode) && !@options.record
  usage(1, "Record offset must be specified using --record (-R)")
end

if /^record-/.match(mode) && !page
  usage(
    1,
    %{
      An index page must be available when using --record (-R); specify either
      --page (-p) or --table-name (-T) and --index-name (-I) for the index root page.
    }
  )
end

if /^record-/.match(mode) && page.type != :INDEX
  usage(1, "Mode #{mode} may be used only with index pages")
end

if /-list-iterate$/.match(mode) && !@options.list
  usage(1, "List name must be specified using --list (-L)")
end

if /-level-/.match(mode) && !@options.level
  usage(1, "Level must be specified using --level (-l)")
end

if %w[
  index-recurse
  index-record-offsets
  index-digraph
  index-level-summary
].include?(mode) && !index.record_describer
  usage(1, "Record describer must be specified using --describer (-d)")
end

if %w[
  space-index-fseg-pages-summary
].include?(mode) && !@options.fseg_id
  usage(1, "File segment id must be specified using --fseg-id (-F)")
end

# rubocop:enable Style/IfUnlessModifier

BufferCursor.trace! if @options.trace.positive?

case mode
when "system-spaces"
  system_spaces(innodb_system)
when "data-dictionary-tables"
  data_dictionary_tables(innodb_system)
when "data-dictionary-columns"
  data_dictionary_columns(innodb_system)
when "data-dictionary-indexes"
  data_dictionary_indexes(innodb_system)
when "data-dictionary-fields"
  data_dictionary_fields(innodb_system)
when "space-summary"
  space_summary(space, @options.page || 0)
when "space-index-pages-summary"
  space_index_pages_summary(space, @options.page || 0)
when "space-index-fseg-pages-summary"
  space_index_fseg_pages_summary(space, @options.fseg_id)
when "space-index-pages-free-plot"
  space_index_pages_free_plot(space, @options.page || 0)
when "space-page-type-regions"
  space_page_type_regions(space, @options.page || 0)
when "space-page-type-summary"
  space_page_type_summary(space, @options.page || 0)
when "space-lists"
  space_lists(space)
when "space-list-iterate"
  space_list_iterate(space, @options.list)
when "space-indexes"
  space_indexes(innodb_system, space)
when "space-extents"
  space_extents(space)
when "space-extents-illustrate"
  space_extents_illustrate(space)
when "space-lsn-age-illustrate"
  space_lsn_age_illustrate(space)
when "space-inodes-fseg-id"
  space_inodes_fseg_id(space)
when "space-inodes-summary"
  space_inodes_summary(space)
when "space-inodes-detail"
  space_inodes_detail(space)
when "index-recurse"
  index_recurse(index)
when "index-record-offsets"
  index_record_offsets(index)
when "index-digraph"
  index_digraph(index)
when "index-level-summary"
  index_level_summary(index, @options.level)
when "index-fseg-leaf-lists"
  index_fseg_lists(index, :leaf)
when "index-fseg-internal-lists"
  index_fseg_lists(index, :internal)
when "index-fseg-leaf-list-iterate"
  index_fseg_list_iterate(index, :leaf, @options.list)
when "index-fseg-internal-list-iterate"
  index_fseg_list_iterate(index, :internal, @options.list)
when "index-fseg-leaf-frag-pages"
  index_fseg_frag_pages(index, :leaf)
when "index-fseg-internal-frag-pages"
  index_fseg_frag_pages(index, :internal)
when "page-dump"
  page.dump
when "page-account"
  page_account(innodb_system, space, @options.page)
when "page-validate"
  page_validate(innodb_system, space, @options.page)
when "page-directory-summary"
  page_directory_summary(space, page)
when "page-records"
  page_records(space, page)
when "page-illustrate"
  page_illustrate(page)
when "record-dump"
  record_dump(page, @options.record)
when "record-history"
  record_history(page, @options.record)
when "undo-history-summary"
  undo_history_summary(innodb_system)
when "undo-record-dump"
  undo_record_dump(innodb_system, page, @options.record)
else
  usage 1, "Unknown mode: #{mode}"
end
