#ifdef DEBUG
/* xo-imgui/example/ex1/imgui_ex4a.cpp
 *
 * author: Roland Conybeare, Aug 2025
 */

#include "xo/object/Integer.hpp"
#include "xo/object/List.hpp"
#include "xo/alloc/GC.hpp"
#include "xo/alloc/Object.hpp"
#include "xo/randomgen/xoshiro256.hpp"
#include "xo/randomgen/random_seed.hpp"
#include "xo/object/Integer.hpp"
#include "xo/flatstring/flatstring.hpp"
#include "xo/indentlog/scope.hpp"

#include <SDL.h>
#include <GL/glew.h>
#include <cstddef>

#include "SDL_events.h"
#include "imgui.h"
#include "backends/imgui_impl_sdl2.h"
#include "backends/imgui_impl_opengl3.h"

//#include <GLFW/glfw3.h>

#include <iostream>
#include <fstream>
#include <algorithm>
#include <unistd.h>

using xo::gc::generation;

ImVec2 operator+(const ImVec2 & p1, const ImVec2 & p2) {
    return ImVec2(p1.x + p2.x, p1.y + p2.y);
}

struct ImRect {
    ImRect() = default;
    ImRect(const ImVec2 & tl, const ImVec2 & br) : top_left_{tl}, bottom_right_{br} {}
    ImRect(float x_lo, float y_lo, float x_hi, float y_hi) : top_left_{x_lo, y_lo}, bottom_right_{x_hi, y_hi} {}

    static ImRect from_xy_span(const ImVec2 & x_span, const ImVec2 & y_span) {
        return ImRect(ImVec2{x_span.x, y_span.x}, ImVec2{x_span.y, y_span.y});
    }

    std::pair<float, float> x_span() const { return std::make_pair(top_left_.x, bottom_right_.x); }
    std::pair<float, float> y_span() const { return std::make_pair(top_left_.y, bottom_right_.y); }

    const ImVec2 & top_left() const { return top_left_; }
    const ImVec2 & bottom_right() const { return bottom_right_; }

    float x_lo() const { return top_left_.x; }
    float x_hi() const { return bottom_right_.x; }
    float y_lo() const { return top_left_.y; }
    float y_hi() const { return bottom_right_.y; }

    float width() const { return bottom_right_.x - top_left_.x; }
    float height() const { return bottom_right_.y - top_left_.y; }

    float x_mid() const { return 0.5 * (top_left_.x + bottom_right_.x); }
    float y_mid() const { return 0.5 * (top_left_.y + bottom_right_.y); }

    ImVec2 bottom_left() const { return ImVec2(x_lo(), y_hi()); }
    ImVec2 top_right() const { return ImVec2(x_hi(), y_lo()); }

    ImRect with_x_span(float x0, float x1) const {
        return ImRect(ImVec2(x0, top_left_.y), ImVec2(x1, bottom_right_.y));
    }
    ImRect with_y_span(float y0, float y1) const {
        return ImRect(ImVec2(top_left_.x, y0), ImVec2(bottom_right_.x, y1));
    }

    ImRect within_margin(const ImRect & margin) const {
        return ImRect(this->x_lo() + margin.x_lo(),
                      this->y_lo() + margin.y_lo(),
                      this->x_hi() - margin.x_hi(),
                      this->y_hi() - margin.y_hi());
    }

    ImRect within_right_margin(float dx) const {
        return ImRect(this->x_lo(), this->y_lo(), this->x_hi() - dx, this->y_hi());
    }

    ImRect within_top_margin(float dy) const {
        return ImRect(this->x_lo(), this->y_lo() + dy, this->x_hi(), this->y_hi());
    }

    ImRect within_bottom_margin(float dy) const {
        return ImRect(this->x_lo(), this->y_lo(), this->x_hi(), this->y_hi() - dy);
    }

    ImRect translate(const ImVec2 & dist) {
        return ImRect(top_left_ + dist,
                      bottom_right_ + dist);
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect left_fraction(float p, float min_width = 0.0, float max_width = 999999.0) const {
        max_width = std::min(max_width, this->width());

        float w = std::clamp(p * this->width(), min_width, max_width);

        return ImRect(top_left_,
                      ImVec2(this->x_lo() + w, this->y_hi()));
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect right_fraction(float p, float min_width = 0.0, float max_width = 999999.0) const {
        max_width = std::min(max_width, this->width());

        float w = std::clamp(p * this->width(), min_width, max_width);

        return ImRect(ImVec2(this->x_hi() - w, this->y_lo()),
                      bottom_right_);
    }

    /** Require: 0.0 <= p <= q <= 1.0 **/
    ImRect mid_x_fraction(float p, float q) const {
        assert(p <= q);

        float w = this->width();

        return this->with_x_span(this->x_lo() + p * w,
                                 this->x_lo() + q * w);
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect top_fraction(float p, float min_height = 0.0, float max_height = 999999.0) const {
        max_height = std::min(max_height, this->height());

        float h = std::clamp(p * this->height(), min_height, max_height);

        return ImRect(top_left_,
                      ImVec2(this->x_hi(), this->y_lo() + h));
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect bottom_fraction(float p, float min_height = 0.0, float max_height = 999999.0) const {
        max_height = std::min(max_height, this->height());

        float h = std::clamp(p * this->height(), min_height, max_height);

        return ImRect(ImVec2(this->x_lo(), this->y_hi() - h),
                      bottom_right_);
    }

    ImVec2 top_left_{0, 0};
    ImVec2 bottom_right_{0, 0};
};

/* We need GUI to be able to fall behind true GC state, so we can animate transitions.
 * To help make this work, provide a model for GC state sufficient to drive rendering.
 */
struct GcGenerationDescription {
    GcGenerationDescription() = default;
    GcGenerationDescription(generation gen,
                            const char * name, const char * mnemonic, const char * gc_type,
                            std::uint8_t polarity,
                            std::size_t tospace_scale,
                            std::size_t before_ckp, std::size_t after_ckp,
                            std::size_t reserved, std::size_t committed, std::size_t gc_threshold)
        : name_{name}, mnemonic_{mnemonic}, gc_type_{gc_type}, polarity_{polarity},
          tospace_scale_{tospace_scale},
          before_checkpoint_{before_ckp}, after_checkpoint_{after_ckp},
          reserved_{reserved}, committed_{committed},
          gc_threshold_{gc_threshold} {}

    /** scale (in bytes) for drawing space **/
    std::size_t scale() const { return std::max(committed_, gc_threshold_); }

    /** nursery or tenured **/
    generation generation_;

    /** "nursery" or "tenured" **/
    const char * name_ = nullptr;

    /** "N" or "T" **/
    const char * mnemonic_ = nullptr;

    /** "incremental" or "full" **/
    const char * gc_type_ = nullptr;

    /** alternates between {0, 1} on each GC **/
    std::uint8_t polarity_ = 0;

    /** size of to-space in bytes represented on screen.
     *  (note however when we animate GC, space roles have already reversed,
     *   so then this will refer to old to-space = new from-space)
     **/
    std::size_t tospace_scale_ = 0;

    std::size_t before_checkpoint_ = 0;
    std::size_t after_checkpoint_ = 0;
    std::size_t reserved_ = 0;
    std::size_t committed_ = 0;
    // G_to_gc_threshold = G1_to_size + gc->config().incr_gc_threshold_;
    std::size_t gc_threshold_ = 0;
};

/* We need GUI to be able to fall behind true GC state, so we can animate transitions.
 * To help make this work, provide a model for GC state sufficient to drive rendering.
 */
struct GcStateDescription {
    using generation = xo::gc::generation;

    GcStateDescription(const GcGenerationDescription & nursery,
                       const GcGenerationDescription & tenured,
                       std::size_t gc_size,
                       std::size_t gc_committed,
                       std::size_t gc_allocated,
                       std::size_t gc_available,
                       std::size_t gc_mlog_size,
                       std::size_t total_promoted,
                       std::size_t total_n_mutation
        );

    const GcGenerationDescription & get_gendescr(generation g) const { return gen_state_v_[gen2int(g)]; }

    std::array<GcGenerationDescription, static_cast<std::size_t>(generation::N)> gen_state_v_;

    /** see @ref GC::size **/
    std::size_t gc_size_ = 0;
    /** see @ref GC::committed **/
    std::size_t gc_committed_ = 0;
    /** see @ref GC::allocated **/
    std::size_t gc_allocated_ = 0;
    /** see @ref GC::available **/
    std::size_t gc_available_ = 0;
    /** see @ref GC::mlog_size **/
    std::size_t gc_mlog_size_ = 0;

    /** see @ref GcStatistics::total_promoted_ **/
    std::size_t total_promoted_ = 0;
    /** see @ref GcStatistics::n_mutation_ **/
    std::size_t total_n_mutation_ = 0;
};

GcStateDescription::GcStateDescription(const GcGenerationDescription & nursery,
                                       const GcGenerationDescription & tenured,
                                       std::size_t gc_size,
                                       std::size_t gc_committed,
                                       std::size_t gc_allocated,
                                       std::size_t gc_available,
                                       std::size_t gc_mlog_size,
                                       std::size_t total_promoted,
                                       std::size_t total_n_mutation)
    : gc_size_{gc_size},
      gc_committed_{gc_committed},
      gc_allocated_{gc_allocated},
      gc_available_{gc_available},
      gc_mlog_size_{gc_mlog_size},
      total_promoted_{total_promoted},
      total_n_mutation_{total_n_mutation}
{
    gen_state_v_[gen2int(generation::nursery)] = nursery;
    gen_state_v_[gen2int(generation::tenured)] = tenured;
}

using xo::gp;
using xo::up;
using xo::Object;
using xo::obj::List;
using xo::obj::Integer;
using xo::rng::xoshiro256ss;
using xo::rng::Seed;

/** details of a single copy event performed by GC **/
struct GcCopyDetail {
    GcCopyDetail(std::size_t z,
                 generation src, std::size_t src_offset, std::size_t src_space_z,
                 generation dest, std::size_t dest_offset, std::size_t dest_z)
        : z_{z},
          src_gen_{src}, src_offset_{src_offset}, src_space_z_{src_space_z},
          dest_gen_{dest}, dest_offset_{dest_offset}, dest_z_{dest_z}
        {}

    /** object size in bytes **/
    std::size_t z_ = 0;
    /** source location **/
    generation src_gen_;
    /** offset from start of allocator **/
    std::size_t src_offset_ = 0;
    /** size of source space.  could store this separately **/
    std::size_t src_space_z_ = 0;

    /** destination location **/
    generation dest_gen_;
    /** offset from start of allocator **/
    std::size_t dest_offset_ = 0;
    /** size of destination space. (could store this separately). **/
    std::size_t dest_z_ = 0;
};

struct AppState {
    using GC = xo::gc::GC;

public:
    AppState();

    std::size_t nursery_tospace_scale() const;
    std::size_t tenured_tospace_scale() const;
    GcStateDescription snapshot_gc_state() const;

    void generate_random_mutation();
    void generate_random_mutations();

public:
    int alloc_per_cycle_ = 1;
    /** if gc triggered, remembers which whether incremental or full **/
    generation upto_ = generation::nursery;
    up<GC> gc_;
    std::size_t next_int_ = 0;
    std::size_t next_root_ = 0;
    std::vector<gp<Object>> gc_root_v_{100};
    Seed<xoshiro256ss> seed_;
    xoshiro256ss rng_{seed_};
    /** remember details for each object copied by GC, so we can animate **/
    std::vector<GcCopyDetail> copy_detail_v_;
    /** max offset for destination, given copied to nursery **/
    std::size_t copy_detail_max_nursery_dest_offset_ = 0;
    std::size_t copy_detail_nursery_dest_size_ = 0;
    std::size_t copy_detail_max_tenured_dest_offset_ = 0;
    std::size_t copy_detail_tenured_dest_size_ = 0;
};

AppState::AppState()
{
    this->gc_ = (GC::make
           (
               {.initial_nursery_z_ = 1024*1024,
                .initial_tenured_z_ = 1024*1024*1024,
                .incr_gc_threshold_ = 16*1024,
                .full_gc_threshold_ = 128*1024,
                .stats_flag_ = true,
                .debug_flag_ = false}));

    Object::mm = gc_.get();

    for (auto & x: gc_root_v_)
        gc_->add_gc_root(x.ptr_address());

    gc_->disable_gc();
}

std::size_t
AppState::nursery_tospace_scale() const {
    std::size_t N1_to_size = gc_->nursery_before_checkpoint();
    std::size_t N_to_committed = gc_->nursery_to_committed();
    std::size_t N_to_incr_gc_threshold = N1_to_size + gc_->config().incr_gc_threshold_;
    std::size_t N_to_scale = std::max(N_to_committed, N_to_incr_gc_threshold);

    return N_to_scale;
}

std::size_t
AppState::tenured_tospace_scale() const {
    std::size_t T1_to_size = gc_->tenured_before_checkpoint();
    std::size_t T_to_committed = gc_->tenured_to_committed();
    std::size_t T_to_full_gc_threshold = T1_to_size + gc_->config().full_gc_threshold_;
    std::size_t T_to_scale = std::max(T_to_committed, T_to_full_gc_threshold);

    return T_to_scale;
}

GcStateDescription
AppState::snapshot_gc_state() const {
    /** NOTE: this gets invoked before GC gets opportunity to run.
     *        in the event that GC does run, from- and to- spaces will
     *        have been reversed (near beginning of GC phase)
     *
     *        This means that nursery_to_reserved() etc. actually refer to from-space
     *        *during gc*
     **/

    // TOOD: may want to use GC::get_gc_statistics() to replace multiple round trips

    return GcStateDescription(GcGenerationDescription
                              (generation::nursery,
                               "nursery",
                               "N",
                               "incremental",
                               gc_->nursery_polarity(),
                               this->nursery_tospace_scale(),
                               gc_->nursery_before_checkpoint(),
                               gc_->nursery_after_checkpoint(),
                               gc_->nursery_to_reserved(),
                               gc_->nursery_to_committed(),
                               gc_->nursery_before_checkpoint() + gc_->config().incr_gc_threshold_),
                              GcGenerationDescription
                              (generation::tenured,
                               "tenured",
                               "T",
                               "full",
                               gc_->tenured_polarity(),
                               this->tenured_tospace_scale(),
                               gc_->tenured_before_checkpoint(),
                               gc_->tenured_after_checkpoint(),
                               gc_->tenured_to_reserved(),
                               gc_->tenured_to_committed(),
                               gc_->tenured_before_checkpoint() + gc_->config().full_gc_threshold_),

                              gc_->size(),
                              gc_->committed(),
                              gc_->allocated(),
                              gc_->available(),
                              gc_->mlog_size(),
                              gc_->native_gc_statistics().total_promoted_,
                              gc_->native_gc_statistics().n_mutation_
        );
}

void
AppState::generate_random_mutation() {
    if (rng_() % 1000 > (5 * 1000) / 7) {
        /* p=16% integer */
        gc_root_v_[next_root_++] = Integer::make(next_int_);
    } else if (rng_() % 1000 > (3 * 1000) / 7) {
        /* p=16% cons */
        gp<Object> random_car = gc_root_v_.at(rng_() % gc_root_v_.size());
        if (random_car.is_null())
            random_car = List::nil;

        /* this will always incorporate existing list as tail of new list */
        gp<List> random_cdr = List::from(gc_root_v_[next_root_]);
        if (random_cdr.is_null())
            random_cdr = List::nil;

        gp<List> random_cons = List::cons(random_car, random_cdr);

        gc_root_v_[next_root_++] = random_cons;
    } else if (rng_() % 1000 > (0 * 1000) / 7) {
        /* p=24% mutation */
        gp<List> random_list = List::from(gc_root_v_.at(rng_() % gc_root_v_.size()));
        if (!random_list.is_null()) {
            if (rng_() % 2 == 0) {
                /* pick up some random object, assign as head */
                gp<Object> random_car = gc_root_v_.at(rng_() % gc_root_v_.size());
                random_list->assign_head(random_car);
            } else {
                /* pick up some random object; if List, assign tail as tail */
                gp<List> random_cdr = List::from(gc_root_v_.at(rng_() % gc_root_v_.size()));
                if (!random_cdr.is_null() && !random_cdr->is_nil())
                    random_list->assign_rest(random_cdr->rest());
            }
        }
    }
    if (next_root_ >= gc_root_v_.size())
        this->next_root_ = 0;
}

void
AppState::generate_random_mutations() {
    for (int i = 0; i < this->alloc_per_cycle_; ++i) {
        this->generate_random_mutation();
    }
}

void
draw_filled_rect_with_label(const char * text,
                            const char * tooltip,
                            const ImRect & rect,
                            ImU32 fillcolor,
                            ImU32 textcolor,
                            ImDrawList * draw_list)
{
    draw_list->AddRectFilled(rect.top_left(),
                             rect.bottom_right(),
                             fillcolor);

    if ((rect.width() > 0.0) && (rect.height() > 0.0)) {
        ImGui::SetCursorScreenPos(rect.top_left());
        ImGui::InvisibleButton("ttbutton", ImVec2(rect.width(), rect.height()));
        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip("%s", tooltip);
        }
    }

    if (text) {
        auto textz = ImGui::CalcTextSize(text);

        /* N1 can be empty: but in that case don't bother to label it */
        if (textz.x < rect.width()) {
            draw_list->AddText(ImVec2(rect.x_mid() - 0.5 * textz.x,
                                      rect.y_mid() - 0.5 * textz.y),
                               textcolor,
                               text);
        }
    }
}

void
draw_filled_rect(const char * tooltip,
                 const ImRect & rect,
                 ImU32 fillcolor,
                 ImDrawList * draw_list)
{
    draw_filled_rect_with_label(nullptr,
                                tooltip,
                                rect,
                                fillcolor,
                                IM_COL32(255, 255, 255, 255),
                                draw_list);
}


using xo::gc::GC;
using xo::gc::GcStatisticsExt;
using xo::gc::GcStatisticsHistory;
using xo::gc::GcStatisticsHistoryItem;
using xo::xtag;
using xo::scope;
using xo::flatstring;
using std::size_t;

/** @class GenerationLayout
 *  @brief layout for displaying a single collector generation.
 *
 *  @text
 *       reserved: xxx committed: xxxx G1: xxx bytes G0: xxx bytes
 *       +--------------------------+--------------------+-------------------------+
 *    to |            G1            |         G0         |                         |
 *       +--------------------------+--------------------+-------------------------+
 *                                                                                   Mem: 28k
 *       +-------------------------------------------------------------------------+
 *  from |                                                                         |
 *       +-------------------------------------------------------------------------+
 *                                                                    ^
 *  layout elements:
 *
 *  <-a-><------------------------------ mem_w ------------------------------------><---b---->
 *       <------------------------- ngc_w ---------------------------->
 *       <--------- G1_w ----------> <------- G0_w ----->
 *
 *  a (lh_text_dx): width for left-hand-side text
 *  b (rh_text_dx): width for right-hand-side text
 *  mem_w:          width for contiguous committed memory
 *  ngc_w:          location (relative to start of GC memory range) of next-collection trigger
 *  G1_w:           width for occupied memory that has survived one GC in this space
 *  G0_w:           width for memory allocated since last GC
 *
 *  @endtext
 *
 **/
struct GenerationLayout {
    GenerationLayout() = default;
    GenerationLayout(const GcGenerationDescription & gendescr, const ImRect & br, bool with_labels);

    const char * name() const { return gendescr_.name_; }
    const char * mnemonic() const { return gendescr_.mnemonic_; }
    const char * gc_type() const { return gendescr_.gc_type_; }
    std::size_t to_G1_size() const { return gendescr_.before_checkpoint_; }
    std::size_t to_G0_size() const { return gendescr_.after_checkpoint_; }
    std::size_t to_gc_threhsold() const { return gendescr_.gc_threshold_; }
    float to_scale() const {
        /** note: deliberate size_t->float conversion here **/
        return gendescr_.scale();
    }
    ImRect to_g1_rect() const {
        return mem_rect_to_.left_fraction(this->to_G1_size() / this->to_scale());
    }
    ImRect to_g0_rect() const {
        return mem_rect_to_.mid_x_fraction(this->to_G1_size() / this->to_scale(),
                                           (this->to_G1_size() + this->to_G0_size()) / this->to_scale());
    }
    ImRect to_alloc_rect() const {
        return mem_rect_to_.left_fraction((this->to_G1_size() + this->to_G0_size()) / this->to_scale());
    }

    ImRect from_alloc_rect() const {
        /* use the same sizing as for source generation */
        return mem_rect_from_.left_fraction((this->to_G1_size() + this->to_G0_size()) / this->to_scale());
    }

    /** size-related statistics for generation to be displayed **/
    GcGenerationDescription gendescr_;

    /** bounding rectangle.  all drawing for generation display will be inside this rectanglge **/
    ImRect bounding_rect_;

    /** true iff text labels enabled **/
    bool with_labels_ = false;

    /** text height in screen units **/
    float text_dy_ = 0.0;

    /** chart rectangle.  bounding rectangle less room for headline text **/
    ImRect chart_withlabel_rect_;
    ImRect chart_nolabel_rect_;

    /** text for RH label.  something like "N: 28k/40k" **/
    flatstring<80> rh_text_;

    /** width of .rh_text in screen units **/
    float rh_text_dx_ = 0.0;

    /** rectangle representing from-space memory range **/
    ImRect mem_rect_from_;
    /** rectangle representing to-space memory range **/
    ImRect mem_rect_to_;
};

GenerationLayout::GenerationLayout(const GcGenerationDescription & gendescr,
                                   const ImRect & br,
                                   bool with_labels)
    : gendescr_{gendescr}, bounding_rect_{br}, with_labels_{with_labels}
{
    this->text_dy_ = ImGui::CalcTextSize("SAMPLE TEXT").y;

    if (with_labels_) {
        snprintf(this->rh_text_.data(), rh_text_.capacity(),
                 "%s: %luk",
                 gendescr_.mnemonic_,
                 std::max(gendescr_.gc_threshold_, gendescr_.committed_) / 1024);
        rh_text_.ensure_final_null();

        auto textz = ImGui::CalcTextSize(rh_text_.c_str());

        /* allow margin between rh edge of mem range and beginning of label */
        this->rh_text_dx_ = 5 + textz.x;
    } else {
        this->rh_text_dx_ = 0.0;
    }

    if (with_labels_) {
        this->chart_withlabel_rect_ = bounding_rect_.within_top_margin(text_dy_ + 2);
        this->chart_nolabel_rect_   = chart_withlabel_rect_.within_right_margin(rh_text_dx_);
    } else {
        this->chart_withlabel_rect_ = bounding_rect_;
        this->chart_nolabel_rect_ = bounding_rect_;
    }

    this->mem_rect_from_ = chart_nolabel_rect_.top_fraction(0.45);
    this->mem_rect_to_   = chart_nolabel_rect_.bottom_fraction(0.45);

    if (gendescr_.polarity_ == 1)
        std::swap(this->mem_rect_from_, this->mem_rect_to_);
}

/**
 *  @p polarity  0 -> draw from-space above to-space; 1 -> draw from-space below to-space
 *  @p p_x1    On exit *p_x1 contains x-coord of right-hand edge of rectangle
 *             depicting potential memory range
 **/
void
draw_generation(const GenerationLayout & layout,
                ImDrawList * draw_list)
{
    //scope log(XO_DEBUG(with_labels));

    using xo::gc::generation;

    /* next GC trigges when G0_to_size reaches this threshold */
    std::size_t G_to_gc_threshold = 0;

    G_to_gc_threshold = layout.gendescr_.gc_threshold_;

    std::size_t G_to_scale = layout.gendescr_.scale();

    /*
     * committed: G_to_committed
     * G1:  G1_to_size
     * G0:  G0_to_size
     * ckp: G1_to_size
     * ngc: G_to_gc_threshold
     *
     *   <----------------------------- committed --------------------------->
     *   <------------------ used ------------------> <-------- free -------->
     *   <------- G1 --------> <-------- G0 -------->
     *  |NNNNNNNNNNNNNNNNNNNNN|nnnnnnnnnnnnnnnnnnnnnn|________________________|
     *                        ^                                      ^
     *                        ckp                                    ngc
     *
     *  in screen coords:
     *
     *  horizontally:
     *
     *         rect.x_lo                                                                   rect.x_hi
     *         v                                                                           v
     *
     *          <--------------------------- display_w -----------------------------> <-+->
     *          <------------------------ ngc_w ---------------------->                 \- rh_text_dx
     *          <------- G1_w ------> <-------- G0_w ------>
     *         ^                     ^                      ^          ^             ^
     *        x0                    G0_x0                  G0_x1     ngc_w          x1
     *     *p_x0                                        *p_g0_x1                 *p_x1
     *
     *  vertically:
     *
     *      <- rect.y_lo
     *     ^
     *
     *     v
     *      <- rect.y_hi
     */

    /* e.g. N1: 34511 bytes */
    char g1_buf[255];

    ImU32 label_color = IM_COL32(255, 255, 192, 255); /*super pale yellow*/

    if (layout.with_labels_) {
        snprintf(g1_buf, sizeof(g1_buf),
                 "reserved: %lu bytes; committed: %lu bytes; %s\u2081: %lu bytes; %s\u2080: %lu bytes",
                 layout.gendescr_.reserved_,
                 layout.gendescr_.committed_,
                 layout.mnemonic(),
                 layout.gendescr_.before_checkpoint_,
                 layout.mnemonic(),
                 layout.gendescr_.after_checkpoint_);

        draw_list->AddText(layout.bounding_rect_.top_left(),
                           label_color,
                           g1_buf);
    }

    if (layout.with_labels_) {
        auto textz = ImGui::CalcTextSize(layout.rh_text_.c_str());

        draw_list->AddText(ImVec2(layout.chart_withlabel_rect_.x_hi() - textz.x,
                                  layout.chart_withlabel_rect_.y_mid() - 0.5 * textz.y),
                           label_color,
                           layout.rh_text_.c_str());
    }

    ImU32 outline_color = IM_COL32(255, 255, 255, 255); /*white*/

    /* chart rectangle */
    draw_list->AddRect(layout.mem_rect_from_.top_left(),
                       layout.mem_rect_from_.bottom_right(),
                       outline_color);

    draw_list->AddRect(layout.mem_rect_to_.top_left(),
                       layout.mem_rect_to_.bottom_right(),
                       outline_color);

    float display_w = layout.mem_rect_from_.width();
    float G1_w = display_w * layout.to_G1_size() / layout.to_scale();
    float G1_x1 = layout.mem_rect_from_.x_lo() + G1_w;
    ImRect G1_rect = layout.to_g1_rect();
    //ImRect G1_rect = layout.mem_rect_to_.left_fraction(layout.to_G1_size() / layout.to_scale());

    /* G1 (i.e. N1 or T1) */
    {
        ImU32   G1_color = IM_COL32(  0, 128,   0, 255);
        ImU32 text_color = IM_COL32(255, 255, 255, 255);

        char buf[255];

        if (layout.with_labels_)
            snprintf(buf, sizeof(buf), "%s\u2081: %luk",
                     layout.mnemonic(), layout.to_G1_size() / 1024); /* N1 / T1 */

        char tooltip[255];
        snprintf(tooltip, sizeof(tooltip),
                 "%s\u2081: %lu - %s survivor size in bytes",
                 layout.mnemonic(),
                 layout.to_G1_size(),
                 layout.name());

        draw_filled_rect_with_label(layout.with_labels_ ? buf : nullptr,
                                    tooltip,
                                    G1_rect,
                                    G1_color,
                                    text_color,
                                    draw_list);
    }

    ImRect G0_rect = layout.to_g0_rect();

    /* G0 (i.e. N0 or T0) */
    {
        ImU32   G0_color = IM_COL32( 32, 192,  32, 255);
        ImU32 text_color = IM_COL32(  0,   0,   0, 255);

        char buf[255];

        if (layout.with_labels_)
            snprintf(buf, sizeof(buf), "%s\u2080: %luk",
                     layout.mnemonic(),
                     layout.to_G0_size() / 1024); /* N(0) */

        char tooltip[255];
        snprintf(tooltip, sizeof(tooltip),
                 "%s\u2080: %lu - %s new alloc size in bytes",
                 layout.mnemonic(), layout.to_G0_size(),
                 layout.name());

        draw_filled_rect_with_label(layout.with_labels_ ? buf : nullptr,
                                    tooltip,
                                    G0_rect,
                                    G0_color,
                                    text_color,
                                    draw_list);
    }

    /* mark where next gc will trigger */
    if (layout.with_labels_) {
        const char * uparrow = reinterpret_cast<const char *>(u8"\u25b3");

        float ngc_w = (display_w * layout.gendescr_.gc_threshold_) / G_to_scale;

        auto tmp = ImGui::CalcTextSize(uparrow);
        std::size_t uparrow_w = tmp.x;
        double ngc_x = layout.chart_withlabel_rect_.x_lo() + ngc_w - uparrow_w/2.0;

        ImVec2 marker_pos(ngc_x, layout.chart_withlabel_rect_.y_hi());

        draw_list->AddText(marker_pos,
                           IM_COL32(255, 128, 128, 255) /*red*/,
                           uparrow);

        ImGui::SetCursorScreenPos(marker_pos);
        ImGui::InvisibleButton("mkbutton", tmp);
        if (ImGui::IsItemHovered()) {
            char marker_tt_buf[255];
            snprintf(marker_tt_buf, sizeof(marker_tt_buf),
                     "Next %s GC when size(%s) >= %lu bytes",
                     layout.gc_type(),
                     layout.name(),
                     layout.gendescr_.gc_threshold_);

            ImGui::SetTooltip("%s", marker_tt_buf);
        }
    }
} /*draw_generation*/

void
draw_nursery(const GcStateDescription & gcstate,
             bool with_labels,
             const ImRect & rect,
             ImDrawList * draw_list,
             GenerationLayout * p_layout)
{
    using xo::gc::generation;

    const GcGenerationDescription & gendescr = gcstate.get_gendescr(generation::nursery);

    GenerationLayout layout(gendescr, rect, with_labels);

    draw_generation(layout, draw_list);

    if (p_layout)
        *p_layout = layout;
}

void
draw_tenured(const GcStateDescription & gcstate,
             bool with_labels,
             const ImRect & rect,
             ImDrawList * draw_list,
             GenerationLayout * p_layout)
{
    using xo::gc::generation;

    const GcGenerationDescription & gendescr = gcstate.get_gendescr(generation::tenured);

    GenerationLayout layout(gendescr, rect, with_labels);

    draw_generation(layout, draw_list);

    if (p_layout)
        *p_layout = layout;
}

/** for history tooltip, choose which statistic to headline **/
enum class gc_history_headline {
    survive,
    promote,
    persist,
    garbage0,
    garbage1,
    garbageN,
    N
};

xo::flatstring<512>
write_gc_history_tooltip(gc_history_headline headline,
                         const GcStatisticsHistoryItem & stats)
{
    xo::flatstring<512> retval;

    xo::flatstring<512> headline_str;
    switch (headline) {
    case gc_history_headline::survive:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "survive: %lu: bytes surviving 1st GC after allocation",
                 stats.survive_z_);
        break;
    case gc_history_headline::promote:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "promote: %lu: bytes surviving 2nd GC; if nursery promote to tenured",
                 stats.promote_z_);
        break;
    case gc_history_headline::persist:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "persist: %lu: bytes surviving 3+ GCs. Only non-zero for full collections",
                 stats.persist_z_);
        break;
    case gc_history_headline::garbage0:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "garbage\u2080: %lu: bytes collected on 1st GC after allocation",
                 stats.garbage0_z_);
        break;
    case gc_history_headline::garbage1:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "garbage\u2081: %lu: bytes collected on 2nd GC after allocation",
                 stats.garbage1_z_);
        break;
    case gc_history_headline::garbageN:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "garbage\u2099: %lu: bytes collected on 3rd or later GC after allocation",
                 stats.garbageN_z_);
        break;
    case gc_history_headline::N:
        assert(false);
        break;
    }



    snprintf(retval.data(), retval.capacity(),
             "%s\n"
             "\n"
             " gcseq: %lu\n"
             " type: %s\n"
             " alloc: %lu\n"
             " survive: %lu\n"
             " promote: %lu\n"
             " persist: %lu\n"
             " garbage\u2080: %lu\n" /*garbage0*/
             " garbage\u2081: %lu\n" /*garbage1*/
             " garbage\u2099: %lu\n" /*garbageN*/
             " effort: %lu dt: %.1lfus\n"
             " copy efficiency: %.1lf%% collection rate: %.0lf bytes/sec",
             headline_str.c_str(),
             stats.gc_seq_,
             (stats.upto_ == generation::nursery) ? "incremental" : "FULL",
             stats.new_alloc_z_,
             stats.survive_z_,
             stats.promote_z_,
             stats.persist_z_,
             stats.garbage0_z_,
             stats.garbage1_z_,
             stats.garbageN_z_,
             stats.effort_z_,
             1e-3 * stats.dt_.scale(),
             100.0 * stats.efficiency(),
             stats.collection_rate()
        );

    return retval.ensure_final_null();
}

/** stacked bar chart
 *
 *  @param gen  if @ref generation::nursery, only display nursery collections.
 *              otherwise display both
 **/
void
draw_gc_history(const GcStateDescription & gcstate,
                generation gen,
                const GcStatisticsHistory & gc_history,
                const ImRect & bounding_rect,
                bool debug_flag,
                ImDrawList * draw_list)
{
    scope log(XO_DEBUG(debug_flag));

    float lm = 10;
    float tm = 25;

    /* we're going to make a bar chart */

    /* x_scale,y_scale in GC units (i.e. bytes) */
    size_t x_scale = gc_history.capacity();
    size_t yplus_scale = 0;
    size_t yminus_scale = 0;

    float display_w = bounding_rect.width()  - lm;
    float display_h = bounding_rect.height() - tm;

    /* 1st loop: figure out max y scale */
    for (const GcStatisticsHistoryItem & stats : gc_history) {
        if ((gen == stats.upto_) || (gen == generation::tenured))
        {
            //size_t  na = stats.new_alloc_z_ - stats.survive_z_; /*new allocs, but dont' double-count survive_z*/
            size_t  sz = stats.survive_z_; /*survive 1st gc */
            size_t  pz = stats.promote_z_; /*survive 2nd gc */
            size_t psz = stats.persist_z_; /*survive 3+ gc */
            size_t g0z = stats.garbage0_z_;
            size_t g1z = stats.garbage1_z_;
            size_t gNz = stats.garbageN_z_;

            if (yplus_scale < sz + pz + psz)
                yplus_scale = sz + pz + psz;

            if (yminus_scale < g0z + g1z + gNz)
                yminus_scale = g0z + g1z + gNz;
        } else {
            ;
        }
    }

    /* y-coord of x-axis */
    float y_zero = bounding_rect.y_lo() + tm + (display_h * yplus_scale) / (yplus_scale + yminus_scale);

    float y_scale = yplus_scale + yminus_scale;

    /* width of 1 bar in screen coords */
    constexpr float c_min_bar_w = 5.0;
    float bar_w = std::max(c_min_bar_w, display_w / gc_history.capacity());

    /* 2nd loop: draw bars */
    std::size_t i = 0;
    for (const GcStatisticsHistoryItem & stats : gc_history)
    {
        if ((gen == stats.upto_) || (gen == generation::tenured))
        {
            /*
             *     ys_lo   +--+
             *             |  |   survive_z  (survived 1st GC)
             *             |  |
             *     yp_lo   +--+
             *             |  |   promote_z  (sruvived 2nd GC)
             *             |  |
             *   ypsz_lo   +--+
             *             |  |   persist_z  (survived 3+ GCs)
             *             |  |
             *    y_zero   +--+
             *             |  |   gN  (killed on 3+ GC)
             *             |  |
             *    ygN_hi   +--+
             *             |  |   g1  (killed on 2nd GC)
             *             |  |
             *    yg1_hi   +--+
             *             |  |   g0  (killed on 1st GC)
             *             |  |
             *    yg0_hi   +--+
             */

            ImU32  persist_color = IM_COL32(  0,  64, 192, 255); /*darker blue*/
            ImU32  promote_color = IM_COL32(  0, 128,   0, 255); /*darker green*/
            ImU32  survive_color = IM_COL32( 32, 192,  32, 255); /*lighter green*/
            ImU32 garbageN_color = IM_COL32(255, 128,  64, 255); /*darker orange*/
            ImU32 garbage1_color = IM_COL32(255, 192, 128, 255); /*medium orange*/
            ImU32 garbage0_color = IM_COL32(255, 255, 192, 255); /*pale yellow*/

            /* x-coordinates of bar */
            float x_lo = bounding_rect.x_lo() + lm + i * bar_w;
            float x_hi = x_lo + bar_w - 1;
            ImVec2 x_span{x_lo, x_hi};

            /* y-coordinates of persist bar (survived 3+ GCs) */
            float ypsz_lo = (y_zero
                             - (display_h * stats.persist_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::persist, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(ypsz_lo, y_zero)),
                                 persist_color,
                                 draw_list);
            }
            /* y-coordinates of promote bar (survived 2nd GC) */
            float yp_hi = ypsz_lo;
            float yp_lo = (yp_hi
                           - (display_h * stats.promote_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::promote, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(yp_lo, yp_hi)),
                                 promote_color,
                                 draw_list);
            }

            /* y-coordinates of survivor bar (survived 1st GC) */
            float ys_hi = yp_lo;
            float ys_lo = (ys_hi - (display_h * stats.survive_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::survive, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(ys_lo, ys_hi)),
                                 survive_color,
                                 draw_list);
            }

            // -----------------------------------------------------------

            /* y-coordinates of garbageN bar (killed on 3+ GC) */
            float ygN_lo = y_zero;
            float ygN_hi = (y_zero
                            + (display_h * stats.garbageN_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::garbageN, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(ygN_lo, ygN_hi)),
                                 garbageN_color,
                                 draw_list);
            }

            /* y-coordinates of garbage1 bar (killed on 2nd GC) */
            float yg1_lo = ygN_hi;
            float yg1_hi = (yg1_lo
                            + (display_h * stats.garbage1_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::garbage1, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect(ImVec2(x_lo, yg1_lo), ImVec2(x_hi, yg1_hi)),
                                 garbage1_color,
                                 draw_list);
            }

            /* y-coordinates of garbage0 bar (killed on 1st GC) */
            float yg0_lo = yg1_hi;
            float yg0_hi = (yg0_lo
                            + (display_h * stats.garbage0_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::garbage0, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect(ImVec2(x_lo, yg0_lo), ImVec2(x_hi, yg0_hi)),
                                 garbage0_color,
                                 draw_list);
            }
        } else {
            /* draw nothing */
            ;
        }

        ++i;
    }

    log && log(xtag("i", i));
}

void
draw_gc_efficiency(const GcStateDescription & gcstate,
                   //generation gen,
                   const GcStatisticsHistory & gc_history,
                   const ImRect & bounding_rect,
                   bool debug_flag,
                   ImDrawList * draw_list)
{
    scope log(XO_DEBUG(debug_flag));

    float lm = 10;
    float tm = 25;

    /* we're going to make a level chart */

    /* x_scale,y_scale in GC units (i.e. bytes) */
    size_t x_scale = gc_history.capacity();
    size_t yplus_scale = 1;
    size_t yminus_scale = 0;

    float display_w = bounding_rect.width()  - lm;
    float display_h = bounding_rect.height() - tm;

#ifdef NOPE // don't need this.  y-scale is [0.0, 1.0]
    /* 1st loop: figure out max y scale */
    for (const GcStatisticsHistoryItem & stats : gc_history) {
        if ((gen == stats.upto_) || (gen == generation::tenured))
        {
            //size_t  na = stats.new_alloc_z_ - stats.survive_z_; /*new allocs, but dont' double-count survive_z*/
            size_t  sz = stats.survive_z_; /*survive 1st gc */
            size_t  pz = stats.promote_z_; /*survive 2nd gc */
            size_t psz = stats.persist_z_; /*survive 3+ gc */
            size_t g0z = stats.garbage0_z_;
            size_t g1z = stats.garbage1_z_;
            size_t gNz = stats.garbageN_z_;

            if (yplus_scale < sz + pz + psz)
                yplus_scale = sz + pz + psz;

            if (yminus_scale < g0z + g1z + gNz)
                yminus_scale = g0z + g1z + gNz;
        } else {
            ;
        }
    }
#endif

    /* y-coord of x-axis */
    float y_zero = bounding_rect.y_lo() + tm + display_h;
    //float y_zero = bounding_rect.y_lo() + tm + (display_h * yplus_scale) / (yplus_scale + yminus_scale);

    float y_scale = 1.0;

    /* width of 1 bar in screen coords */
    constexpr float c_min_bar_w = 5.0;
    float bar_w = std::max(c_min_bar_w, display_w / gc_history.capacity());

    /* TODO: use temporary arena */
    std::vector<ImVec2> line_points;
    line_points.reserve(gc_history.size());

    ImU32 average_color = IM_COL32(255, 255,  64, 255); /*solid yellow*/
    ImU32  sample_color = IM_COL32(255, 255, 255, 255); /*white*/

    /* 2nd loop: draw levels */
    std::size_t i = 0;
    for (const GcStatisticsHistoryItem & stats : gc_history)
    {
        //std::vector<ImVec2> line_points = { /* your points */ };
        //draw_list->AddPolyline(line_points.data(), line_points.size(),
        //                       IM_COL32(255, 255, 0, 255), false, 2.0f);

        float y = y_zero - display_h * stats.efficiency();
        float y_mean = y_zero - display_h * stats.average_efficiency();

        /* x-coordinates of point */
        float x = bounding_rect.x_lo() + lm + i * bar_w + 0.5 * bar_w;

        line_points.push_back(ImVec2(x, y_mean));

        draw_list->AddCircleFilled(ImVec2(x, y), 2.0f, sample_color);

        ++i;
    }

    draw_list->AddPolyline(line_points.data(),
                           line_points.size(),
                           average_color,
                           false,
                           1.0f /*line width?*/);

    log && log(xtag("i", i));
} /*draw_gc_efficiency*/


void
draw_gc_alloc_state(const GcStateDescription & gcstate,
                    const ImRect & canvas_rect,
                    ImDrawList * draw_list,
                    GenerationLayout * p_nursery_layout,
                    GenerationLayout * p_tenured_layout
                    //ImRect * p_nursery_alloc_rect,
                    //ImRect * p_tenured_alloc_rect
    )
{
    constexpr float c_est_chart_text_height = 14.0;
    constexpr float c_min_h =  7;  // chart bar height
    constexpr float c_max_h = 40;  // chart bar height

    /* bounding rectange for nursery display */
    ImRect N_space_rect = canvas_rect.top_fraction(0.5,
                                                   c_min_h + c_est_chart_text_height,
                                                   c_max_h + c_est_chart_text_height);

    //assert(N_space_rect.height() >= c_min_h + c_est_chart_text_height);
    //assert(N_space_rect.height() <= c_max_h + c_est_chart_text_height);

    /* rectangle representing allocated nursery range */
    //float N_x1 = 0.0;
    GenerationLayout N_layout;

    draw_nursery(gcstate,
                 true /*with_labels*/,
                 N_space_rect,
                 draw_list,
                 &N_layout);

    float N_x1 = N_layout.chart_nolabel_rect_.x_hi();

    if (p_nursery_layout)
        *p_nursery_layout = N_layout;

//    if (p_nursery_alloc_rect)
//        *p_nursery_alloc_rect = N_layout.to_alloc_rect();

    /* N0_to_size..N_to_scale: in bytes */
    std::size_t N_to_scale = gcstate.gen_state_v_[gen2int(generation::nursery)].tospace_scale_;

    /* display_w .. N0_h : viewportcoords */
    std::size_t display_w = canvas_rect.width();

    std::size_t x0 = canvas_rect.x_lo();
    std::size_t x1 = canvas_rect.x_hi();

    // now turn to Tenured space

    std::size_t T_to_scale = gcstate.gen_state_v_[gen2int(generation::tenured)].tospace_scale_;

    /* want to put to-scale image of nursery next to to-scale image of tenured;
     * but also want space between them.
     */
    float TplusN_to_scale = N_to_scale + T_to_scale;
    /* space between T, N images */
    float TplusN_spacer = 10;

    /* bounding rectange for tenured display */
    ImRect T_space_rect = (canvas_rect
                           .within_bottom_margin(c_est_chart_text_height)
                           .bottom_fraction(0.5,
                                            c_min_h + c_est_chart_text_height,
                                            c_max_h + c_est_chart_text_height));

    if (N_space_rect.y_hi() > T_space_rect.y_lo()) {
        T_space_rect = T_space_rect.translate(ImVec2(0, N_space_rect.y_hi() - T_space_rect.y_lo()));
    }

    assert(T_space_rect.y_lo() >= N_space_rect.y_hi());

    /* for smaller image of nursery */
    //std::size_t t_y0 = canvas_rect.y_lo() + 70 + alloc_height + 20;

    /* for side-by-side tenured + nursery, with both on same scale
     * 2nd term is horiz space used for N label like 'Mem: 28k'
     */
    std::size_t adj_display_w = display_w - (N_space_rect.x_hi() - N_x1);

    /* bounding rectangle for secondary nursery display */
    ImRect np_rect(ImVec2(x0 + (adj_display_w * (T_to_scale/TplusN_to_scale)),
                          T_space_rect.y_lo() + c_est_chart_text_height),
                   ImVec2(x0 + adj_display_w,
                          T_space_rect.y_hi()));

    // redraw nursery to same scale as tenured
    {
        draw_list->AddLine(N_space_rect.bottom_left(), np_rect.top_left(),
                           IM_COL32(128, 128, 128, 255) /*grey*/);
        draw_list->AddLine(ImVec2(N_x1, N_space_rect.y_hi()), np_rect.top_right(),
                           IM_COL32(128, 128, 128, 255) /*grey*/);

        draw_nursery(gcstate,
                     false /*no labels*/,
                     np_rect,
                     draw_list,
                     nullptr);
    }

    /* rectangle representing allocated tenured range */
    GenerationLayout T_layout;

    draw_tenured(gcstate,
                 true /*with labels*/,
                 ImRect(ImVec2(x0, T_space_rect.y_lo()),
                        ImVec2(x0 + (adj_display_w * (T_to_scale/TplusN_to_scale)) - TplusN_spacer,
                               T_space_rect.y_hi())),
                 draw_list,
                 &T_layout);

    if (p_tenured_layout)
        *p_tenured_layout = T_layout;

//    if (p_tenured_alloc_rect)
//        *p_tenured_alloc_rect = T_layout.to_alloc_rect();

} /*draw_gc_alloc_state*/

void
draw_gc_state(const AppState & app_state,
              const GcStateDescription & gcstate,
              const ImRect & canvas_rect,
              ImDrawList * draw_list,
              GenerationLayout * p_nursery_layout,
              GenerationLayout * p_tenured_layout,
              //ImRect * p_nursery_alloc_rect,
              //ImRect * p_tenured_alloc_rect,
              ImRect * p_history_rect)
{
    // draw stuff
    draw_list->AddRect(canvas_rect.top_left(),
                       canvas_rect.bottom_right(),
                       IM_COL32(255, 255, 255, 255));

    /* TODO: does this reset coord space? */
    ImRect alloc_rect;
    {
        ImGui::BeginChild("top pane", ImVec2(0, 105), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY);

        alloc_rect = ImRect(canvas_rect.top_left() + ImGui::GetWindowContentRegionMin(),
                            canvas_rect.top_left() + ImGui::GetWindowContentRegionMax());
        ImRect draw_rect = alloc_rect.within_margin(ImRect(50, 10, 70, 10));

        draw_list->PushClipRect(draw_rect.top_left(), draw_rect.bottom_right());

        draw_gc_alloc_state(gcstate,
                            draw_rect,
                            draw_list,
                            p_nursery_layout,
                            p_tenured_layout
                            //p_nursery_alloc_rect,
                            //p_tenured_alloc_rect
            );

        draw_list->PopClipRect();

        ImGui::EndChild();
    }

    ImRect history_rect;
    {
        ImGui::BeginChild("left pane", ImVec2(800, 0), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX);

        /* history below alloc area */
        history_rect = ImRect(alloc_rect.bottom_left() + ImGui::GetWindowContentRegionMin(),
                              alloc_rect.bottom_left() + ImGui::GetWindowContentRegionMax());

         if (p_history_rect)
             *p_history_rect = history_rect;

         draw_list->PushClipRect(history_rect.top_left(), history_rect.bottom_right());

         float lm = 50;
         float rm = 70;
         float tm = 10;
         std::size_t x0 = history_rect.x_lo() + lm;
         std::size_t x1 = history_rect.x_hi() - rm;
         std::size_t h_y0 = history_rect.y_lo() + tm;

         /* just incremental (nursery) collections */
         ImRect incremental_rect = history_rect.top_fraction(0.33);

         draw_gc_history(gcstate,
                         generation::nursery,
                         app_state.gc_->gc_history(),
                         incremental_rect,
                         false /*debug_flag*/,
                         draw_list);

         /* just full (nursery+tenured) collections */
         ImRect full_rect = history_rect.bottom_fraction(0.67).top_fraction(0.5);

         /* both nursery + full collections */
         draw_gc_history(gcstate,
                         generation::tenured,
                         app_state.gc_->gc_history(),
                         full_rect,
                         false /*debug_flag*/,
                         draw_list);

         ImRect efficiency_rect = history_rect.bottom_fraction(0.67).bottom_fraction(0.5);

         draw_gc_efficiency(gcstate,
                            app_state.gc_->gc_history(),
                            efficiency_rect,
                            false /*debug_flag*/,
                            draw_list);

         draw_list->PopClipRect();

         ImGui::EndChild();
    }

    ImGui::Text("placeholder text");

    /* BeginChild() again ? */

#ifdef NOPE
    draw_list->AddCircleFilled(ImVec2(canvas_p0.x + 50, canvas_p0.y + 50),
                               30.0f, IM_COL32(255, 0, 0, 255));

    draw_list->AddText(ImVec2(canvas_p0.x + 10, canvas_p0.y + 10),
                       IM_COL32(255, 255, 255, 255), "Hello 2D!");
#endif
}

struct DrawState;

struct AnimateGcCopyCb : public xo::gc::GcCopyCallback {
    using generation = xo::gc::generation;

    explicit AnimateGcCopyCb(AppState * appstate, DrawState * drawstate)
        : p_app_state_{appstate}, p_draw_state_{drawstate} {}

    virtual void notify_gc_copy(std::size_t z,
                                const void * src_addr, const void * dest_addr,
                                generation src_gen, generation dest_gen);

    AppState * p_app_state_ = nullptr;
    DrawState * p_draw_state_ = nullptr;
};

enum class draw_state_type {
    alloc,
    animate_gc
};

struct DrawState {
    up<xo::gc::GcCopyCallback> make_gc_copy_animation(AppState * app_state) {
        return std::make_unique<AnimateGcCopyCb>(app_state, this);
    }

    draw_state_type state_type_ = draw_state_type::alloc;

    /** budgeted time period over which to animate gc copy **/
    int animate_copy_budget_ms_ = 2000;
    /** start time of current copy animation **/
    std::chrono::steady_clock::time_point animate_copy_t0_;
    /** when animating copy step, display objects from AppState::copy_detail_v_[i]
     *  where i < .animate_copy_hi_ / 100 * AppState::copy_detail_v_.size()
     **/
    float animate_copy_hi_pct_ = 0;

    ImDrawList * gcw_draw_list_ = nullptr;

    /** draw area **/
    ImVec2 gcw_canvas_p0_;
    ImVec2 gcw_canvas_p1_;

    /** layout for nursery display **/
    GenerationLayout gcw_nursery_layout_;
    /** layout for tenured display **/
    GenerationLayout gcw_tenured_layout_;

    /** rect displaying gc history (strip charts) **/
    ImRect gcw_history_rect_;
};

ImRect map_src_alloc_to_screen(const GcCopyDetail & copy_detail,
                               const ImRect & space_rect)
{
    // TODO: methods on copy_detail / and/or ImPoint

    auto [x_coord_lo, x_coord_hi] = space_rect.x_span();

    double w0 = copy_detail.src_offset_ / static_cast<double>(copy_detail.src_space_z_);
    float src0_x = ((1.0 - w0) * x_coord_lo) + (w0 * x_coord_hi);

    double w1 = ((copy_detail.src_offset_ + copy_detail.z_)
                 / static_cast<double>(copy_detail.src_space_z_));
    float src1_x = ((1.0 - w1) * x_coord_lo) + (w1 * x_coord_hi);

    return space_rect.with_x_span(src0_x, src1_x);
}

ImRect map_dest_alloc_to_screen(const GcCopyDetail & copy_detail,
                                const ImRect & space_rect)
{
    /* for from-space, want to use full width of memory space */

    auto [x_coord_lo, x_coord_hi] = space_rect.x_span();

    // dest_space_z_ ?

    double w0 = copy_detail.dest_offset_ / static_cast<double>(copy_detail.dest_z_);
    float dest0_x = ((1.0 - w0) * x_coord_lo) + (w0 * x_coord_hi);

    double w1 = ((copy_detail.dest_offset_ + copy_detail.z_)
                 / static_cast<double>(copy_detail.dest_z_));
    float dest1_x = ((1.0 - w1) * x_coord_lo) + (w1 * x_coord_hi);

    return space_rect.with_x_span(dest0_x, dest1_x);
}

/* editor bait: animate_copy() */
void animate_gc_copy(const AppState & app_state,
                     const DrawState & draw_state,
                     ImDrawList * draw_list)
{
    /* NOTE: this only runs during GC copy.
     *       draw_state.gcw_nursery_layout_ and gcw_tenured_layout_
     *       are taken from snapshots made before GC began,
     *       ergo before to/from spaces were swapped
     */
    ImRect nursery_src_rect = draw_state.gcw_nursery_layout_.to_alloc_rect();
    ImRect tenured_src_rect = draw_state.gcw_tenured_layout_.to_alloc_rect();

    ImRect nursery_dest_rect = draw_state.gcw_nursery_layout_.mem_rect_from_;
    ImRect tenured_dest_rect;

    if (app_state.upto_ == generation::nursery) {
        tenured_dest_rect = draw_state.gcw_tenured_layout_.mem_rect_to_;
    } else {
        tenured_dest_rect = draw_state.gcw_tenured_layout_.mem_rect_from_;
    }

    std::size_t n_copy = app_state.copy_detail_v_.size();
    /* grade from black-to-white between lo_copy and hi_copy.
     * Note we allow animate_copy_hi_pct_ > 100.0
     */
    float lo_copy = 0.01 * std::max(0.0, draw_state.animate_copy_hi_pct_ - 14.0) * n_copy;
    float hi_copy = 0.01 * draw_state.animate_copy_hi_pct_ * n_copy;

    /* remember max copy offset seen in {nursery, tenured} space respectively,
     * so we can label it
     */
    std::size_t last_nursery_dest_offset = 0;
    ImRect last_nursery_dest_rect;
    std::size_t first_tenured_dest_offset = 0;
    ImRect first_tenured_dest_rect;
    std::size_t last_tenured_dest_offset = 0;
    ImRect last_tenured_dest_rect;

    std::size_t i_copy = 0;
    for (const auto & copy_detail : app_state.copy_detail_v_) {
        /* cutout for each copied object */
        {
            float wt = (i_copy > lo_copy) ? static_cast<float>(i_copy) / hi_copy : 0.0;
            /* grey fading to black */
            //ImU32 color = IM_COL32(wt*128, 64+wt*64, wt*128, 255);
            ImU32 color = IM_COL32( 96, 224, 255, 255);

            ImRect src_rect;

            if (copy_detail.src_gen_ == generation::nursery) {
                src_rect = map_src_alloc_to_screen(copy_detail, nursery_src_rect);
            } else {
                src_rect = map_src_alloc_to_screen(copy_detail, tenured_src_rect);
            }

            draw_list->AddRectFilled(src_rect.top_left(),
                                     src_rect.bottom_right(),
                                     color);
        }

        if (copy_detail.dest_gen_ == generation::nursery) {
            last_nursery_dest_rect = map_dest_alloc_to_screen(copy_detail, nursery_dest_rect);
            last_nursery_dest_offset = copy_detail.dest_offset_;
        } else if (copy_detail.dest_gen_ == generation::tenured) {
            last_tenured_dest_rect = map_dest_alloc_to_screen(copy_detail, tenured_dest_rect);
            last_tenured_dest_offset = copy_detail.dest_offset_;

            if (first_tenured_dest_rect.width() == 0) {
                first_tenured_dest_rect = last_tenured_dest_rect;
                first_tenured_dest_offset = copy_detail.dest_offset_;
            }
        }

        if (++i_copy >= hi_copy) {
            break;
        }
    }

    if (last_nursery_dest_rect.width() > 0.0) {
        //ImU32 color = IM_COL32(64, 255, static_cast<int>(64 + (128 * wt)), 255);
        ImU32 color = IM_COL32(  0, 96, 192, 255);

        draw_list->AddRectFilled(nursery_dest_rect.top_left(),
                                 last_nursery_dest_rect.bottom_right(),
                                 color);

        char buf[255];
        snprintf(buf, sizeof(buf), "N\u2081: %luk", last_nursery_dest_offset / 1024);

        auto textz = ImGui::CalcTextSize(buf);

        ImU32  text_color = IM_COL32(255, 255, 255, 255); /*black*/
        ImVec2 text_pos   = ImVec2(0.5 * (nursery_dest_rect.x_lo()
                                          + last_nursery_dest_rect.x_hi()
                                          - textz.x),
                                   nursery_dest_rect.y_mid() - 0.5 * textz.y);

        if (text_pos.x < nursery_dest_rect.x_lo())
            text_pos.x = nursery_dest_rect.x_lo() + 2;
        else if (text_pos.x < nursery_dest_rect.x_lo() + 0.5 * textz.x)
            text_pos.x = nursery_dest_rect.x_lo() + 0.5 * textz.x;

        draw_list->AddText(text_pos, text_color, buf);

    }

    if (last_tenured_dest_rect.width() > 0.0) {
        ImU32 color = IM_COL32(  0,  96, 192, 255);

        draw_list->AddRectFilled(first_tenured_dest_rect.top_left(),
                                 last_tenured_dest_rect.bottom_right(),
                                 color);

        char buf[255];
        snprintf(buf, sizeof(buf), "+%luk", (last_tenured_dest_offset - first_tenured_dest_offset) / 1024);

        auto textz = ImGui::CalcTextSize(buf);

        ImU32  text_color = IM_COL32(255, 255, 255, 255); /*black*/
        float x0 = first_tenured_dest_rect.x_lo();
        float x1 = last_tenured_dest_rect.x_hi();
        ImVec2 text_pos   = ImVec2(0.5 * (x0 + x1 - textz.x),
                                   tenured_dest_rect.y_mid() - 0.5 * textz.y);

        if (text_pos.x < x0 + 2)
            text_pos.x = x0 + 2;
        else if (text_pos.x < x0 + 0.5 * textz.x)
            text_pos.x = x0 + 0.5 * textz.x;

        draw_list->AddText(text_pos, text_color, buf);
    }
}

void
AnimateGcCopyCb::notify_gc_copy(std::size_t z,
                                const void * src_addr,
                                const void * dest_addr,
                                generation src_gen,
                                generation dest_gen)
{
    using xo::scope;
    using xo::xtag;
    using xo::gc::generation_result;
    using xo::gc::generation;
    using xo::gc::role;

    scope log(XO_DEBUG(false),
              xtag("z", z),
              xtag("src", src_addr),
              xtag("dest", dest_addr),
              xtag("src_gen", src_gen),
              xtag("dest_gen", dest_gen));

    auto [src_gen2, src_offset, src_alloc, src_size] = p_app_state_->gc_->fromspace_location_of(src_addr);

    if (src_gen2 == generation_result::not_found) {
        auto [lo, hi] = p_app_state_->gc_->nursery_span(role::from_space);

        log && log(xtag("N.from.lo", (void*)lo), xtag("N.from.hi", (void*)hi));

        assert(false);
    }

    generation src_valid_gen = xo::gc::valid_genresult2gen(src_gen2);

    auto [dest_gen2, dest_offset, _, dest_size] = p_app_state_->gc_->tospace_location_of(dest_addr);

    generation dest_valid_gen = xo::gc::valid_genresult2gen(dest_gen2);

    p_app_state_->copy_detail_v_.push_back(GcCopyDetail(z,
                                                        src_valid_gen, src_offset, src_alloc,
                                                        dest_valid_gen, dest_offset, dest_size));

    if (dest_valid_gen == generation::nursery) {
        p_app_state_->copy_detail_max_nursery_dest_offset_
            = std::max(p_app_state_->copy_detail_max_nursery_dest_offset_, dest_offset);
        p_app_state_->copy_detail_nursery_dest_size_
            = std::max(p_app_state_->copy_detail_nursery_dest_size_, dest_size);
    } else if (dest_valid_gen == generation::tenured) {
        p_app_state_->copy_detail_max_tenured_dest_offset_
            = std::max(p_app_state_->copy_detail_max_tenured_dest_offset_, dest_offset);
        p_app_state_->copy_detail_tenured_dest_size_
            = std::max(p_app_state_->copy_detail_tenured_dest_size_, dest_size);
    }

    /* will be animated across frames, see animate_gc_copy() */
}

int main(int, char **)
{
    using namespace std;

    scope log(XO_DEBUG(true));

    std::cout << "Hello, world!" << std::endl;

    SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "0");

    SDL_Init(SDL_INIT_VIDEO);

    SDL_version compiled;
    SDL_VERSION(&compiled);
    std::cerr << "SDL version: "
              << (int)compiled.major
              << "." << (int)compiled.minor
              << "." << (int)compiled.patch
              << std::endl;

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                        SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

#if 0
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS,
                        SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif

    std::cerr << "SDL video driver: " << SDL_GetCurrentVideoDriver() << std::endl;

    SDL_Window * window = SDL_CreateWindow("imgui + sdl2 + opengl",
                                           SDL_WINDOWPOS_CENTERED,
                                           SDL_WINDOWPOS_CENTERED,
                                           2000,
                                           1000,
                                           SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
    if (window) {
        std::cerr << "SDL_CreateWindow done" << std::endl;
    } else {
        std::cerr << "SDL_CreateWindow failed: [" << SDL_GetError() << "]" << std::endl;
        SDL_Quit();
        return -1;
    }

    SDL_GLContext gl_context = SDL_GL_CreateContext(window);

    int major, minor;
    SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &major);
    SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &minor);

    std::cerr << "Requested OpenGL vtersion: " << major << "." << minor << std::endl;

    if (gl_context) {
        std::cerr << "SDL_GL_CreateContext done" << std::endl;
    } else {
        std::cerr << "SDL_GL_CreateContext failed: [" << SDL_GetError() << "]" << std::endl;
        return -1;
    }

    if (SDL_GL_MakeCurrent(window, gl_context) != 0) {
        std::cerr << "SDL_GL_MakeCurrent failed: [" << SDL_GetError() << "]" << std::endl;
        SDL_GL_DeleteContext(gl_context);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    SDL_GL_SetSwapInterval(0); // disable vsync

    GLenum glew_status = glewInit();
    if (glew_status == GLEW_OK) {
        std::cerr << "glewInit done" << std::endl;
    } else {
        std::cerr << "glewInit failed: [" << glewGetErrorString(glew_status) << std::endl;
        return -1;
    }

    const GLubyte * version = glGetString(GL_VERSION);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    if (version) {
        std::cerr << "OpenGL version: [" << version << "]" << std::endl;
    } else {
        std::cerr << "OpenGL version not available" << std::endl;
    }

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO & io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;

    // Load noto sans font from unix environment NOTO_ONFTS_PATH
    // (see xo-umbrella2/default.nix shellHook)

    const char * fonts_path = std::getenv("DEJAVU_FONTS_PATH");

    if (fonts_path) {
        const float font_size = 14.0f;
        std::string font_path = xo::tostr(fonts_path, "/truetype/DejaVuSans.ttf");

        /* check file exists */
        std::ifstream font_in(font_path);
        if (font_in.good()) {
            std::cerr << "loading font [" << font_path << "]" << std::endl;
            ImFont * font = io.Fonts->AddFontFromFileTTF(font_path.c_str(), font_size);
            if (font) {
                std::cerr << "font loaded" << std::endl;

                ImFontConfig config;
                config.MergeMode = true;

                // latin extended chars
                static const ImWchar latin_ranges[] = {
                    0x0020, 0x00ff,  // basic latin + latin supplement
                    0x0100, 0x017f,  // latin extended-A
                    0x0180, 0x024f,  // latin extended-B
                    0x2080, 0x2099,  // subscript numerals + letters through n
                    0x25b2, 0x25b4,  // arrows
                    0,
                };
                io.Fonts->AddFontFromFileTTF(font_path.c_str(), font_size, &config, latin_ranges);
            } else {
                std::cerr << "font file load failed" << std::endl;
                std::cerr << "Fallback to default ImGui font" << std::endl;
            }
        }


    } else {
        std::cerr << "Expected DEJAVU_FONTS_PATH environment var." << std::endl;
        std::cerr << "Fallback to default ImGui font" << std::endl;
    }
    ImGui::StyleColorsDark();

    ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
    ImGui_ImplOpenGL3_Init("#version 330");

    bool show_demo_window = true;
    bool show_another_window = false;
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
    float counter_value = 0.0f;

    using xo::obj::Integer;
    using xo::obj::List;
    using xo::rng::xoshiro256ss;
    using xo::rng::Seed;
    using xo::up;
    using xo::gp;
    using xo::gc::GC;
    using xo::Object;

    AppState app_state;
    DrawState draw_state;
    /* note: during gc copy animation, this records state _before_ gc was triggered */
    GcStateDescription gcstate = app_state.snapshot_gc_state();

    app_state.gc_->add_gc_copy_callback(draw_state.make_gc_copy_animation(&app_state));

    // Main Loop
    bool done = false;

    while (!done) {
        /** on each draw cycle, app state falls into categories:
         *  1. allocation
         *     multiple draw cycles because many allocations per gc.
         *  2. garbage collection
         *     multiple draw cycles to animate copying process
         *     Settle conflict between {GC, imgui} as to who drives event loop,
         *     in favor of imgui; achieve this by copying what GC did,
         *     so that we can animate it over multiple draw cycles
         **/

        switch (draw_state.state_type_) {
        case draw_state_type::alloc:
        {
            /** generate random alloc **/
            app_state.generate_random_mutations();

            gcstate = app_state.snapshot_gc_state();

            app_state.upto_ = (app_state.gc_->is_full_gc_pending()
                               ? generation::tenured
                               : generation::nursery);

            /* GC may run here, in which case control reenters via AnimateGcCopyCb;
             * that callback captures copy details (per object!) in AppState
             */
            if (app_state.gc_->enable_gc_once()) {
                log && log(xtag("gc-type", (app_state.upto_ == generation::tenured) ? "full" : "incremental"));

                draw_state.state_type_ = draw_state_type::animate_gc;
                draw_state.animate_copy_t0_ = std::chrono::steady_clock::now();
            }

            break;
        }
        case draw_state_type::animate_gc:
        {
            /* don't update gcstate while animating,
             * that would use post-GC space sizing
             */

            break;
        }
        }

        /** poll + handle events */
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            ImGui_ImplSDL2_ProcessEvent(&event);
            if (event.type == SDL_QUIT)
                done = true;

            if (event.type == SDL_WINDOWEVENT) {
                if (event.window.event == SDL_WINDOWEVENT_CLOSE) {
                    if (event.window.windowID == SDL_GetWindowID(window))
                    {
                        done = true;
                    }
                } else if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
                    // defer handling resize until device available
                    this->framebuffer_resized_flag_ = true;
                }
            }
        }

        //int w, h;
        //SDL_GetWindowSize(window, &w, &h);
        //glViewport(0, 0, w, h);
        glClearColor(clear_color.x * clear_color.w,
                     clear_color.y * clear_color.w,
                     clear_color.z * clear_color.w,
                     clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw dear imgui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplSDL2_NewFrame();
        ImGui::NewFrame();

        ImGui::SetNextWindowPos(ImVec2(0, 0));
        ImGui::SetNextWindowSize(io.DisplaySize);
        ImGui::Begin("Background", nullptr,
                     ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize
                     | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus
                     | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoDecoration);
        ImGui::End();

        // 1. big demo window
        if (show_demo_window)
            ImGui::ShowDemoWindow(&show_demo_window);

        // 2. show window that we create ourselves
        {
            static int counter = 0;

            ImGui::Begin("Hello, world!");
            //ImGui::Text("This is totes useful text...");
            ImGui::Checkbox("demo window", &show_demo_window);
            //ImGui::Checkbox("second window", &show_another_window);

            ImGui::SliderInt("alloc/cycle", &app_state.alloc_per_cycle_, 1, 100);
            ImGui::SliderInt("copy animation budget", &draw_state.animate_copy_budget_ms_, 10, 10000);
            //ImGui::SliderFloat("alloc/cycle", &alloc_per_cycle, 0.0f, 1.0f);
            //ImGui::ColorEdit3("clear color", (float*)&clear_color);

            //if (ImGui::Button("Button"))
            //  ++counter;
            ImGui::NewLine(); // ImGui::SameLine()
            /* N\u2080 = N0, N\u2081 = N1 */
            ImGui::Text("alloc [%lu] avail [%lu] ",
                        gcstate.gc_allocated_,
                        gcstate.gc_available_);
            //ImGui::NewLine();
            ImGui::Text("promoted [%lu] copy animation [%lu / %lu]",
                        gcstate.total_promoted_,
                        static_cast<std::size_t>(draw_state.animate_copy_hi_pct_ * app_state.copy_detail_v_.size() / 100),
                        app_state.copy_detail_v_.size());

            ImGui::Text("mutation [%lu] mlog [%lu]",
                        gcstate.total_n_mutation_,
                        gcstate.gc_mlog_size_);

            ImGui::Text("appl average %.3f ms/frame (%.1f fps)",
                        1000.0f / io.Framerate, io.Framerate);

            ImGui::Text("layout:"
                        " nursery-src alloc rect [%.1f %.1f %.1f %.1f]"
                        " nursery-dest alloc rect [%.1f %.1f %.1f %.1f]"
                        " history rect [%.1f %.1f %.1f %.1f]",
                        draw_state.gcw_nursery_layout_.to_alloc_rect().x_lo(),
                        draw_state.gcw_nursery_layout_.to_alloc_rect().y_lo(),
                        draw_state.gcw_nursery_layout_.to_alloc_rect().x_hi(),
                        draw_state.gcw_nursery_layout_.to_alloc_rect().y_hi(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().x_lo(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().y_lo(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().x_hi(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().y_hi(),
                        draw_state.gcw_history_rect_.x_lo(),
                        draw_state.gcw_history_rect_.y_lo(),
                        draw_state.gcw_history_rect_.x_hi(),
                        draw_state.gcw_history_rect_.y_hi());
            ImGui::Text("nursery-dest copy offset [%lu] / size [%lu]"
                        " tenured-dest copy offset [%lu] / size [%lu]",
                        app_state.copy_detail_max_nursery_dest_offset_,
                        app_state.copy_detail_nursery_dest_size_,
                        app_state.copy_detail_max_tenured_dest_offset_,
                        app_state.copy_detail_tenured_dest_size_
                );

            ImDrawList * draw_list = ImGui::GetWindowDrawList();

            ImVec2 canvas_p0 = ImGui::GetCursorScreenPos();
            ImVec2 canvas_sz = ImGui::GetContentRegionAvail();
            ImVec2 canvas_p1 = ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y);

            /* stash so GC copy animation can find it */
            draw_state.gcw_draw_list_ = draw_list;
            draw_state.gcw_canvas_p0_ = canvas_p0;
            draw_state.gcw_canvas_p1_ = canvas_p1;

            draw_gc_state(app_state,
                          gcstate,
                          ImRect(canvas_p0, canvas_p1),
                          draw_list,
                          &draw_state.gcw_nursery_layout_,
                          &draw_state.gcw_tenured_layout_,
                          //nullptr,
                          //&draw_state.gcw_tenured_alloc_rect_,
                          &draw_state.gcw_history_rect_);

            if (draw_state.state_type_ == draw_state_type::animate_gc) {
                auto animate_copy_t1 = std::chrono::steady_clock::now();
                auto animate_dt = animate_copy_t1 - draw_state.animate_copy_t0_;
                float animate_fraction_spent
                    = (std::chrono::duration_cast<std::chrono::milliseconds>(animate_dt).count()
                       / static_cast<float>(draw_state.animate_copy_budget_ms_));

                draw_state.animate_copy_hi_pct_ = 100.0 * animate_fraction_spent;
                animate_gc_copy(app_state,
                                draw_state,
                                draw_list);

                /* see 25.0 constant in animate_gc_copy() */
                if (draw_state.animate_copy_hi_pct_ >= 114) {
                    draw_state.state_type_ = draw_state_type::alloc;
                    draw_state.animate_copy_hi_pct_ = 0;
                    app_state.copy_detail_v_.clear();
                    app_state.copy_detail_max_nursery_dest_offset_ = 0;
                    app_state.copy_detail_nursery_dest_size_ = 0;
                    app_state.copy_detail_max_tenured_dest_offset_ = 0;
                    app_state.copy_detail_tenured_dest_size_ = 0;
                }
            }

            ImGui::End();
        }

        // 3. another window
        if (show_another_window) {
            ImGui::Begin("another window", &show_another_window);

            ImGui::Text("hello from second window");
            if (ImGui::Button("close me"))
                show_another_window = false;

            ImGui::End();
        }

        // rendering
        ImGui::Render();

        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        SDL_GL_SwapWindow(window);
    }

    std::cerr << "cleanup.." << std::endl;

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL2_Shutdown();
    ImGui::DestroyContext();

    SDL_GL_DeleteContext(gl_context);
    SDL_DestroyWindow(window);
    SDL_Quit();

    std::cerr << "All done, goodbye..." << std::endl;

    return 0;
}

/* xo-imgui/example/ex1/imgui_ex2.cpp
 *
 * author: Roland Conybeare, Aug 2025
 */

#include "xo/object/Integer.hpp"
#include "xo/object/List.hpp"
#include "xo/alloc/GC.hpp"
#include "xo/alloc/Object.hpp"
#include "xo/randomgen/xoshiro256.hpp"
#include "xo/randomgen/random_seed.hpp"
#include "xo/object/Integer.hpp"
#include "xo/flatstring/flatstring.hpp"
#include "xo/indentlog/scope.hpp"

#include <SDL.h>
#include <GL/glew.h>
#include <cstddef>

#include "SDL_events.h"
#include "imgui.h"
#include "backends/imgui_impl_sdl2.h"
#include "backends/imgui_impl_opengl3.h"

//#include <GLFW/glfw3.h>

#ifdef NOPE
#include <SDL_opengl.h>
#endif

#include <iostream>
#include <fstream>
#include <algorithm>
#include <unistd.h>

using xo::gc::generation;

ImVec2 operator+(const ImVec2 & p1, const ImVec2 & p2) {
    return ImVec2(p1.x + p2.x, p1.y + p2.y);
}

struct ImRect {
    ImRect() = default;
    ImRect(const ImVec2 & tl, const ImVec2 & br) : top_left_{tl}, bottom_right_{br} {}
    ImRect(float x_lo, float y_lo, float x_hi, float y_hi) : top_left_{x_lo, y_lo}, bottom_right_{x_hi, y_hi} {}

    static ImRect from_xy_span(const ImVec2 & x_span, const ImVec2 & y_span) {
        return ImRect(ImVec2{x_span.x, y_span.x}, ImVec2{x_span.y, y_span.y});
    }

    std::pair<float, float> x_span() const { return std::make_pair(top_left_.x, bottom_right_.x); }
    std::pair<float, float> y_span() const { return std::make_pair(top_left_.y, bottom_right_.y); }

    const ImVec2 & top_left() const { return top_left_; }
    const ImVec2 & bottom_right() const { return bottom_right_; }

    float x_lo() const { return top_left_.x; }
    float x_hi() const { return bottom_right_.x; }
    float y_lo() const { return top_left_.y; }
    float y_hi() const { return bottom_right_.y; }

    float width() const { return bottom_right_.x - top_left_.x; }
    float height() const { return bottom_right_.y - top_left_.y; }

    float x_mid() const { return 0.5 * (top_left_.x + bottom_right_.x); }
    float y_mid() const { return 0.5 * (top_left_.y + bottom_right_.y); }

    ImVec2 bottom_left() const { return ImVec2(x_lo(), y_hi()); }
    ImVec2 top_right() const { return ImVec2(x_hi(), y_lo()); }

    ImRect with_x_span(float x0, float x1) const {
        return ImRect(ImVec2(x0, top_left_.y), ImVec2(x1, bottom_right_.y));
    }
    ImRect with_y_span(float y0, float y1) const {
        return ImRect(ImVec2(top_left_.x, y0), ImVec2(bottom_right_.x, y1));
    }

    ImRect within_margin(const ImRect & margin) const {
        return ImRect(this->x_lo() + margin.x_lo(),
                      this->y_lo() + margin.y_lo(),
                      this->x_hi() - margin.x_hi(),
                      this->y_hi() - margin.y_hi());
    }

    ImRect within_right_margin(float dx) const {
        return ImRect(this->x_lo(), this->y_lo(), this->x_hi() - dx, this->y_hi());
    }

    ImRect within_top_margin(float dy) const {
        return ImRect(this->x_lo(), this->y_lo() + dy, this->x_hi(), this->y_hi());
    }

    ImRect within_bottom_margin(float dy) const {
        return ImRect(this->x_lo(), this->y_lo(), this->x_hi(), this->y_hi() - dy);
    }

    ImRect translate(const ImVec2 & dist) {
        return ImRect(top_left_ + dist,
                      bottom_right_ + dist);
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect left_fraction(float p, float min_width = 0.0, float max_width = 999999.0) const {
        max_width = std::min(max_width, this->width());

        float w = std::clamp(p * this->width(), min_width, max_width);

        return ImRect(top_left_,
                      ImVec2(this->x_lo() + w, this->y_hi()));
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect right_fraction(float p, float min_width = 0.0, float max_width = 999999.0) const {
        max_width = std::min(max_width, this->width());

        float w = std::clamp(p * this->width(), min_width, max_width);

        return ImRect(ImVec2(this->x_hi() - w, this->y_lo()),
                      bottom_right_);
    }

    /** Require: 0.0 <= p <= q <= 1.0 **/
    ImRect mid_x_fraction(float p, float q) const {
        assert(p <= q);

        float w = this->width();

        return this->with_x_span(this->x_lo() + p * w,
                                 this->x_lo() + q * w);
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect top_fraction(float p, float min_height = 0.0, float max_height = 999999.0) const {
        max_height = std::min(max_height, this->height());

        float h = std::clamp(p * this->height(), min_height, max_height);

        return ImRect(top_left_,
                      ImVec2(this->x_hi(), this->y_lo() + h));
    }

    /** Require: 0.0 <= p <= 1.0 **/
    ImRect bottom_fraction(float p, float min_height = 0.0, float max_height = 999999.0) const {
        max_height = std::min(max_height, this->height());

        float h = std::clamp(p * this->height(), min_height, max_height);

        return ImRect(ImVec2(this->x_lo(), this->y_hi() - h),
                      bottom_right_);
    }

    ImVec2 top_left_{0, 0};
    ImVec2 bottom_right_{0, 0};
};

/* We need GUI to be able to fall behind true GC state, so we can animate transitions.
 * To help make this work, provide a model for GC state sufficient to drive rendering.
 */
struct GcGenerationDescription {
    GcGenerationDescription() = default;
    GcGenerationDescription(generation gen,
                            const char * name, const char * mnemonic, const char * gc_type,
                            std::uint8_t polarity,
                            std::size_t tospace_scale,
                            std::size_t before_ckp, std::size_t after_ckp,
                            std::size_t reserved, std::size_t committed, std::size_t gc_threshold)
        : name_{name}, mnemonic_{mnemonic}, gc_type_{gc_type}, polarity_{polarity},
          tospace_scale_{tospace_scale},
          before_checkpoint_{before_ckp}, after_checkpoint_{after_ckp},
          reserved_{reserved}, committed_{committed},
          gc_threshold_{gc_threshold} {}

    /** scale (in bytes) for drawing space **/
    std::size_t scale() const { return std::max(committed_, gc_threshold_); }

    /** nursery or tenured **/
    generation generation_;

    /** "nursery" or "tenured" **/
    const char * name_ = nullptr;

    /** "N" or "T" **/
    const char * mnemonic_ = nullptr;

    /** "incremental" or "full" **/
    const char * gc_type_ = nullptr;

    /** alternates between {0, 1} on each GC **/
    std::uint8_t polarity_ = 0;

    /** size of to-space in bytes represented on screen.
     *  (note however when we animate GC, space roles have already reversed,
     *   so then this will refer to old to-space = new from-space)
     **/
    std::size_t tospace_scale_ = 0;

    std::size_t before_checkpoint_ = 0;
    std::size_t after_checkpoint_ = 0;
    std::size_t reserved_ = 0;
    std::size_t committed_ = 0;
    // G_to_gc_threshold = G1_to_size + gc->config().incr_gc_threshold_;
    std::size_t gc_threshold_ = 0;
};

/* We need GUI to be able to fall behind true GC state, so we can animate transitions.
 * To help make this work, provide a model for GC state sufficient to drive rendering.
 */
struct GcStateDescription {
    using generation = xo::gc::generation;

    GcStateDescription(const GcGenerationDescription & nursery,
                       const GcGenerationDescription & tenured,
                       std::size_t gc_size,
                       std::size_t gc_committed,
                       std::size_t gc_allocated,
                       std::size_t gc_available,
                       std::size_t gc_mlog_size,
                       std::size_t total_promoted,
                       std::size_t total_n_mutation
        );

    const GcGenerationDescription & get_gendescr(generation g) const { return gen_state_v_[gen2int(g)]; }

    std::array<GcGenerationDescription, static_cast<std::size_t>(generation::N)> gen_state_v_;

    /** see @ref GC::size **/
    std::size_t gc_size_ = 0;
    /** see @ref GC::committed **/
    std::size_t gc_committed_ = 0;
    /** see @ref GC::allocated **/
    std::size_t gc_allocated_ = 0;
    /** see @ref GC::available **/
    std::size_t gc_available_ = 0;
    /** see @ref GC::mlog_size **/
    std::size_t gc_mlog_size_ = 0;

    /** see @ref GcStatistics::total_promoted_ **/
    std::size_t total_promoted_ = 0;
    /** see @ref GcStatistics::n_mutation_ **/
    std::size_t total_n_mutation_ = 0;
};

GcStateDescription::GcStateDescription(const GcGenerationDescription & nursery,
                                       const GcGenerationDescription & tenured,
                                       std::size_t gc_size,
                                       std::size_t gc_committed,
                                       std::size_t gc_allocated,
                                       std::size_t gc_available,
                                       std::size_t gc_mlog_size,
                                       std::size_t total_promoted,
                                       std::size_t total_n_mutation)
    : gc_size_{gc_size},
      gc_committed_{gc_committed},
      gc_allocated_{gc_allocated},
      gc_available_{gc_available},
      gc_mlog_size_{gc_mlog_size},
      total_promoted_{total_promoted},
      total_n_mutation_{total_n_mutation}
{
    gen_state_v_[gen2int(generation::nursery)] = nursery;
    gen_state_v_[gen2int(generation::tenured)] = tenured;
}

using xo::gp;
using xo::up;
using xo::Object;
using xo::obj::List;
using xo::obj::Integer;
using xo::rng::xoshiro256ss;
using xo::rng::Seed;

/** details of a single copy event performed by GC **/
struct GcCopyDetail {
    GcCopyDetail(std::size_t z,
                 generation src, std::size_t src_offset, std::size_t src_space_z,
                 generation dest, std::size_t dest_offset, std::size_t dest_z)
        : z_{z},
          src_gen_{src}, src_offset_{src_offset}, src_space_z_{src_space_z},
          dest_gen_{dest}, dest_offset_{dest_offset}, dest_z_{dest_z}
        {}

    /** object size in bytes **/
    std::size_t z_ = 0;
    /** source location **/
    generation src_gen_;
    /** offset from start of allocator **/
    std::size_t src_offset_ = 0;
    /** size of source space.  could store this separately **/
    std::size_t src_space_z_ = 0;

    /** destination location **/
    generation dest_gen_;
    /** offset from start of allocator **/
    std::size_t dest_offset_ = 0;
    /** size of destination space. (could store this separately). **/
    std::size_t dest_z_ = 0;
};

struct AppState {
    using GC = xo::gc::GC;

public:
    AppState();

    std::size_t nursery_tospace_scale() const;
    std::size_t tenured_tospace_scale() const;
    GcStateDescription snapshot_gc_state() const;

    void generate_random_mutation();
    void generate_random_mutations();

public:
    int alloc_per_cycle_ = 1;
    /** if gc triggered, remembers which whether incremental or full **/
    generation upto_ = generation::nursery;
    up<GC> gc_;
    std::size_t next_int_ = 0;
    std::size_t next_root_ = 0;
    std::vector<gp<Object>> gc_root_v_{100};
    Seed<xoshiro256ss> seed_;
    xoshiro256ss rng_{seed_};
    /** remember details for each object copied by GC, so we can animate **/
    std::vector<GcCopyDetail> copy_detail_v_;
    /** max offset for destination, given copied to nursery **/
    std::size_t copy_detail_max_nursery_dest_offset_ = 0;
    std::size_t copy_detail_nursery_dest_size_ = 0;
    std::size_t copy_detail_max_tenured_dest_offset_ = 0;
    std::size_t copy_detail_tenured_dest_size_ = 0;
};

AppState::AppState()
{
    this->gc_ = (GC::make
           (
               {.initial_nursery_z_ = 1024*1024,
                .initial_tenured_z_ = 1024*1024*1024,
                .incr_gc_threshold_ = 16*1024,
                .full_gc_threshold_ = 128*1024,
                .stats_flag_ = true,
                .debug_flag_ = false}));

    Object::mm = gc_.get();

    for (auto & x: gc_root_v_)
        gc_->add_gc_root(x.ptr_address());

    gc_->disable_gc();
}

std::size_t
AppState::nursery_tospace_scale() const {
    std::size_t N1_to_size = gc_->nursery_before_checkpoint();
    std::size_t N_to_committed = gc_->nursery_to_committed();
    std::size_t N_to_incr_gc_threshold = N1_to_size + gc_->config().incr_gc_threshold_;
    std::size_t N_to_scale = std::max(N_to_committed, N_to_incr_gc_threshold);

    return N_to_scale;
}

std::size_t
AppState::tenured_tospace_scale() const {
    std::size_t T1_to_size = gc_->tenured_before_checkpoint();
    std::size_t T_to_committed = gc_->tenured_to_committed();
    std::size_t T_to_full_gc_threshold = T1_to_size + gc_->config().full_gc_threshold_;
    std::size_t T_to_scale = std::max(T_to_committed, T_to_full_gc_threshold);

    return T_to_scale;
}

GcStateDescription
AppState::snapshot_gc_state() const {
    /** NOTE: this gets invoked before GC gets opportunity to run.
     *        in the event that GC does run, from- and to- spaces will
     *        have been reversed (near beginning of GC phase)
     *
     *        This means that nursery_to_reserved() etc. actually refer to from-space
     *        *during gc*
     **/

    // TOOD: may want to use GC::get_gc_statistics() to replace multiple round trips

    return GcStateDescription(GcGenerationDescription
                              (generation::nursery,
                               "nursery",
                               "N",
                               "incremental",
                               gc_->nursery_polarity(),
                               this->nursery_tospace_scale(),
                               gc_->nursery_before_checkpoint(),
                               gc_->nursery_after_checkpoint(),
                               gc_->nursery_to_reserved(),
                               gc_->nursery_to_committed(),
                               gc_->nursery_before_checkpoint() + gc_->config().incr_gc_threshold_),
                              GcGenerationDescription
                              (generation::tenured,
                               "tenured",
                               "T",
                               "full",
                               gc_->tenured_polarity(),
                               this->tenured_tospace_scale(),
                               gc_->tenured_before_checkpoint(),
                               gc_->tenured_after_checkpoint(),
                               gc_->tenured_to_reserved(),
                               gc_->tenured_to_committed(),
                               gc_->tenured_before_checkpoint() + gc_->config().full_gc_threshold_),

                              gc_->size(),
                              gc_->committed(),
                              gc_->allocated(),
                              gc_->available(),
                              gc_->mlog_size(),
                              gc_->native_gc_statistics().total_promoted_,
                              gc_->native_gc_statistics().n_mutation_
        );
}

void
AppState::generate_random_mutation() {
    if (rng_() % 1000 > (5 * 1000) / 7) {
        /* p=16% integer */
        gc_root_v_[next_root_++] = Integer::make(next_int_);
    } else if (rng_() % 1000 > (3 * 1000) / 7) {
        /* p=16% cons */
        gp<Object> random_car = gc_root_v_.at(rng_() % gc_root_v_.size());
        if (random_car.is_null())
            random_car = List::nil;

        /* this will always incorporate existing list as tail of new list */
        gp<List> random_cdr = List::from(gc_root_v_[next_root_]);
        if (random_cdr.is_null())
            random_cdr = List::nil;

        gp<List> random_cons = List::cons(random_car, random_cdr);

        gc_root_v_[next_root_++] = random_cons;
    } else if (rng_() % 1000 > (0 * 1000) / 7) {
        /* p=24% mutation */
        gp<List> random_list = List::from(gc_root_v_.at(rng_() % gc_root_v_.size()));
        if (!random_list.is_null()) {
            if (rng_() % 2 == 0) {
                /* pick up some random object, assign as head */
                gp<Object> random_car = gc_root_v_.at(rng_() % gc_root_v_.size());
                random_list->assign_head(random_car);
            } else {
                /* pick up some random object; if List, assign tail as tail */
                gp<List> random_cdr = List::from(gc_root_v_.at(rng_() % gc_root_v_.size()));
                if (!random_cdr.is_null() && !random_cdr->is_nil())
                    random_list->assign_rest(random_cdr->rest());
            }
        }
    }
    if (next_root_ >= gc_root_v_.size())
        this->next_root_ = 0;
}

void
AppState::generate_random_mutations() {
    for (int i = 0; i < this->alloc_per_cycle_; ++i) {
        this->generate_random_mutation();
    }
}

void
draw_filled_rect_with_label(const char * text,
                            const char * tooltip,
                            const ImRect & rect,
                            ImU32 fillcolor,
                            ImU32 textcolor,
                            ImDrawList * draw_list)
{
    draw_list->AddRectFilled(rect.top_left(),
                             rect.bottom_right(),
                             fillcolor);

    if ((rect.width() > 0.0) && (rect.height() > 0.0)) {
        ImGui::SetCursorScreenPos(rect.top_left());
        ImGui::InvisibleButton("ttbutton", ImVec2(rect.width(), rect.height()));
        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip("%s", tooltip);
        }
    }

    if (text) {
        auto textz = ImGui::CalcTextSize(text);

        /* N1 can be empty: but in that case don't bother to label it */
        if (textz.x < rect.width()) {
            draw_list->AddText(ImVec2(rect.x_mid() - 0.5 * textz.x,
                                      rect.y_mid() - 0.5 * textz.y),
                               textcolor,
                               text);
        }
    }
}

void
draw_filled_rect(const char * tooltip,
                 const ImRect & rect,
                 ImU32 fillcolor,
                 ImDrawList * draw_list)
{
    draw_filled_rect_with_label(nullptr,
                                tooltip,
                                rect,
                                fillcolor,
                                IM_COL32(255, 255, 255, 255),
                                draw_list);
}


using xo::gc::GC;
using xo::gc::GcStatisticsExt;
using xo::gc::GcStatisticsHistory;
using xo::gc::GcStatisticsHistoryItem;
using xo::xtag;
using xo::scope;
using xo::flatstring;
using std::size_t;

/** @class GenerationLayout
 *  @brief layout for displaying a single collector generation.
 *
 *  @text
 *       reserved: xxx committed: xxxx G1: xxx bytes G0: xxx bytes
 *       +--------------------------+--------------------+-------------------------+
 *    to |            G1            |         G0         |                         |
 *       +--------------------------+--------------------+-------------------------+
 *                                                                                   Mem: 28k
 *       +-------------------------------------------------------------------------+
 *  from |                                                                         |
 *       +-------------------------------------------------------------------------+
 *                                                                    ^
 *  layout elements:
 *
 *  <-a-><------------------------------ mem_w ------------------------------------><---b---->
 *       <------------------------- ngc_w ---------------------------->
 *       <--------- G1_w ----------> <------- G0_w ----->
 *
 *  a (lh_text_dx): width for left-hand-side text
 *  b (rh_text_dx): width for right-hand-side text
 *  mem_w:          width for contiguous committed memory
 *  ngc_w:          location (relative to start of GC memory range) of next-collection trigger
 *  G1_w:           width for occupied memory that has survived one GC in this space
 *  G0_w:           width for memory allocated since last GC
 *
 *  @endtext
 *
 **/
struct GenerationLayout {
    GenerationLayout() = default;
    GenerationLayout(const GcGenerationDescription & gendescr, const ImRect & br, bool with_labels);

    const char * name() const { return gendescr_.name_; }
    const char * mnemonic() const { return gendescr_.mnemonic_; }
    const char * gc_type() const { return gendescr_.gc_type_; }
    std::size_t to_G1_size() const { return gendescr_.before_checkpoint_; }
    std::size_t to_G0_size() const { return gendescr_.after_checkpoint_; }
    std::size_t to_gc_threhsold() const { return gendescr_.gc_threshold_; }
    float to_scale() const {
        /** note: deliberate size_t->float conversion here **/
        return gendescr_.scale();
    }
    ImRect to_g1_rect() const {
        return mem_rect_to_.left_fraction(this->to_G1_size() / this->to_scale());
    }
    ImRect to_g0_rect() const {
        return mem_rect_to_.mid_x_fraction(this->to_G1_size() / this->to_scale(),
                                           (this->to_G1_size() + this->to_G0_size()) / this->to_scale());
    }
    ImRect to_alloc_rect() const {
        return mem_rect_to_.left_fraction((this->to_G1_size() + this->to_G0_size()) / this->to_scale());
    }

    ImRect from_alloc_rect() const {
        /* use the same sizing as for source generation */
        return mem_rect_from_.left_fraction((this->to_G1_size() + this->to_G0_size()) / this->to_scale());
    }

    /** size-related statistics for generation to be displayed **/
    GcGenerationDescription gendescr_;

    /** bounding rectangle.  all drawing for generation display will be inside this rectanglge **/
    ImRect bounding_rect_;

    /** true iff text labels enabled **/
    bool with_labels_ = false;

    /** text height in screen units **/
    float text_dy_ = 0.0;

    /** chart rectangle.  bounding rectangle less room for headline text **/
    ImRect chart_withlabel_rect_;
    ImRect chart_nolabel_rect_;

    /** text for RH label.  something like "N: 28k/40k" **/
    flatstring<80> rh_text_;

    /** width of .rh_text in screen units **/
    float rh_text_dx_ = 0.0;

    /** rectangle representing from-space memory range **/
    ImRect mem_rect_from_;
    /** rectangle representing to-space memory range **/
    ImRect mem_rect_to_;
};

GenerationLayout::GenerationLayout(const GcGenerationDescription & gendescr,
                                   const ImRect & br,
                                   bool with_labels)
    : gendescr_{gendescr}, bounding_rect_{br}, with_labels_{with_labels}
{
    this->text_dy_ = ImGui::CalcTextSize("SAMPLE TEXT").y;

    if (with_labels_) {
        snprintf(this->rh_text_.data(), rh_text_.capacity(),
                 "%s: %luk",
                 gendescr_.mnemonic_,
                 std::max(gendescr_.gc_threshold_, gendescr_.committed_) / 1024);
        rh_text_.ensure_final_null();

        auto textz = ImGui::CalcTextSize(rh_text_.c_str());

        /* allow margin between rh edge of mem range and beginning of label */
        this->rh_text_dx_ = 5 + textz.x;
    } else {
        this->rh_text_dx_ = 0.0;
    }

    if (with_labels_) {
        this->chart_withlabel_rect_ = bounding_rect_.within_top_margin(text_dy_ + 2);
        this->chart_nolabel_rect_   = chart_withlabel_rect_.within_right_margin(rh_text_dx_);
    } else {
        this->chart_withlabel_rect_ = bounding_rect_;
        this->chart_nolabel_rect_ = bounding_rect_;
    }

    this->mem_rect_from_ = chart_nolabel_rect_.top_fraction(0.45);
    this->mem_rect_to_   = chart_nolabel_rect_.bottom_fraction(0.45);

    if (gendescr_.polarity_ == 1)
        std::swap(this->mem_rect_from_, this->mem_rect_to_);
}

/**
 *  @p polarity  0 -> draw from-space above to-space; 1 -> draw from-space below to-space
 *  @p p_x1    On exit *p_x1 contains x-coord of right-hand edge of rectangle
 *             depicting potential memory range
 **/
void
draw_generation(const GenerationLayout & layout,
                ImDrawList * draw_list)
{
    //scope log(XO_DEBUG(with_labels));

    using xo::gc::generation;

    /* next GC trigges when G0_to_size reaches this threshold */
    std::size_t G_to_gc_threshold = 0;

    G_to_gc_threshold = layout.gendescr_.gc_threshold_;

    std::size_t G_to_scale = layout.gendescr_.scale();

    /*
     * committed: G_to_committed
     * G1:  G1_to_size
     * G0:  G0_to_size
     * ckp: G1_to_size
     * ngc: G_to_gc_threshold
     *
     *   <----------------------------- committed --------------------------->
     *   <------------------ used ------------------> <-------- free -------->
     *   <------- G1 --------> <-------- G0 -------->
     *  |NNNNNNNNNNNNNNNNNNNNN|nnnnnnnnnnnnnnnnnnnnnn|________________________|
     *                        ^                                      ^
     *                        ckp                                    ngc
     *
     *  in screen coords:
     *
     *  horizontally:
     *
     *         rect.x_lo                                                                   rect.x_hi
     *         v                                                                           v
     *
     *          <--------------------------- display_w -----------------------------> <-+->
     *          <------------------------ ngc_w ---------------------->                 \- rh_text_dx
     *          <------- G1_w ------> <-------- G0_w ------>
     *         ^                     ^                      ^          ^             ^
     *        x0                    G0_x0                  G0_x1     ngc_w          x1
     *     *p_x0                                        *p_g0_x1                 *p_x1
     *
     *  vertically:
     *
     *      <- rect.y_lo
     *     ^
     *
     *     v
     *      <- rect.y_hi
     */

    /* e.g. N1: 34511 bytes */
    char g1_buf[255];

    ImU32 label_color = IM_COL32(255, 255, 192, 255); /*super pale yellow*/

    if (layout.with_labels_) {
        snprintf(g1_buf, sizeof(g1_buf),
                 "reserved: %lu bytes; committed: %lu bytes; %s\u2081: %lu bytes; %s\u2080: %lu bytes",
                 layout.gendescr_.reserved_,
                 layout.gendescr_.committed_,
                 layout.mnemonic(),
                 layout.gendescr_.before_checkpoint_,
                 layout.mnemonic(),
                 layout.gendescr_.after_checkpoint_);

        draw_list->AddText(layout.bounding_rect_.top_left(),
                           label_color,
                           g1_buf);
    }

    if (layout.with_labels_) {
        auto textz = ImGui::CalcTextSize(layout.rh_text_.c_str());

        draw_list->AddText(ImVec2(layout.chart_withlabel_rect_.x_hi() - textz.x,
                                  layout.chart_withlabel_rect_.y_mid() - 0.5 * textz.y),
                           label_color,
                           layout.rh_text_.c_str());
    }

    ImU32 outline_color = IM_COL32(255, 255, 255, 255); /*white*/

    /* chart rectangle */
    draw_list->AddRect(layout.mem_rect_from_.top_left(),
                       layout.mem_rect_from_.bottom_right(),
                       outline_color);

    draw_list->AddRect(layout.mem_rect_to_.top_left(),
                       layout.mem_rect_to_.bottom_right(),
                       outline_color);

    float display_w = layout.mem_rect_from_.width();
    float G1_w = display_w * layout.to_G1_size() / layout.to_scale();
    float G1_x1 = layout.mem_rect_from_.x_lo() + G1_w;
    ImRect G1_rect = layout.to_g1_rect();
    //ImRect G1_rect = layout.mem_rect_to_.left_fraction(layout.to_G1_size() / layout.to_scale());

    /* G1 (i.e. N1 or T1) */
    {
        ImU32   G1_color = IM_COL32(  0, 128,   0, 255);
        ImU32 text_color = IM_COL32(255, 255, 255, 255);

        char buf[255];

        if (layout.with_labels_)
            snprintf(buf, sizeof(buf), "%s\u2081: %luk",
                     layout.mnemonic(), layout.to_G1_size() / 1024); /* N1 / T1 */

        char tooltip[255];
        snprintf(tooltip, sizeof(tooltip),
                 "%s\u2081: %lu - %s survivor size in bytes",
                 layout.mnemonic(),
                 layout.to_G1_size(),
                 layout.name());

        draw_filled_rect_with_label(layout.with_labels_ ? buf : nullptr,
                                    tooltip,
                                    G1_rect,
                                    G1_color,
                                    text_color,
                                    draw_list);
    }

    ImRect G0_rect = layout.to_g0_rect();

    /* G0 (i.e. N0 or T0) */
    {
        ImU32   G0_color = IM_COL32( 32, 192,  32, 255);
        ImU32 text_color = IM_COL32(  0,   0,   0, 255);

        char buf[255];

        if (layout.with_labels_)
            snprintf(buf, sizeof(buf), "%s\u2080: %luk",
                     layout.mnemonic(),
                     layout.to_G0_size() / 1024); /* N(0) */

        char tooltip[255];
        snprintf(tooltip, sizeof(tooltip),
                 "%s\u2080: %lu - %s new alloc size in bytes",
                 layout.mnemonic(), layout.to_G0_size(),
                 layout.name());

        draw_filled_rect_with_label(layout.with_labels_ ? buf : nullptr,
                                    tooltip,
                                    G0_rect,
                                    G0_color,
                                    text_color,
                                    draw_list);
    }

    /* mark where next gc will trigger */
    if (layout.with_labels_) {
        const char * uparrow = reinterpret_cast<const char *>(u8"\u25b3");

        float ngc_w = (display_w * layout.gendescr_.gc_threshold_) / G_to_scale;

        auto tmp = ImGui::CalcTextSize(uparrow);
        std::size_t uparrow_w = tmp.x;
        double ngc_x = layout.chart_withlabel_rect_.x_lo() + ngc_w - uparrow_w/2.0;

        ImVec2 marker_pos(ngc_x, layout.chart_withlabel_rect_.y_hi());

        draw_list->AddText(marker_pos,
                           IM_COL32(255, 128, 128, 255) /*red*/,
                           uparrow);

        ImGui::SetCursorScreenPos(marker_pos);
        ImGui::InvisibleButton("mkbutton", tmp);
        if (ImGui::IsItemHovered()) {
            char marker_tt_buf[255];
            snprintf(marker_tt_buf, sizeof(marker_tt_buf),
                     "Next %s GC when size(%s) >= %lu bytes",
                     layout.gc_type(),
                     layout.name(),
                     layout.gendescr_.gc_threshold_);

            ImGui::SetTooltip("%s", marker_tt_buf);
        }
    }
} /*draw_generation*/

void
draw_nursery(const GcStateDescription & gcstate,
             bool with_labels,
             const ImRect & rect,
             ImDrawList * draw_list,
             GenerationLayout * p_layout)
{
    using xo::gc::generation;

    const GcGenerationDescription & gendescr = gcstate.get_gendescr(generation::nursery);

    GenerationLayout layout(gendescr, rect, with_labels);

    draw_generation(layout, draw_list);

    if (p_layout)
        *p_layout = layout;
}

void
draw_tenured(const GcStateDescription & gcstate,
             bool with_labels,
             const ImRect & rect,
             ImDrawList * draw_list,
             GenerationLayout * p_layout)
{
    using xo::gc::generation;

    const GcGenerationDescription & gendescr = gcstate.get_gendescr(generation::tenured);

    GenerationLayout layout(gendescr, rect, with_labels);

    draw_generation(layout, draw_list);

    if (p_layout)
        *p_layout = layout;
}

/** for history tooltip, choose which statistic to headline **/
enum class gc_history_headline {
    survive,
    promote,
    persist,
    garbage0,
    garbage1,
    garbageN,
    N
};

xo::flatstring<512>
write_gc_history_tooltip(gc_history_headline headline,
                         const GcStatisticsHistoryItem & stats)
{
    xo::flatstring<512> retval;

    xo::flatstring<512> headline_str;
    switch (headline) {
    case gc_history_headline::survive:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "survive: %lu: bytes surviving 1st GC after allocation",
                 stats.survive_z_);
        break;
    case gc_history_headline::promote:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "promote: %lu: bytes surviving 2nd GC; if nursery promote to tenured",
                 stats.promote_z_);
        break;
    case gc_history_headline::persist:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "persist: %lu: bytes surviving 3+ GCs. Only non-zero for full collections",
                 stats.persist_z_);
        break;
    case gc_history_headline::garbage0:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "garbage\u2080: %lu: bytes collected on 1st GC after allocation",
                 stats.garbage0_z_);
        break;
    case gc_history_headline::garbage1:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "garbage\u2081: %lu: bytes collected on 2nd GC after allocation",
                 stats.garbage1_z_);
        break;
    case gc_history_headline::garbageN:
        snprintf(headline_str.data(), headline_str.capacity(),
                 "garbage\u2099: %lu: bytes collected on 3rd or later GC after allocation",
                 stats.garbageN_z_);
        break;
    case gc_history_headline::N:
        assert(false);
        break;
    }



    snprintf(retval.data(), retval.capacity(),
             "%s\n"
             "\n"
             " gcseq: %lu\n"
             " type: %s\n"
             " alloc: %lu\n"
             " survive: %lu\n"
             " promote: %lu\n"
             " persist: %lu\n"
             " garbage\u2080: %lu\n" /*garbage0*/
             " garbage\u2081: %lu\n" /*garbage1*/
             " garbage\u2099: %lu\n" /*garbageN*/
             " effort: %lu dt: %.1lfus\n"
             " copy efficiency: %.1lf%% collection rate: %.0lf bytes/sec",
             headline_str.c_str(),
             stats.gc_seq_,
             (stats.upto_ == generation::nursery) ? "incremental" : "FULL",
             stats.new_alloc_z_,
             stats.survive_z_,
             stats.promote_z_,
             stats.persist_z_,
             stats.garbage0_z_,
             stats.garbage1_z_,
             stats.garbageN_z_,
             stats.effort_z_,
             1e-3 * stats.dt_.scale(),
             100.0 * stats.efficiency(),
             stats.collection_rate()
        );

    return retval.ensure_final_null();
}

/** stacked bar chart
 *
 *  @param gen  if @ref generation::nursery, only display nursery collections.
 *              otherwise display both
 **/
void
draw_gc_history(const GcStateDescription & gcstate,
                generation gen,
                const GcStatisticsHistory & gc_history,
                const ImRect & bounding_rect,
                bool debug_flag,
                ImDrawList * draw_list)
{
    scope log(XO_DEBUG(debug_flag));

    float lm = 10;
    float tm = 25;

    /* we're going to make a bar chart */

    /* x_scale,y_scale in GC units (i.e. bytes) */
    size_t x_scale = gc_history.capacity();
    size_t yplus_scale = 0;
    size_t yminus_scale = 0;

    float display_w = bounding_rect.width()  - lm;
    float display_h = bounding_rect.height() - tm;

    /* 1st loop: figure out max y scale */
    for (const GcStatisticsHistoryItem & stats : gc_history) {
        if ((gen == stats.upto_) || (gen == generation::tenured))
        {
            //size_t  na = stats.new_alloc_z_ - stats.survive_z_; /*new allocs, but dont' double-count survive_z*/
            size_t  sz = stats.survive_z_; /*survive 1st gc */
            size_t  pz = stats.promote_z_; /*survive 2nd gc */
            size_t psz = stats.persist_z_; /*survive 3+ gc */
            size_t g0z = stats.garbage0_z_;
            size_t g1z = stats.garbage1_z_;
            size_t gNz = stats.garbageN_z_;

            if (yplus_scale < sz + pz + psz)
                yplus_scale = sz + pz + psz;

            if (yminus_scale < g0z + g1z + gNz)
                yminus_scale = g0z + g1z + gNz;
        } else {
            ;
        }
    }

    /* y-coord of x-axis */
    float y_zero = bounding_rect.y_lo() + tm + (display_h * yplus_scale) / (yplus_scale + yminus_scale);

    float y_scale = yplus_scale + yminus_scale;

    /* width of 1 bar in screen coords */
    constexpr float c_min_bar_w = 5.0;
    float bar_w = std::max(c_min_bar_w, display_w / gc_history.capacity());

    /* 2nd loop: draw bars */
    std::size_t i = 0;
    for (const GcStatisticsHistoryItem & stats : gc_history)
    {
        if ((gen == stats.upto_) || (gen == generation::tenured))
        {
            /*
             *     ys_lo   +--+
             *             |  |   survive_z  (survived 1st GC)
             *             |  |
             *     yp_lo   +--+
             *             |  |   promote_z  (sruvived 2nd GC)
             *             |  |
             *   ypsz_lo   +--+
             *             |  |   persist_z  (survived 3+ GCs)
             *             |  |
             *    y_zero   +--+
             *             |  |   gN  (killed on 3+ GC)
             *             |  |
             *    ygN_hi   +--+
             *             |  |   g1  (killed on 2nd GC)
             *             |  |
             *    yg1_hi   +--+
             *             |  |   g0  (killed on 1st GC)
             *             |  |
             *    yg0_hi   +--+
             */

            ImU32  persist_color = IM_COL32(  0,  64, 192, 255); /*darker blue*/
            ImU32  promote_color = IM_COL32(  0, 128,   0, 255); /*darker green*/
            ImU32  survive_color = IM_COL32( 32, 192,  32, 255); /*lighter green*/
            ImU32 garbageN_color = IM_COL32(255, 128,  64, 255); /*darker orange*/
            ImU32 garbage1_color = IM_COL32(255, 192, 128, 255); /*medium orange*/
            ImU32 garbage0_color = IM_COL32(255, 255, 192, 255); /*pale yellow*/

            /* x-coordinates of bar */
            float x_lo = bounding_rect.x_lo() + lm + i * bar_w;
            float x_hi = x_lo + bar_w - 1;
            ImVec2 x_span{x_lo, x_hi};

            /* y-coordinates of persist bar (survived 3+ GCs) */
            float ypsz_lo = (y_zero
                             - (display_h * stats.persist_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::persist, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(ypsz_lo, y_zero)),
                                 persist_color,
                                 draw_list);
            }
            /* y-coordinates of promote bar (survived 2nd GC) */
            float yp_hi = ypsz_lo;
            float yp_lo = (yp_hi
                           - (display_h * stats.promote_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::promote, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(yp_lo, yp_hi)),
                                 promote_color,
                                 draw_list);
            }

            /* y-coordinates of survivor bar (survived 1st GC) */
            float ys_hi = yp_lo;
            float ys_lo = (ys_hi - (display_h * stats.survive_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::survive, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(ys_lo, ys_hi)),
                                 survive_color,
                                 draw_list);
            }

            // -----------------------------------------------------------

            /* y-coordinates of garbageN bar (killed on 3+ GC) */
            float ygN_lo = y_zero;
            float ygN_hi = (y_zero
                            + (display_h * stats.garbageN_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::garbageN, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect::from_xy_span(x_span, ImVec2(ygN_lo, ygN_hi)),
                                 garbageN_color,
                                 draw_list);
            }

            /* y-coordinates of garbage1 bar (killed on 2nd GC) */
            float yg1_lo = ygN_hi;
            float yg1_hi = (yg1_lo
                            + (display_h * stats.garbage1_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::garbage1, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect(ImVec2(x_lo, yg1_lo), ImVec2(x_hi, yg1_hi)),
                                 garbage1_color,
                                 draw_list);
            }

            /* y-coordinates of garbage0 bar (killed on 1st GC) */
            float yg0_lo = yg1_hi;
            float yg0_hi = (yg0_lo
                            + (display_h * stats.garbage0_z_ / y_scale));
            {
                xo::flatstring<512> tt = write_gc_history_tooltip(gc_history_headline::garbage0, stats);

                draw_filled_rect(tt.c_str(),
                                 ImRect(ImVec2(x_lo, yg0_lo), ImVec2(x_hi, yg0_hi)),
                                 garbage0_color,
                                 draw_list);
            }
        } else {
            /* draw nothing */
            ;
        }

        ++i;
    }

    log && log(xtag("i", i));
}

void
draw_gc_efficiency(const GcStateDescription & gcstate,
                   //generation gen,
                   const GcStatisticsHistory & gc_history,
                   const ImRect & bounding_rect,
                   bool debug_flag,
                   ImDrawList * draw_list)
{
    scope log(XO_DEBUG(debug_flag));

    float lm = 10;
    float tm = 25;

    /* we're going to make a level chart */

    /* x_scale,y_scale in GC units (i.e. bytes) */
    size_t x_scale = gc_history.capacity();
    size_t yplus_scale = 1;
    size_t yminus_scale = 0;

    float display_w = bounding_rect.width()  - lm;
    float display_h = bounding_rect.height() - tm;

#ifdef NOPE // don't need this.  y-scale is [0.0, 1.0]
    /* 1st loop: figure out max y scale */
    for (const GcStatisticsHistoryItem & stats : gc_history) {
        if ((gen == stats.upto_) || (gen == generation::tenured))
        {
            //size_t  na = stats.new_alloc_z_ - stats.survive_z_; /*new allocs, but dont' double-count survive_z*/
            size_t  sz = stats.survive_z_; /*survive 1st gc */
            size_t  pz = stats.promote_z_; /*survive 2nd gc */
            size_t psz = stats.persist_z_; /*survive 3+ gc */
            size_t g0z = stats.garbage0_z_;
            size_t g1z = stats.garbage1_z_;
            size_t gNz = stats.garbageN_z_;

            if (yplus_scale < sz + pz + psz)
                yplus_scale = sz + pz + psz;

            if (yminus_scale < g0z + g1z + gNz)
                yminus_scale = g0z + g1z + gNz;
        } else {
            ;
        }
    }
#endif

    /* y-coord of x-axis */
    float y_zero = bounding_rect.y_lo() + tm + display_h;
    //float y_zero = bounding_rect.y_lo() + tm + (display_h * yplus_scale) / (yplus_scale + yminus_scale);

    float y_scale = 1.0;

    /* width of 1 bar in screen coords */
    constexpr float c_min_bar_w = 5.0;
    float bar_w = std::max(c_min_bar_w, display_w / gc_history.capacity());

    /* TODO: use temporary arena */
    std::vector<ImVec2> line_points;
    line_points.reserve(gc_history.size());

    ImU32 average_color = IM_COL32(255, 255,  64, 255); /*solid yellow*/
    ImU32  sample_color = IM_COL32(255, 255, 255, 255); /*white*/

    /* 2nd loop: draw levels */
    std::size_t i = 0;
    for (const GcStatisticsHistoryItem & stats : gc_history)
    {
        //std::vector<ImVec2> line_points = { /* your points */ };
        //draw_list->AddPolyline(line_points.data(), line_points.size(),
        //                       IM_COL32(255, 255, 0, 255), false, 2.0f);

        float y = y_zero - display_h * stats.efficiency();
        float y_mean = y_zero - display_h * stats.average_efficiency();

        /* x-coordinates of point */
        float x = bounding_rect.x_lo() + lm + i * bar_w + 0.5 * bar_w;

        line_points.push_back(ImVec2(x, y_mean));

        draw_list->AddCircleFilled(ImVec2(x, y), 2.0f, sample_color);

        ++i;
    }

    draw_list->AddPolyline(line_points.data(),
                           line_points.size(),
                           average_color,
                           false,
                           1.0f /*line width?*/);

    log && log(xtag("i", i));
} /*draw_gc_efficiency*/


void
draw_gc_alloc_state(const GcStateDescription & gcstate,
                    const ImRect & canvas_rect,
                    ImDrawList * draw_list,
                    GenerationLayout * p_nursery_layout,
                    GenerationLayout * p_tenured_layout
                    //ImRect * p_nursery_alloc_rect,
                    //ImRect * p_tenured_alloc_rect
    )
{
    constexpr float c_est_chart_text_height = 14.0;
    constexpr float c_min_h =  7;  // chart bar height
    constexpr float c_max_h = 40;  // chart bar height

    /* bounding rectange for nursery display */
    ImRect N_space_rect = canvas_rect.top_fraction(0.5,
                                                   c_min_h + c_est_chart_text_height,
                                                   c_max_h + c_est_chart_text_height);

    //assert(N_space_rect.height() >= c_min_h + c_est_chart_text_height);
    //assert(N_space_rect.height() <= c_max_h + c_est_chart_text_height);

    /* rectangle representing allocated nursery range */
    //float N_x1 = 0.0;
    GenerationLayout N_layout;

    draw_nursery(gcstate,
                 true /*with_labels*/,
                 N_space_rect,
                 draw_list,
                 &N_layout);

    float N_x1 = N_layout.chart_nolabel_rect_.x_hi();

    if (p_nursery_layout)
        *p_nursery_layout = N_layout;

//    if (p_nursery_alloc_rect)
//        *p_nursery_alloc_rect = N_layout.to_alloc_rect();

    /* N0_to_size..N_to_scale: in bytes */
    std::size_t N_to_scale = gcstate.gen_state_v_[gen2int(generation::nursery)].tospace_scale_;

    /* display_w .. N0_h : viewportcoords */
    std::size_t display_w = canvas_rect.width();

    std::size_t x0 = canvas_rect.x_lo();
    std::size_t x1 = canvas_rect.x_hi();

    // now turn to Tenured space

    std::size_t T_to_scale = gcstate.gen_state_v_[gen2int(generation::tenured)].tospace_scale_;

    /* want to put to-scale image of nursery next to to-scale image of tenured;
     * but also want space between them.
     */
    float TplusN_to_scale = N_to_scale + T_to_scale;
    /* space between T, N images */
    float TplusN_spacer = 10;

    /* bounding rectange for tenured display */
    ImRect T_space_rect = (canvas_rect
                           .within_bottom_margin(c_est_chart_text_height)
                           .bottom_fraction(0.5,
                                            c_min_h + c_est_chart_text_height,
                                            c_max_h + c_est_chart_text_height));

    if (N_space_rect.y_hi() > T_space_rect.y_lo()) {
        T_space_rect = T_space_rect.translate(ImVec2(0, N_space_rect.y_hi() - T_space_rect.y_lo()));
    }

    assert(T_space_rect.y_lo() >= N_space_rect.y_hi());

    /* for smaller image of nursery */
    //std::size_t t_y0 = canvas_rect.y_lo() + 70 + alloc_height + 20;

    /* for side-by-side tenured + nursery, with both on same scale
     * 2nd term is horiz space used for N label like 'Mem: 28k'
     */
    std::size_t adj_display_w = display_w - (N_space_rect.x_hi() - N_x1);

    /* bounding rectangle for secondary nursery display */
    ImRect np_rect(ImVec2(x0 + (adj_display_w * (T_to_scale/TplusN_to_scale)),
                          T_space_rect.y_lo() + c_est_chart_text_height),
                   ImVec2(x0 + adj_display_w,
                          T_space_rect.y_hi()));

    // redraw nursery to same scale as tenured
    {
        draw_list->AddLine(N_space_rect.bottom_left(), np_rect.top_left(),
                           IM_COL32(128, 128, 128, 255) /*grey*/);
        draw_list->AddLine(ImVec2(N_x1, N_space_rect.y_hi()), np_rect.top_right(),
                           IM_COL32(128, 128, 128, 255) /*grey*/);

        draw_nursery(gcstate,
                     false /*no labels*/,
                     np_rect,
                     draw_list,
                     nullptr);
    }

    /* rectangle representing allocated tenured range */
    GenerationLayout T_layout;

    draw_tenured(gcstate,
                 true /*with labels*/,
                 ImRect(ImVec2(x0, T_space_rect.y_lo()),
                        ImVec2(x0 + (adj_display_w * (T_to_scale/TplusN_to_scale)) - TplusN_spacer,
                               T_space_rect.y_hi())),
                 draw_list,
                 &T_layout);

    if (p_tenured_layout)
        *p_tenured_layout = T_layout;

//    if (p_tenured_alloc_rect)
//        *p_tenured_alloc_rect = T_layout.to_alloc_rect();

} /*draw_gc_alloc_state*/

void
draw_gc_state(const AppState & app_state,
              const GcStateDescription & gcstate,
              const ImRect & canvas_rect,
              ImDrawList * draw_list,
              GenerationLayout * p_nursery_layout,
              GenerationLayout * p_tenured_layout,
              //ImRect * p_nursery_alloc_rect,
              //ImRect * p_tenured_alloc_rect,
              ImRect * p_history_rect)
{
    // draw stuff
    draw_list->AddRect(canvas_rect.top_left(),
                       canvas_rect.bottom_right(),
                       IM_COL32(255, 255, 255, 255));

    /* TODO: does this reset coord space? */
    ImRect alloc_rect;
    {
        ImGui::BeginChild("top pane", ImVec2(0, 105), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY);

        alloc_rect = ImRect(canvas_rect.top_left() + ImGui::GetWindowContentRegionMin(),
                            canvas_rect.top_left() + ImGui::GetWindowContentRegionMax());
        ImRect draw_rect = alloc_rect.within_margin(ImRect(50, 10, 70, 10));

        draw_list->PushClipRect(draw_rect.top_left(), draw_rect.bottom_right());

        draw_gc_alloc_state(gcstate,
                            draw_rect,
                            draw_list,
                            p_nursery_layout,
                            p_tenured_layout
                            //p_nursery_alloc_rect,
                            //p_tenured_alloc_rect
            );

        draw_list->PopClipRect();

        ImGui::EndChild();
    }

    ImRect history_rect;
    {
        ImGui::BeginChild("left pane", ImVec2(800, 0), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX);

        /* history below alloc area */
        history_rect = ImRect(alloc_rect.bottom_left() + ImGui::GetWindowContentRegionMin(),
                              alloc_rect.bottom_left() + ImGui::GetWindowContentRegionMax());

         if (p_history_rect)
             *p_history_rect = history_rect;

         draw_list->PushClipRect(history_rect.top_left(), history_rect.bottom_right());

         float lm = 50;
         float rm = 70;
         float tm = 10;
         std::size_t x0 = history_rect.x_lo() + lm;
         std::size_t x1 = history_rect.x_hi() - rm;
         std::size_t h_y0 = history_rect.y_lo() + tm;

         /* just incremental (nursery) collections */
         ImRect incremental_rect = history_rect.top_fraction(0.33);

         draw_gc_history(gcstate,
                         generation::nursery,
                         app_state.gc_->gc_history(),
                         incremental_rect,
                         false /*debug_flag*/,
                         draw_list);

         /* just full (nursery+tenured) collections */
         ImRect full_rect = history_rect.bottom_fraction(0.67).top_fraction(0.5);

         /* both nursery + full collections */
         draw_gc_history(gcstate,
                         generation::tenured,
                         app_state.gc_->gc_history(),
                         full_rect,
                         false /*debug_flag*/,
                         draw_list);

         ImRect efficiency_rect = history_rect.bottom_fraction(0.67).bottom_fraction(0.5);

         draw_gc_efficiency(gcstate,
                            app_state.gc_->gc_history(),
                            efficiency_rect,
                            false /*debug_flag*/,
                            draw_list);

         draw_list->PopClipRect();

         ImGui::EndChild();
    }

    ImGui::Text("placeholder text");

    /* BeginChild() again ? */

#ifdef NOPE
    draw_list->AddCircleFilled(ImVec2(canvas_p0.x + 50, canvas_p0.y + 50),
                               30.0f, IM_COL32(255, 0, 0, 255));

    draw_list->AddText(ImVec2(canvas_p0.x + 10, canvas_p0.y + 10),
                       IM_COL32(255, 255, 255, 255), "Hello 2D!");
#endif
}

struct DrawState;

struct AnimateGcCopyCb : public xo::gc::GcCopyCallback {
    using generation = xo::gc::generation;

    explicit AnimateGcCopyCb(AppState * appstate, DrawState * drawstate)
        : p_app_state_{appstate}, p_draw_state_{drawstate} {}

    virtual void notify_gc_copy(std::size_t z,
                                const void * src_addr, const void * dest_addr,
                                generation src_gen, generation dest_gen);

    AppState * p_app_state_ = nullptr;
    DrawState * p_draw_state_ = nullptr;
};

enum class draw_state_type {
    alloc,
    animate_gc
};

struct DrawState {
    up<xo::gc::GcCopyCallback> make_gc_copy_animation(AppState * app_state) {
        return std::make_unique<AnimateGcCopyCb>(app_state, this);
    }

    draw_state_type state_type_ = draw_state_type::alloc;

    /** budgeted time period over which to animate gc copy **/
    int animate_copy_budget_ms_ = 2000;
    /** start time of current copy animation **/
    std::chrono::steady_clock::time_point animate_copy_t0_;
    /** when animating copy step, display objects from AppState::copy_detail_v_[i]
     *  where i < .animate_copy_hi_ / 100 * AppState::copy_detail_v_.size()
     **/
    float animate_copy_hi_pct_ = 0;

    ImDrawList * gcw_draw_list_ = nullptr;

    /** draw area **/
    ImVec2 gcw_canvas_p0_;
    ImVec2 gcw_canvas_p1_;

    /** layout for nursery display **/
    GenerationLayout gcw_nursery_layout_;
    /** layout for tenured display **/
    GenerationLayout gcw_tenured_layout_;

    /** rect displaying gc history (strip charts) **/
    ImRect gcw_history_rect_;
};

ImRect map_src_alloc_to_screen(const GcCopyDetail & copy_detail,
                               const ImRect & space_rect)
{
    // TODO: methods on copy_detail / and/or ImPoint

    auto [x_coord_lo, x_coord_hi] = space_rect.x_span();

    double w0 = copy_detail.src_offset_ / static_cast<double>(copy_detail.src_space_z_);
    float src0_x = ((1.0 - w0) * x_coord_lo) + (w0 * x_coord_hi);

    double w1 = ((copy_detail.src_offset_ + copy_detail.z_)
                 / static_cast<double>(copy_detail.src_space_z_));
    float src1_x = ((1.0 - w1) * x_coord_lo) + (w1 * x_coord_hi);

    return space_rect.with_x_span(src0_x, src1_x);
}

ImRect map_dest_alloc_to_screen(const GcCopyDetail & copy_detail,
                                const ImRect & space_rect)
{
    /* for from-space, want to use full width of memory space */

    auto [x_coord_lo, x_coord_hi] = space_rect.x_span();

    // dest_space_z_ ?

    double w0 = copy_detail.dest_offset_ / static_cast<double>(copy_detail.dest_z_);
    float dest0_x = ((1.0 - w0) * x_coord_lo) + (w0 * x_coord_hi);

    double w1 = ((copy_detail.dest_offset_ + copy_detail.z_)
                 / static_cast<double>(copy_detail.dest_z_));
    float dest1_x = ((1.0 - w1) * x_coord_lo) + (w1 * x_coord_hi);

    return space_rect.with_x_span(dest0_x, dest1_x);
}

/* editor bait: animate_copy() */
void animate_gc_copy(const AppState & app_state,
                     const DrawState & draw_state,
                     ImDrawList * draw_list)
{
    /* NOTE: this only runs during GC copy.
     *       draw_state.gcw_nursery_layout_ and gcw_tenured_layout_
     *       are taken from snapshots made before GC began,
     *       ergo before to/from spaces were swapped
     */
    ImRect nursery_src_rect = draw_state.gcw_nursery_layout_.to_alloc_rect();
    ImRect tenured_src_rect = draw_state.gcw_tenured_layout_.to_alloc_rect();

    ImRect nursery_dest_rect = draw_state.gcw_nursery_layout_.mem_rect_from_;
    ImRect tenured_dest_rect;

    if (app_state.upto_ == generation::nursery) {
        tenured_dest_rect = draw_state.gcw_tenured_layout_.mem_rect_to_;
    } else {
        tenured_dest_rect = draw_state.gcw_tenured_layout_.mem_rect_from_;
    }

    std::size_t n_copy = app_state.copy_detail_v_.size();
    /* grade from black-to-white between lo_copy and hi_copy.
     * Note we allow animate_copy_hi_pct_ > 100.0
     */
    float lo_copy = 0.01 * std::max(0.0, draw_state.animate_copy_hi_pct_ - 14.0) * n_copy;
    float hi_copy = 0.01 * draw_state.animate_copy_hi_pct_ * n_copy;

    /* remember max copy offset seen in {nursery, tenured} space respectively,
     * so we can label it
     */
    std::size_t last_nursery_dest_offset = 0;
    ImRect last_nursery_dest_rect;
    std::size_t first_tenured_dest_offset = 0;
    ImRect first_tenured_dest_rect;
    std::size_t last_tenured_dest_offset = 0;
    ImRect last_tenured_dest_rect;

    std::size_t i_copy = 0;
    for (const auto & copy_detail : app_state.copy_detail_v_) {
        /* cutout for each copied object */
        {
            float wt = (i_copy > lo_copy) ? static_cast<float>(i_copy) / hi_copy : 0.0;
            /* grey fading to black */
            //ImU32 color = IM_COL32(wt*128, 64+wt*64, wt*128, 255);
            ImU32 color = IM_COL32( 96, 224, 255, 255);

            ImRect src_rect;

            if (copy_detail.src_gen_ == generation::nursery) {
                src_rect = map_src_alloc_to_screen(copy_detail, nursery_src_rect);
            } else {
                src_rect = map_src_alloc_to_screen(copy_detail, tenured_src_rect);
            }

            draw_list->AddRectFilled(src_rect.top_left(),
                                     src_rect.bottom_right(),
                                     color);
        }

        if (copy_detail.dest_gen_ == generation::nursery) {
            last_nursery_dest_rect = map_dest_alloc_to_screen(copy_detail, nursery_dest_rect);
            last_nursery_dest_offset = copy_detail.dest_offset_;
        } else if (copy_detail.dest_gen_ == generation::tenured) {
            last_tenured_dest_rect = map_dest_alloc_to_screen(copy_detail, tenured_dest_rect);
            last_tenured_dest_offset = copy_detail.dest_offset_;

            if (first_tenured_dest_rect.width() == 0) {
                first_tenured_dest_rect = last_tenured_dest_rect;
                first_tenured_dest_offset = copy_detail.dest_offset_;
            }
        }

        if (++i_copy >= hi_copy) {
            break;
        }
    }

    if (last_nursery_dest_rect.width() > 0.0) {
        //ImU32 color = IM_COL32(64, 255, static_cast<int>(64 + (128 * wt)), 255);
        ImU32 color = IM_COL32(  0, 96, 192, 255);

        draw_list->AddRectFilled(nursery_dest_rect.top_left(),
                                 last_nursery_dest_rect.bottom_right(),
                                 color);

        char buf[255];
        snprintf(buf, sizeof(buf), "N\u2081: %luk", last_nursery_dest_offset / 1024);

        auto textz = ImGui::CalcTextSize(buf);

        ImU32  text_color = IM_COL32(255, 255, 255, 255); /*black*/
        ImVec2 text_pos   = ImVec2(0.5 * (nursery_dest_rect.x_lo()
                                          + last_nursery_dest_rect.x_hi()
                                          - textz.x),
                                   nursery_dest_rect.y_mid() - 0.5 * textz.y);

        if (text_pos.x < nursery_dest_rect.x_lo())
            text_pos.x = nursery_dest_rect.x_lo() + 2;
        else if (text_pos.x < nursery_dest_rect.x_lo() + 0.5 * textz.x)
            text_pos.x = nursery_dest_rect.x_lo() + 0.5 * textz.x;

        draw_list->AddText(text_pos, text_color, buf);

    }

    if (last_tenured_dest_rect.width() > 0.0) {
        ImU32 color = IM_COL32(  0,  96, 192, 255);

        draw_list->AddRectFilled(first_tenured_dest_rect.top_left(),
                                 last_tenured_dest_rect.bottom_right(),
                                 color);

        char buf[255];
        snprintf(buf, sizeof(buf), "+%luk", (last_tenured_dest_offset - first_tenured_dest_offset) / 1024);

        auto textz = ImGui::CalcTextSize(buf);

        ImU32  text_color = IM_COL32(255, 255, 255, 255); /*black*/
        float x0 = first_tenured_dest_rect.x_lo();
        float x1 = last_tenured_dest_rect.x_hi();
        ImVec2 text_pos   = ImVec2(0.5 * (x0 + x1 - textz.x),
                                   tenured_dest_rect.y_mid() - 0.5 * textz.y);

        if (text_pos.x < x0 + 2)
            text_pos.x = x0 + 2;
        else if (text_pos.x < x0 + 0.5 * textz.x)
            text_pos.x = x0 + 0.5 * textz.x;

        draw_list->AddText(text_pos, text_color, buf);
    }
}

void
AnimateGcCopyCb::notify_gc_copy(std::size_t z,
                                const void * src_addr,
                                const void * dest_addr,
                                generation src_gen,
                                generation dest_gen)
{
    using xo::scope;
    using xo::xtag;
    using xo::gc::generation_result;
    using xo::gc::generation;
    using xo::gc::role;

    scope log(XO_DEBUG(false),
              xtag("z", z),
              xtag("src", src_addr),
              xtag("dest", dest_addr),
              xtag("src_gen", src_gen),
              xtag("dest_gen", dest_gen));

    auto [src_gen2, src_offset, src_alloc, src_size] = p_app_state_->gc_->fromspace_location_of(src_addr);

    if (src_gen2 == generation_result::not_found) {
        auto [lo, hi] = p_app_state_->gc_->nursery_span(role::from_space);

        log && log(xtag("N.from.lo", (void*)lo), xtag("N.from.hi", (void*)hi));

        assert(false);
    }

    generation src_valid_gen = xo::gc::valid_genresult2gen(src_gen2);

    auto [dest_gen2, dest_offset, _, dest_size] = p_app_state_->gc_->tospace_location_of(dest_addr);

    generation dest_valid_gen = xo::gc::valid_genresult2gen(dest_gen2);

    p_app_state_->copy_detail_v_.push_back(GcCopyDetail(z,
                                                        src_valid_gen, src_offset, src_alloc,
                                                        dest_valid_gen, dest_offset, dest_size));

    if (dest_valid_gen == generation::nursery) {
        p_app_state_->copy_detail_max_nursery_dest_offset_
            = std::max(p_app_state_->copy_detail_max_nursery_dest_offset_, dest_offset);
        p_app_state_->copy_detail_nursery_dest_size_
            = std::max(p_app_state_->copy_detail_nursery_dest_size_, dest_size);
    } else if (dest_valid_gen == generation::tenured) {
        p_app_state_->copy_detail_max_tenured_dest_offset_
            = std::max(p_app_state_->copy_detail_max_tenured_dest_offset_, dest_offset);
        p_app_state_->copy_detail_tenured_dest_size_
            = std::max(p_app_state_->copy_detail_tenured_dest_size_, dest_size);
    }

    /* will be animated across frames, see animate_gc_copy() */
}

int main(int, char **)
{
    using namespace std;

    scope log(XO_DEBUG(true));

    std::cout << "Hello, world!" << std::endl;

    SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "0");

    SDL_Init(SDL_INIT_VIDEO);

    SDL_version compiled;
    SDL_VERSION(&compiled);
    std::cerr << "SDL version: "
              << (int)compiled.major
              << "." << (int)compiled.minor
              << "." << (int)compiled.patch
              << std::endl;

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                        SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

#if 0
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS,
                        SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif

    std::cerr << "SDL video driver: " << SDL_GetCurrentVideoDriver() << std::endl;

    SDL_Window * window = SDL_CreateWindow("imgui + sdl2 + opengl",
                                           SDL_WINDOWPOS_CENTERED,
                                           SDL_WINDOWPOS_CENTERED,
                                           2000,
                                           1000,
                                           SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
    if (window) {
        std::cerr << "SDL_CreateWindow done" << std::endl;
    } else {
        std::cerr << "SDL_CreateWindow failed: [" << SDL_GetError() << "]" << std::endl;
        SDL_Quit();
        return -1;
    }

    SDL_GLContext gl_context = SDL_GL_CreateContext(window);

    int major, minor;
    SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &major);
    SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &minor);

    std::cerr << "Requested OpenGL vtersion: " << major << "." << minor << std::endl;

    if (gl_context) {
        std::cerr << "SDL_GL_CreateContext done" << std::endl;
    } else {
        std::cerr << "SDL_GL_CreateContext failed: [" << SDL_GetError() << "]" << std::endl;
        return -1;
    }

    if (SDL_GL_MakeCurrent(window, gl_context) != 0) {
        std::cerr << "SDL_GL_MakeCurrent failed: [" << SDL_GetError() << "]" << std::endl;
        SDL_GL_DeleteContext(gl_context);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }

    SDL_GL_SetSwapInterval(0); // disable vsync

    GLenum glew_status = glewInit();
    if (glew_status == GLEW_OK) {
        std::cerr << "glewInit done" << std::endl;
    } else {
        std::cerr << "glewInit failed: [" << glewGetErrorString(glew_status) << std::endl;
        return -1;
    }

    const GLubyte * version = glGetString(GL_VERSION);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    if (version) {
        std::cerr << "OpenGL version: [" << version << "]" << std::endl;
    } else {
        std::cerr << "OpenGL version not available" << std::endl;
    }

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO & io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;

    // Load noto sans font from unix environment NOTO_ONFTS_PATH
    // (see xo-umbrella2/default.nix shellHook)

    const char * fonts_path = std::getenv("DEJAVU_FONTS_PATH");

    if (fonts_path) {
        const float font_size = 14.0f;
        std::string font_path = xo::tostr(fonts_path, "/truetype/DejaVuSans.ttf");

        /* check file exists */
        std::ifstream font_in(font_path);
        if (font_in.good()) {
            std::cerr << "loading font [" << font_path << "]" << std::endl;
            ImFont * font = io.Fonts->AddFontFromFileTTF(font_path.c_str(), font_size);
            if (font) {
                std::cerr << "font loaded" << std::endl;

                ImFontConfig config;
                config.MergeMode = true;

                // latin extended chars
                static const ImWchar latin_ranges[] = {
                    0x0020, 0x00ff,  // basic latin + latin supplement
                    0x0100, 0x017f,  // latin extended-A
                    0x0180, 0x024f,  // latin extended-B
                    0x2080, 0x2099,  // subscript numerals + letters through n
                    0x25b2, 0x25b4,  // arrows
                    0,
                };
                io.Fonts->AddFontFromFileTTF(font_path.c_str(), font_size, &config, latin_ranges);
            } else {
                std::cerr << "font file load failed" << std::endl;
                std::cerr << "Fallback to default ImGui font" << std::endl;
            }
        }


    } else {
        std::cerr << "Expected DEJAVU_FONTS_PATH environment var." << std::endl;
        std::cerr << "Fallback to default ImGui font" << std::endl;
    }
    ImGui::StyleColorsDark();

    ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
    ImGui_ImplOpenGL3_Init("#version 330");

    bool show_demo_window = true;
    bool show_another_window = false;
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
    float counter_value = 0.0f;

    using xo::obj::Integer;
    using xo::obj::List;
    using xo::rng::xoshiro256ss;
    using xo::rng::Seed;
    using xo::up;
    using xo::gp;
    using xo::gc::GC;
    using xo::Object;

    AppState app_state;
    DrawState draw_state;
    /* note: during gc copy animation, this records state _before_ gc was triggered */
    GcStateDescription gcstate = app_state.snapshot_gc_state();

    app_state.gc_->add_gc_copy_callback(draw_state.make_gc_copy_animation(&app_state));

    // Main Loop
    bool done = false;

    while (!done) {
        /** on each draw cycle, app state falls into categories:
         *  1. allocation
         *     multiple draw cycles because many allocations per gc.
         *  2. garbage collection
         *     multiple draw cycles to animate copying process
         *     Settle conflict between {GC, imgui} as to who drives event loop,
         *     in favor of imgui; achieve this by copying what GC did,
         *     so that we can animate it over multiple draw cycles
         **/

        switch (draw_state.state_type_) {
        case draw_state_type::alloc:
        {
            /** generate random alloc **/
            app_state.generate_random_mutations();

            gcstate = app_state.snapshot_gc_state();

            app_state.upto_ = (app_state.gc_->is_full_gc_pending()
                               ? generation::tenured
                               : generation::nursery);

            /* GC may run here, in which case control reenters via AnimateGcCopyCb;
             * that callback captures copy details (per object!) in AppState
             */
            if (app_state.gc_->enable_gc_once()) {
                log && log(xtag("gc-type", (app_state.upto_ == generation::tenured) ? "full" : "incremental"));

                draw_state.state_type_ = draw_state_type::animate_gc;
                draw_state.animate_copy_t0_ = std::chrono::steady_clock::now();
            }

            break;
        }
        case draw_state_type::animate_gc:
        {
            /* don't update gcstate while animating,
             * that would use post-GC space sizing
             */

            break;
        }
        }

        /** poll + handle events */
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            ImGui_ImplSDL2_ProcessEvent(&event);
            if (event.type == SDL_QUIT)
                done = true;

            if (event.type == SDL_WINDOWEVENT) {
                if (event.window.event == SDL_WINDOWEVENT_CLOSE) {
                    if (event.window.windowID == SDL_GetWindowID(window))
                    {
                        done = true;
                    }
                } else if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
                    // handle resize immediately
                    int w, h;
                    SDL_GetWindowSize(window, &w, &h);
                    glViewport(0, 0, w, h);

                    break;  // to force render during resize
                }
            }
        }

        //int w, h;
        //SDL_GetWindowSize(window, &w, &h);
        //glViewport(0, 0, w, h);
        glClearColor(clear_color.x * clear_color.w,
                     clear_color.y * clear_color.w,
                     clear_color.z * clear_color.w,
                     clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw dear imgui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplSDL2_NewFrame();
        ImGui::NewFrame();

        ImGui::SetNextWindowPos(ImVec2(0, 0));
        ImGui::SetNextWindowSize(io.DisplaySize);
        ImGui::Begin("Background", nullptr,
                     ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize
                     | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus
                     | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoDecoration);
        ImGui::End();

        // 1. big demo window
        if (show_demo_window)
            ImGui::ShowDemoWindow(&show_demo_window);

        // 2. show window that we create ourselves
        {
            static int counter = 0;

            ImGui::Begin("Hello, world!");
            //ImGui::Text("This is totes useful text...");
            ImGui::Checkbox("demo window", &show_demo_window);
            //ImGui::Checkbox("second window", &show_another_window);

            ImGui::SliderInt("alloc/cycle", &app_state.alloc_per_cycle_, 1, 100);
            ImGui::SliderInt("copy animation budget", &draw_state.animate_copy_budget_ms_, 10, 10000);
            //ImGui::SliderFloat("alloc/cycle", &alloc_per_cycle, 0.0f, 1.0f);
            //ImGui::ColorEdit3("clear color", (float*)&clear_color);

            //if (ImGui::Button("Button"))
            //  ++counter;
            ImGui::NewLine(); // ImGui::SameLine()
            /* N\u2080 = N0, N\u2081 = N1 */
            ImGui::Text("alloc [%lu] avail [%lu] ",
                        gcstate.gc_allocated_,
                        gcstate.gc_available_);
            //ImGui::NewLine();
            ImGui::Text("promoted [%lu] copy animation [%lu / %lu]",
                        gcstate.total_promoted_,
                        static_cast<std::size_t>(draw_state.animate_copy_hi_pct_ * app_state.copy_detail_v_.size() / 100),
                        app_state.copy_detail_v_.size());

            ImGui::Text("mutation [%lu] mlog [%lu]",
                        gcstate.total_n_mutation_,
                        gcstate.gc_mlog_size_);

            ImGui::Text("appl average %.3f ms/frame (%.1f fps)",
                        1000.0f / io.Framerate, io.Framerate);

            ImGui::Text("layout:"
                        " nursery-src alloc rect [%.1f %.1f %.1f %.1f]"
                        " nursery-dest alloc rect [%.1f %.1f %.1f %.1f]"
                        " history rect [%.1f %.1f %.1f %.1f]",
                        draw_state.gcw_nursery_layout_.to_alloc_rect().x_lo(),
                        draw_state.gcw_nursery_layout_.to_alloc_rect().y_lo(),
                        draw_state.gcw_nursery_layout_.to_alloc_rect().x_hi(),
                        draw_state.gcw_nursery_layout_.to_alloc_rect().y_hi(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().x_lo(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().y_lo(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().x_hi(),
                        draw_state.gcw_nursery_layout_.from_alloc_rect().y_hi(),
                        draw_state.gcw_history_rect_.x_lo(),
                        draw_state.gcw_history_rect_.y_lo(),
                        draw_state.gcw_history_rect_.x_hi(),
                        draw_state.gcw_history_rect_.y_hi());
            ImGui::Text("nursery-dest copy offset [%lu] / size [%lu]"
                        " tenured-dest copy offset [%lu] / size [%lu]",
                        app_state.copy_detail_max_nursery_dest_offset_,
                        app_state.copy_detail_nursery_dest_size_,
                        app_state.copy_detail_max_tenured_dest_offset_,
                        app_state.copy_detail_tenured_dest_size_
                );

            ImDrawList * draw_list = ImGui::GetWindowDrawList();

            ImVec2 canvas_p0 = ImGui::GetCursorScreenPos();
            ImVec2 canvas_sz = ImGui::GetContentRegionAvail();
            ImVec2 canvas_p1 = ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y);

            /* stash so GC copy animation can find it */
            draw_state.gcw_draw_list_ = draw_list;
            draw_state.gcw_canvas_p0_ = canvas_p0;
            draw_state.gcw_canvas_p1_ = canvas_p1;

            draw_gc_state(app_state,
                          gcstate,
                          ImRect(canvas_p0, canvas_p1),
                          draw_list,
                          &draw_state.gcw_nursery_layout_,
                          &draw_state.gcw_tenured_layout_,
                          //nullptr,
                          //&draw_state.gcw_tenured_alloc_rect_,
                          &draw_state.gcw_history_rect_);

            if (draw_state.state_type_ == draw_state_type::animate_gc) {
                auto animate_copy_t1 = std::chrono::steady_clock::now();
                auto animate_dt = animate_copy_t1 - draw_state.animate_copy_t0_;
                float animate_fraction_spent
                    = (std::chrono::duration_cast<std::chrono::milliseconds>(animate_dt).count()
                       / static_cast<float>(draw_state.animate_copy_budget_ms_));

                draw_state.animate_copy_hi_pct_ = 100.0 * animate_fraction_spent;
                animate_gc_copy(app_state,
                                draw_state,
                                draw_list);

                /* see 25.0 constant in animate_gc_copy() */
                if (draw_state.animate_copy_hi_pct_ >= 114) {
                    draw_state.state_type_ = draw_state_type::alloc;
                    draw_state.animate_copy_hi_pct_ = 0;
                    app_state.copy_detail_v_.clear();
                    app_state.copy_detail_max_nursery_dest_offset_ = 0;
                    app_state.copy_detail_nursery_dest_size_ = 0;
                    app_state.copy_detail_max_tenured_dest_offset_ = 0;
                    app_state.copy_detail_tenured_dest_size_ = 0;
                }
            }

            ImGui::End();
        }

        // 3. another window
        if (show_another_window) {
            ImGui::Begin("another window", &show_another_window);

            ImGui::Text("hello from second window");
            if (ImGui::Button("close me"))
                show_another_window = false;

            ImGui::End();
        }

        // rendering
        ImGui::Render();

        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        SDL_GL_SwapWindow(window);
    }

    std::cerr << "cleanup.." << std::endl;

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL2_Shutdown();
    ImGui::DestroyContext();

    SDL_GL_DeleteContext(gl_context);
    SDL_DestroyWindow(window);
    SDL_Quit();

    std::cerr << "All done, goodbye..." << std::endl;

    return 0;
}

/* imgui_ex4a.cpp */
#endif //DEBUG

#include "VulkanApp.hpp"

int main() {
    MinimalImGuiVulkan app;

    try {
        app.run();
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

/* end imgui_ex4a.cpp */
