/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/** \file
 * \ingroup spinfo
 */

#include <stdio.h>
#include <string.h>

#include "MEM_guardedalloc.h"

#include "DNA_armature_types.h"
#include "DNA_collection_types.h"
#include "DNA_curve_types.h"
#include "DNA_gpencil_types.h"
#include "DNA_lattice_types.h"
#include "DNA_mesh_types.h"
#include "DNA_meta_types.h"
#include "DNA_scene_types.h"
#include "DNA_windowmanager_types.h"

#include "BLF_api.h"

#include "BLI_listbase.h"
#include "BLI_math.h"
#include "BLI_string.h"
#include "BLI_utildefines.h"

#include "BLT_translation.h"

#include "BKE_armature.h"
#include "BKE_blender_version.h"
#include "BKE_context.h"
#include "BKE_curve.h"
#include "BKE_displist.h"
#include "BKE_editmesh.h"
#include "BKE_gpencil.h"
#include "BKE_key.h"
#include "BKE_layer.h"
#include "BKE_main.h"
#include "BKE_object.h"
#include "BKE_paint.h"
#include "BKE_particle.h"
#include "BKE_pbvh.h"
#include "BKE_scene.h"
#include "BKE_subdiv_ccg.h"

#include "DEG_depsgraph_query.h"

#include "ED_info.h"

#include "UI_resources.h"

#include "GPU_capabilities.h"

#define MAX_INFO_NUM_LEN 16

typedef struct SceneStats {
  uint64_t totvert, totvertsel, totvertsculpt;
  uint64_t totedge, totedgesel;
  uint64_t totface, totfacesel, totfacesculpt;
  uint64_t totbone, totbonesel;
  uint64_t totobj, totobjsel;
  uint64_t totlamp, totlampsel;
  uint64_t tottri;
  uint64_t totgplayer, totgpframe, totgpstroke, totgppoint;
} SceneStats;

typedef struct SceneStatsFmt {
  /* Totals */
  char totvert[MAX_INFO_NUM_LEN], totvertsel[MAX_INFO_NUM_LEN], totvertsculpt[MAX_INFO_NUM_LEN];
  char totface[MAX_INFO_NUM_LEN], totfacesel[MAX_INFO_NUM_LEN];
  char totedge[MAX_INFO_NUM_LEN], totedgesel[MAX_INFO_NUM_LEN], totfacesculpt[MAX_INFO_NUM_LEN];
  char totbone[MAX_INFO_NUM_LEN], totbonesel[MAX_INFO_NUM_LEN];
  char totobj[MAX_INFO_NUM_LEN], totobjsel[MAX_INFO_NUM_LEN];
  char totlamp[MAX_INFO_NUM_LEN], totlampsel[MAX_INFO_NUM_LEN];
  char tottri[MAX_INFO_NUM_LEN];
  char totgplayer[MAX_INFO_NUM_LEN], totgpframe[MAX_INFO_NUM_LEN];
  char totgpstroke[MAX_INFO_NUM_LEN], totgppoint[MAX_INFO_NUM_LEN];
} SceneStatsFmt;

static bool stats_mesheval(Mesh *me_eval, bool is_selected, SceneStats *stats)
{
  if (me_eval == NULL) {
    return false;
  }

  int totvert, totedge, totface, totloop;
  if (me_eval->runtime.subdiv_ccg != NULL) {
    const SubdivCCG *subdiv_ccg = me_eval->runtime.subdiv_ccg;
    BKE_subdiv_ccg_topology_counters(subdiv_ccg, &totvert, &totedge, &totface, &totloop);
  }
  else {
    totvert = me_eval->totvert;
    totedge = me_eval->totedge;
    totface = me_eval->totpoly;
    totloop = me_eval->totloop;
  }

  stats->totvert += totvert;
  stats->totedge += totedge;
  stats->totface += totface;
  stats->tottri += poly_to_tri_count(totface, totloop);

  if (is_selected) {
    stats->totvertsel += totvert;
    stats->totfacesel += totface;
  }
  return true;
}

static void stats_object(Object *ob, SceneStats *stats, GSet *objects_gset)
{
  if ((ob->base_flag & BASE_VISIBLE_VIEWLAYER) == 0) {
    return;
  }

  const bool is_selected = (ob->base_flag & BASE_SELECTED) != 0;

  stats->totobj++;
  if (is_selected) {
    stats->totobjsel++;
  }

  switch (ob->type) {
    case OB_MESH: {
      /* we assume evaluated mesh is already built, this strictly does stats now. */
      Mesh *me_eval = BKE_object_get_evaluated_mesh(ob);
      if (!BLI_gset_add(objects_gset, me_eval)) {
        break;
      }
      stats_mesheval(me_eval, is_selected, stats);
      break;
    }
    case OB_LAMP:
      stats->totlamp++;
      if (is_selected) {
        stats->totlampsel++;
      }
      break;
    case OB_SURF:
    case OB_CURVE:
    case OB_FONT: {
      Mesh *me_eval = BKE_object_get_evaluated_mesh(ob);
      if ((me_eval != NULL) && !BLI_gset_add(objects_gset, me_eval)) {
        break;
      }

      if (stats_mesheval(me_eval, is_selected, stats)) {
        break;
      }
      ATTR_FALLTHROUGH; /* Fallthrough to displist. */
    }
    case OB_MBALL: {
      int totv = 0, totf = 0, tottri = 0;

      if (ob->runtime.curve_cache && ob->runtime.curve_cache->disp.first) {
        /* Note: We only get the same curve_cache for instances of the same curve/font/...
         * For simple linked duplicated objects, each has its own dispList. */
        if (!BLI_gset_add(objects_gset, ob->runtime.curve_cache)) {
          break;
        }

        BKE_displist_count(&ob->runtime.curve_cache->disp, &totv, &totf, &tottri);
      }

      stats->totvert += totv;
      stats->totface += totf;
      stats->tottri += tottri;

      if (is_selected) {
        stats->totvertsel += totv;
        stats->totfacesel += totf;
      }
      break;
    }
    case OB_GPENCIL: {
      if (is_selected) {
        bGPdata *gpd = (bGPdata *)ob->data;
        if (!BLI_gset_add(objects_gset, gpd)) {
          break;
        }
        /* GPXX Review if we can move to other place when object change
         * maybe to depsgraph evaluation
         */
        BKE_gpencil_stats_update(gpd);

        stats->totgplayer += gpd->totlayer;
        stats->totgpframe += gpd->totframe;
        stats->totgpstroke += gpd->totstroke;
        stats->totgppoint += gpd->totpoint;
      }
      break;
    }
    case OB_HAIR:
    case OB_POINTCLOUD:
    case OB_VOLUME: {
      break;
    }
  }
}

static void stats_object_edit(Object *obedit, SceneStats *stats)
{
  if (obedit->type == OB_MESH) {
    BMEditMesh *em = BKE_editmesh_from_object(obedit);

    stats->totvert += em->bm->totvert;
    stats->totvertsel += em->bm->totvertsel;

    stats->totedge += em->bm->totedge;
    stats->totedgesel += em->bm->totedgesel;

    stats->totface += em->bm->totface;
    stats->totfacesel += em->bm->totfacesel;

    stats->tottri += em->tottri;
  }
  else if (obedit->type == OB_ARMATURE) {
    /* Armature Edit */
    bArmature *arm = obedit->data;
    EditBone *ebo;

    for (ebo = arm->edbo->first; ebo; ebo = ebo->next) {
      stats->totbone++;

      if ((ebo->flag & BONE_CONNECTED) && ebo->parent) {
        stats->totvert--;
      }

      if (ebo->flag & BONE_TIPSEL) {
        stats->totvertsel++;
      }
      if (ebo->flag & BONE_ROOTSEL) {
        stats->totvertsel++;
      }

      if (ebo->flag & BONE_SELECTED) {
        stats->totbonesel++;
      }

      /* if this is a connected child and its parent is being moved, remove our root */
      if ((ebo->flag & BONE_CONNECTED) && (ebo->flag & BONE_ROOTSEL) && ebo->parent &&
          (ebo->parent->flag & BONE_TIPSEL)) {
        stats->totvertsel--;
      }

      stats->totvert += 2;
    }
  }
  else if (ELEM(obedit->type, OB_CURVE, OB_SURF)) { /* OB_FONT has no cu->editnurb */
    /* Curve Edit */
    Curve *cu = obedit->data;
    Nurb *nu;
    BezTriple *bezt;
    BPoint *bp;
    int a;
    ListBase *nurbs = BKE_curve_editNurbs_get(cu);

    for (nu = nurbs->first; nu; nu = nu->next) {
      if (nu->type == CU_BEZIER) {
        bezt = nu->bezt;
        a = nu->pntsu;
        while (a--) {
          stats->totvert += 3;
          if (bezt->f1 & SELECT) {
            stats->totvertsel++;
          }
          if (bezt->f2 & SELECT) {
            stats->totvertsel++;
          }
          if (bezt->f3 & SELECT) {
            stats->totvertsel++;
          }
          bezt++;
        }
      }
      else {
        bp = nu->bp;
        a = nu->pntsu * nu->pntsv;
        while (a--) {
          stats->totvert++;
          if (bp->f1 & SELECT) {
            stats->totvertsel++;
          }
          bp++;
        }
      }
    }
  }
  else if (obedit->type == OB_MBALL) {
    /* MetaBall Edit */
    MetaBall *mball = obedit->data;
    MetaElem *ml;

    for (ml = mball->editelems->first; ml; ml = ml->next) {
      stats->totvert++;
      if (ml->flag & SELECT) {
        stats->totvertsel++;
      }
    }
  }
  else if (obedit->type == OB_LATTICE) {
    /* Lattice Edit */
    Lattice *lt = obedit->data;
    Lattice *editlatt = lt->editlatt->latt;
    BPoint *bp;
    int a;

    bp = editlatt->def;

    a = editlatt->pntsu * editlatt->pntsv * editlatt->pntsw;
    while (a--) {
      stats->totvert++;
      if (bp->f1 & SELECT) {
        stats->totvertsel++;
      }
      bp++;
    }
  }
}

static void stats_object_pose(Object *ob, SceneStats *stats)
{
  if (ob->pose) {
    bArmature *arm = ob->data;
    bPoseChannel *pchan;

    for (pchan = ob->pose->chanbase.first; pchan; pchan = pchan->next) {
      stats->totbone++;
      if (pchan->bone && (pchan->bone->flag & BONE_SELECTED)) {
        if (pchan->bone->layer & arm->layer) {
          stats->totbonesel++;
        }
      }
    }
  }
}

static bool stats_is_object_dynamic_topology_sculpt(Object *ob)
{
  if (ob == NULL) {
    return false;
  }
  const eObjectMode object_mode = ob->mode;
  return ((object_mode & OB_MODE_SCULPT) && ob->sculpt && ob->sculpt->bm);
}

static void stats_object_sculpt(Object *ob, SceneStats *stats)
{

  SculptSession *ss = ob->sculpt;

  if (ss == NULL || ss->pbvh == NULL) {
    return;
  }

  switch (BKE_pbvh_type(ss->pbvh)) {
    case PBVH_FACES:
      stats->totvertsculpt = ss->totvert;
      stats->totfacesculpt = ss->totfaces;
      break;
    case PBVH_BMESH:
      stats->totvertsculpt = ob->sculpt->bm->totvert;
      stats->tottri = ob->sculpt->bm->totface;
      break;
    case PBVH_GRIDS:
      stats->totvertsculpt = BKE_pbvh_get_grid_num_vertices(ss->pbvh);
      stats->totfacesculpt = BKE_pbvh_get_grid_num_faces(ss->pbvh);
      break;
  }
}

/* Statistics displayed in info header. Called regularly on scene changes. */
static void stats_update(Depsgraph *depsgraph, ViewLayer *view_layer)
{
  SceneStats stats = {0};
  Object *ob = OBACT(view_layer);
  Object *obedit = OBEDIT_FROM_VIEW_LAYER(view_layer);

  if (obedit) {
    /* Edit Mode */
    FOREACH_OBJECT_BEGIN (view_layer, ob_iter) {
      if (ob_iter->base_flag & BASE_VISIBLE_VIEWLAYER) {
        if (ob_iter->mode == OB_MODE_EDIT) {
          stats_object_edit(ob_iter, &stats);
          stats.totobjsel++;
        }
        stats.totobj++;
      }
    }
    FOREACH_OBJECT_END;
  }
  else if (ob && (ob->mode & OB_MODE_POSE)) {
    /* Pose Mode */
    stats_object_pose(ob, &stats);
  }
  else if (stats_is_object_dynamic_topology_sculpt(ob)) {
    /* Dynamic topology. Do not count all vertices, dynamic topology stats are initialized later as
     * part of sculpt stats. */
  }
  else {
    /* Objects */
    GSet *objects_gset = BLI_gset_new(BLI_ghashutil_ptrhash, BLI_ghashutil_ptrcmp, __func__);
    DEG_OBJECT_ITER_FOR_RENDER_ENGINE_BEGIN (depsgraph, ob_iter) {
      stats_object(ob_iter, &stats, objects_gset);
    }
    DEG_OBJECT_ITER_FOR_RENDER_ENGINE_END;
    BLI_gset_free(objects_gset, NULL);
  }

  if (ob && (ob->mode & OB_MODE_SCULPT)) {
    /* Sculpt Mode. When dynamic topology is not enabled both sculpt stats and scene stats are
     * collected. */
    stats_object_sculpt(ob, &stats);
  }

  if (!view_layer->stats) {
    view_layer->stats = MEM_callocN(sizeof(SceneStats), "SceneStats");
  }

  *(view_layer->stats) = stats;
}

void ED_info_stats_clear(ViewLayer *view_layer)
{
  if (view_layer->stats) {
    MEM_freeN(view_layer->stats);
    view_layer->stats = NULL;
  }
}

static bool format_stats(Main *bmain,
                         Scene *scene,
                         ViewLayer *view_layer,
                         SceneStatsFmt *stats_fmt)
{
  /* Create stats if they don't already exist. */
  if (!view_layer->stats) {
    /* Do not not access dependency graph if interface is marked as locked. */
    wmWindowManager *wm = bmain->wm.first;
    if (wm->is_interface_locked) {
      return false;
    }
    Depsgraph *depsgraph = BKE_scene_ensure_depsgraph(bmain, scene, view_layer);
    stats_update(depsgraph, view_layer);
  }

  SceneStats *stats = view_layer->stats;

  /* Generate formatted numbers. */
#define SCENE_STATS_FMT_INT(_id) BLI_str_format_uint64_grouped(stats_fmt->_id, stats->_id)

  SCENE_STATS_FMT_INT(totvert);
  SCENE_STATS_FMT_INT(totvertsel);
  SCENE_STATS_FMT_INT(totvertsculpt);

  SCENE_STATS_FMT_INT(totedge);
  SCENE_STATS_FMT_INT(totedgesel);

  SCENE_STATS_FMT_INT(totface);
  SCENE_STATS_FMT_INT(totfacesel);
  SCENE_STATS_FMT_INT(totfacesculpt);

  SCENE_STATS_FMT_INT(totbone);
  SCENE_STATS_FMT_INT(totbonesel);

  SCENE_STATS_FMT_INT(totobj);
  SCENE_STATS_FMT_INT(totobjsel);

  SCENE_STATS_FMT_INT(totlamp);
  SCENE_STATS_FMT_INT(totlampsel);

  SCENE_STATS_FMT_INT(tottri);

  SCENE_STATS_FMT_INT(totgplayer);
  SCENE_STATS_FMT_INT(totgpframe);
  SCENE_STATS_FMT_INT(totgpstroke);
  SCENE_STATS_FMT_INT(totgppoint);

#undef SCENE_STATS_FMT_INT
  return true;
}

static void get_stats_string(
    char *info, int len, size_t *ofs, ViewLayer *view_layer, SceneStatsFmt *stats_fmt)
{
  Object *ob = OBACT(view_layer);
  Object *obedit = OBEDIT_FROM_OBACT(ob);
  eObjectMode object_mode = ob ? ob->mode : OB_MODE_OBJECT;
  LayerCollection *layer_collection = view_layer->active_collection;

  if (object_mode == OB_MODE_OBJECT) {
    *ofs += BLI_snprintf(info + *ofs,
                         len - *ofs,
                         "%s | ",
                         BKE_collection_ui_name_get(layer_collection->collection));
  }

  if (ob) {
    *ofs += BLI_snprintf(info + *ofs, len - *ofs, "%s | ", ob->id.name + 2);
  }

  if (obedit) {
    if (BKE_keyblock_from_object(obedit)) {
      *ofs += BLI_strncpy_rlen(info + *ofs, TIP_("(Key) "), len - *ofs);
    }

    if (obedit->type == OB_MESH) {
      *ofs += BLI_snprintf(info + *ofs,
                           len - *ofs,
                           TIP_("Verts:%s/%s | Edges:%s/%s | Faces:%s/%s | Tris:%s"),
                           stats_fmt->totvertsel,
                           stats_fmt->totvert,
                           stats_fmt->totedgesel,
                           stats_fmt->totedge,
                           stats_fmt->totfacesel,
                           stats_fmt->totface,
                           stats_fmt->tottri);
    }
    else if (obedit->type == OB_ARMATURE) {
      *ofs += BLI_snprintf(info + *ofs,
                           len - *ofs,
                           TIP_("Joints:%s/%s | Bones:%s/%s"),
                           stats_fmt->totvertsel,
                           stats_fmt->totvert,
                           stats_fmt->totbonesel,
                           stats_fmt->totbone);
    }
    else {
      *ofs += BLI_snprintf(
          info + *ofs, len - *ofs, TIP_("Verts:%s/%s"), stats_fmt->totvertsel, stats_fmt->totvert);
    }
  }
  else if (ob && (object_mode & OB_MODE_POSE)) {
    *ofs += BLI_snprintf(
        info + *ofs, len - *ofs, TIP_("Bones:%s/%s"), stats_fmt->totbonesel, stats_fmt->totbone);
  }
  else if ((ob) && (ob->type == OB_GPENCIL)) {
    *ofs += BLI_snprintf(info + *ofs,
                         len - *ofs,
                         TIP_("Layers:%s | Frames:%s | Strokes:%s | Points:%s"),
                         stats_fmt->totgplayer,
                         stats_fmt->totgpframe,
                         stats_fmt->totgpstroke,
                         stats_fmt->totgppoint);
  }
  else if (ob && (object_mode & OB_MODE_SCULPT)) {
    if (stats_is_object_dynamic_topology_sculpt(ob)) {
      *ofs += BLI_snprintf(info + *ofs,
                           len - *ofs,
                           TIP_("Verts:%s | Tris:%s"),
                           stats_fmt->totvert,
                           stats_fmt->tottri);
    }
    else {
      *ofs += BLI_snprintf(info + *ofs,
                           len - *ofs,
                           TIP_("Verts:%s/%s | Faces:%s/%s"),
                           stats_fmt->totvertsculpt,
                           stats_fmt->totvert,
                           stats_fmt->totfacesculpt,
                           stats_fmt->totface);
    }
  }
  else {
    *ofs += BLI_snprintf(info + *ofs,
                         len - *ofs,
                         TIP_("Verts:%s | Faces:%s | Tris:%s"),
                         stats_fmt->totvert,
                         stats_fmt->totface,
                         stats_fmt->tottri);
  }

  *ofs += BLI_snprintf(
      info + *ofs, len - *ofs, TIP_(" | Objects:%s/%s"), stats_fmt->totobjsel, stats_fmt->totobj);
}

static const char *info_statusbar_string(Main *bmain,
                                         Scene *scene,
                                         ViewLayer *view_layer,
                                         char statusbar_flag)
{
  char formatted_mem[15];
  size_t ofs = 0;
  static char info[256];
  int len = sizeof(info);

  info[0] = '\0';

  /* Scene statistics. */
  if (statusbar_flag & STATUSBAR_SHOW_STATS) {
    SceneStatsFmt stats_fmt;
    if (format_stats(bmain, scene, view_layer, &stats_fmt)) {
      get_stats_string(info + ofs, len, &ofs, view_layer, &stats_fmt);
    }
  }

  /* Memory status. */
  if (statusbar_flag & STATUSBAR_SHOW_MEMORY) {
    if (info[0]) {
      ofs += BLI_snprintf(info + ofs, len - ofs, " | ");
    }
    uintptr_t mem_in_use = MEM_get_memory_in_use();
    BLI_str_format_byte_unit(formatted_mem, mem_in_use, false);
    ofs += BLI_snprintf(info + ofs, len, TIP_("Memory: %s"), formatted_mem);
  }

  /* GPU VRAM status. */
  if ((statusbar_flag & STATUSBAR_SHOW_VRAM) && (GPU_mem_stats_supported())) {
    int gpu_free_mem_kb, gpu_tot_mem_kb;
    GPU_mem_stats_get(&gpu_tot_mem_kb, &gpu_free_mem_kb);
    float gpu_total_gb = gpu_tot_mem_kb / 1048576.0f;
    float gpu_free_gb = gpu_free_mem_kb / 1048576.0f;
    if (info[0]) {
      ofs += BLI_snprintf(info + ofs, len - ofs, " | ");
    }
    if (gpu_free_mem_kb && gpu_tot_mem_kb) {
      ofs += BLI_snprintf(info + ofs,
                          len - ofs,
                          TIP_("VRAM: %.1f/%.1f GiB"),
                          gpu_total_gb - gpu_free_gb,
                          gpu_total_gb);
    }
    else {
      /* Can only show amount of GPU VRAM available. */
      ofs += BLI_snprintf(info + ofs, len - ofs, TIP_("VRAM: %.1f GiB Free"), gpu_free_gb);
    }
  }

  /* Blender version. */
  if (statusbar_flag & STATUSBAR_SHOW_VERSION) {
    if (info[0]) {
      ofs += BLI_snprintf(info + ofs, len - ofs, " | ");
    }
    ofs += BLI_snprintf(info + ofs, len - ofs, TIP_("%s"), BKE_blender_version_string());
  }

  return info;
}

const char *ED_info_statusbar_string(Main *bmain, Scene *scene, ViewLayer *view_layer)
{
  return info_statusbar_string(bmain, scene, view_layer, U.statusbar_flag);
}

const char *ED_info_statistics_string(Main *bmain, Scene *scene, ViewLayer *view_layer)
{
  const eUserpref_StatusBar_Flag statistics_status_bar_flag = STATUSBAR_SHOW_STATS |
                                                              STATUSBAR_SHOW_MEMORY |
                                                              STATUSBAR_SHOW_VERSION;

  return info_statusbar_string(bmain, scene, view_layer, statistics_status_bar_flag);
}

static void stats_row(int col1,
                      const char *key,
                      int col2,
                      const char *value1,
                      const char *value2,
                      int *y,
                      int height)
{
  *y -= height;
  BLF_draw_default(col1, *y, 0.0f, key, 128);
  char values[128];
  BLI_snprintf(values, sizeof(values), (value2) ? "%s / %s" : "%s", value1, value2);
  BLF_draw_default(col2, *y, 0.0f, values, sizeof(values));
}

void ED_info_draw_stats(
    Main *bmain, Scene *scene, ViewLayer *view_layer, int x, int *y, int height)
{
  SceneStatsFmt stats_fmt;
  if (!format_stats(bmain, scene, view_layer, &stats_fmt)) {
    return;
  }

  Object *ob = OBACT(view_layer);
  Object *obedit = OBEDIT_FROM_OBACT(ob);
  eObjectMode object_mode = ob ? ob->mode : OB_MODE_OBJECT;
  const int font_id = BLF_default();

  UI_FontThemeColor(font_id, TH_TEXT_HI);
  BLF_enable(font_id, BLF_SHADOW);
  BLF_shadow(font_id, 5, (const float[4]){0.0f, 0.0f, 0.0f, 1.0f});
  BLF_shadow_offset(font_id, 1, -1);

  /* Translated labels for each stat row. */
  enum {
    OBJ,
    VERTS,
    EDGES,
    FACES,
    TRIS,
    JOINTS,
    BONES,
    LAYERS,
    FRAMES,
    STROKES,
    POINTS,
    MAX_LABELS_COUNT
  };
  char labels[MAX_LABELS_COUNT][64];

  STRNCPY(labels[OBJ], IFACE_("Objects"));
  STRNCPY(labels[VERTS], IFACE_("Vertices"));
  STRNCPY(labels[EDGES], IFACE_("Edges"));
  STRNCPY(labels[FACES], IFACE_("Faces"));
  STRNCPY(labels[TRIS], IFACE_("Triangles"));
  STRNCPY(labels[JOINTS], IFACE_("Joints"));
  STRNCPY(labels[BONES], IFACE_("Bones"));
  STRNCPY(labels[LAYERS], IFACE_("Layers"));
  STRNCPY(labels[FRAMES], IFACE_("Frames"));
  STRNCPY(labels[STROKES], IFACE_("Strokes"));
  STRNCPY(labels[POINTS], IFACE_("Points"));

  int longest_label = 0;
  int i;
  for (i = 0; i < MAX_LABELS_COUNT; ++i) {
    longest_label = max_ii(longest_label, BLF_width(font_id, labels[i], sizeof(labels[i])));
  }

  int col1 = x;
  int col2 = x + longest_label + (0.5f * U.widget_unit);

  /* Add some extra margin above this section. */
  *y -= (0.6f * height);

  if (object_mode == OB_MODE_OBJECT) {
    stats_row(col1, labels[OBJ], col2, stats_fmt.totobjsel, stats_fmt.totobj, y, height);
  }

  if (obedit) {
    if (obedit->type == OB_MESH) {
      stats_row(col1, labels[OBJ], col2, stats_fmt.totobjsel, stats_fmt.totobj, y, height);
      stats_row(col1, labels[VERTS], col2, stats_fmt.totvertsel, stats_fmt.totvert, y, height);
      stats_row(col1, labels[EDGES], col2, stats_fmt.totedgesel, stats_fmt.totedge, y, height);
      stats_row(col1, labels[FACES], col2, stats_fmt.totfacesel, stats_fmt.totface, y, height);
      stats_row(col1, labels[TRIS], col2, stats_fmt.tottri, NULL, y, height);
    }
    else if (obedit->type == OB_ARMATURE) {
      stats_row(col1, labels[JOINTS], col2, stats_fmt.totvertsel, stats_fmt.totvert, y, height);
      stats_row(col1, labels[BONES], col2, stats_fmt.totbonesel, stats_fmt.totbone, y, height);
    }
    else {
      stats_row(col1, labels[VERTS], col2, stats_fmt.totvertsel, stats_fmt.totvert, y, height);
    }
  }
  else if (ob && (object_mode & OB_MODE_POSE)) {
    stats_row(col1, labels[BONES], col2, stats_fmt.totbonesel, stats_fmt.totbone, y, height);
  }
  else if ((ob) && (ob->type == OB_GPENCIL)) {
    stats_row(col1, labels[LAYERS], col2, stats_fmt.totgplayer, NULL, y, height);
    stats_row(col1, labels[FRAMES], col2, stats_fmt.totgpframe, NULL, y, height);
    stats_row(col1, labels[STROKES], col2, stats_fmt.totgpstroke, NULL, y, height);
    stats_row(col1, labels[POINTS], col2, stats_fmt.totgppoint, NULL, y, height);
  }
  else if (ob && (object_mode & OB_MODE_SCULPT)) {
    if (stats_is_object_dynamic_topology_sculpt(ob)) {
      stats_row(col1, labels[VERTS], col2, stats_fmt.totvertsculpt, NULL, y, height);
      stats_row(col1, labels[TRIS], col2, stats_fmt.tottri, NULL, y, height);
    }
    else {
      stats_row(col1, labels[VERTS], col2, stats_fmt.totvertsculpt, stats_fmt.totvert, y, height);
      stats_row(col1, labels[FACES], col2, stats_fmt.totfacesculpt, stats_fmt.totface, y, height);
    }
  }
  else {
    stats_row(col1, labels[VERTS], col2, stats_fmt.totvert, NULL, y, height);
    stats_row(col1, labels[EDGES], col2, stats_fmt.totedge, NULL, y, height);
    stats_row(col1, labels[FACES], col2, stats_fmt.totface, NULL, y, height);
    stats_row(col1, labels[TRIS], col2, stats_fmt.tottri, NULL, y, height);
  }

  BLF_disable(font_id, BLF_SHADOW);
}
