/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

//!  Overview:
//!    - Summary of text layout and rendering:
//!      - A line iterator is created to iterate over the lines in a text-box
//!      - The line iterator creates a word iterator to advance through the text
//!      - The word iterator creates a character iterator to advance through
//!        codepoints. This allows reserved codepoints to be used for in-line text
//!        formatting.
//!      - The character iterator uses a UTF-8 iterator to advance through the
//!        UTF-8 encoded unicode codepoints.

#include "text.h"
#include "text_layout_private.h"

#include "graphics.h"
#include "graphics_private.h"
#include "gtypes.h"
#include "text_render.h"
#include "text_resources.h"
#include "utf8.h"

#include "applib/fonts/codepoint.h"
#include "applib/fonts/fonts.h"
#include "kernel/ui/kernel_ui.h"
#include "process_state/app_state/app_state.h"
#include "applib/applib_malloc.auto.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/hash.h"
#include "util/iterator.h"
#include "util/math.h"

#include "process_management/process_manager.h"

#include <stdint.h>
#include <string.h>
#include <limits.h>

static bool prv_char_iter_next_start_of_word(Iterator* char_iter);

// PBL-23045 Eventually remove perimeter debugging
void graphics_text_perimeter_debugging_enable(bool enable) {
  app_state_set_text_perimeter_debugging_enabled(enable);
}

// [CTX] processing individual codepoints doesn't work for contextual writing systems.
static int8_t prv_codepoint_get_horizontal_advance(FontCache* const font_cache,
                                                   const GFont font,
                                                   const Codepoint codepoint) {
  PBL_ASSERTN(font_cache);
  int8_t horiz_advance = 0;
  if (!codepoint_is_zero_width(codepoint)) {
    horiz_advance = text_resources_get_glyph_horiz_advance(font_cache, codepoint, font);
  }
  return MAX(horiz_advance, 0);
}

////////////////////////////////////////////////////////////
// Init functions

//! @note can be init to a null-termination character
void char_iter_init(Iterator* char_iter, CharIterState* char_iter_state, const TextBoxParams* const text_box_params, utf8_t* start) {
  Iterator* utf8_iter = &char_iter_state->utf8_iter;
  Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state->utf8_iter_state;

  utf8_iter_init(utf8_iter, utf8_iter_state, text_box_params->utf8_bounds, start);

  char_iter_state->text_box_params = text_box_params;

  iter_init(char_iter, (IteratorCallback) char_iter_next, char_iter_prev, (IteratorState) char_iter_state);
}

typedef enum {
  WordStateStart,
  WordStateIdeograph,
  WordStateGrowing,
  WordStateJoining,
  WordStateEnd,
} WordState;

WordState word_state_update(WordState state, Codepoint codepoint) {
  WordState new_state = state;

  switch (state) {
    case WordStateStart:
      if (codepoint == NEWLINE_CODEPOINT) {
        new_state = WordStateEnd;
      } else if (codepoint_is_ideograph(codepoint)) {
        new_state = WordStateIdeograph;
      } else {
        new_state = WordStateGrowing;
      }
      break;
    case WordStateIdeograph:
      if (codepoint == WORD_JOINER_CODEPOINT) {
        new_state = WordStateJoining;
      } else {
        new_state = WordStateEnd;
      }
      break;
    case WordStateGrowing:
      if (codepoint == WORD_JOINER_CODEPOINT) {
        new_state = WordStateJoining;
      } else if (codepoint_is_ideograph(codepoint) || codepoint_is_end_of_word(codepoint)) {
        new_state = WordStateEnd;
      } else {
        new_state = WordStateGrowing;
      }
      break;
    case WordStateJoining:
      if (codepoint == NEWLINE_CODEPOINT) {
        new_state = WordStateEnd;
      } else if (codepoint_is_ideograph(codepoint)) {
        new_state = WordStateIdeograph;
      } else if (codepoint == WORD_JOINER_CODEPOINT) {
        new_state = WordStateJoining;
      } else {
        new_state = WordStateGrowing;
      }
      break;
    case WordStateEnd:
      new_state = WordStateEnd;
      break;
  }

  return new_state;
}

//! @return true if init to new word, false otherwise (ie end of text)
//! @note assumes 'start' is not NULL, but does not assume 'start' is valid start of word
bool word_init(GContext* ctx, Word* word, const TextBoxParams* const text_box_params, utf8_t* start) {
  word->width_px = 0;

  if (*start == NULL_CODEPOINT) {
    word->start = start;
    word->end = start;
    return false;
  }

  // Set up iterator
  Iterator char_iter;
  CharIterState char_iter_state;
  char_iter_init(&char_iter, &char_iter_state, text_box_params, start);
  Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state.utf8_iter_state;

  bool success = prv_char_iter_next_start_of_word(&char_iter);
  if (!success) {
    // We couldn't find the next start of the word, just initialize to nothing
    word->start = start;
    word->end = start;
    return false;
  }

  // Init the word & state 
  word->start = utf8_iter_state->current;
  WordState state = WordStateStart;
  state = word_state_update(state, utf8_iter_state->codepoint);

  do {
    if (state == WordStateGrowing || state == WordStateIdeograph) {
      word->width_px += prv_codepoint_get_horizontal_advance(&ctx->font_cache,
          text_box_params->font, utf8_iter_state->codepoint);
    }

    iter_next(&char_iter);
    state = word_state_update(state, utf8_iter_state->codepoint);
  } while (state != WordStateEnd);

  word->end = utf8_iter_state->current;

  return true;
}

void word_iter_init(Iterator* word_iter, WordIterState* word_iter_state, GContext* ctx,
                    const TextBoxParams* const text_box_params, utf8_t* start) {
  *word_iter_state = (WordIterState) {
    .ctx = ctx,
    .text_box_params = text_box_params
  };

  word_init(ctx, &word_iter_state->current, text_box_params, start);

  iter_init(word_iter, (IteratorCallback) word_iter_next, NULL, (IteratorState) word_iter_state);
}

void line_iter_init(Iterator* line_iter, LineIterState* line_iter_state, GContext* ctx) {
  *line_iter_state = (LineIterState) {
    .ctx = ctx,
    .current = &ctx->text_draw_state.line
  };

  WordIterState* word_iter_state = &line_iter_state->word_iter_state;
  word_iter_init(&line_iter_state->word_iter, word_iter_state, ctx,
                 &ctx->text_draw_state.text_box, ctx->text_draw_state.text_box.utf8_bounds->start);

  iter_init(line_iter, (IteratorCallback) line_iter_next, NULL, (IteratorState) line_iter_state);
}

////////////////////////////////////////////////////////////
// Private helper functions

static int16_t prv_get_line_height(const TextBoxParams *text_box_params) {
  return fonts_get_font_height(text_box_params->font) + text_box_params->line_spacing_delta;
}

static int16_t prv_layout_get_line_spacing_delta(GTextLayoutCacheRef layout) {
  if (process_manager_compiled_with_legacy2_sdk()) {
    return 0;
  }

  return (layout ? ((TextLayoutExtended *)layout)->line_spacing_delta: 0);
}

////////////////////////////////////////////////////////////
// Iterator advance functions

//! Advance the char iterator to the start of the next word. Used by word_init
//! to find the start of the next word.
//! @return is_success
static bool prv_char_iter_next_start_of_word(Iterator* char_iter) {
  CharIterState* char_iter_state = (CharIterState*) char_iter->state;
  Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state->utf8_iter_state;

  // the first codepoint could be invalid, iter_next takes care of the others
  Codepoint codepoint = utf8_iter_state->codepoint;
  if (codepoint_should_skip(codepoint) || codepoint_is_formatting_indicator(codepoint)) {
    if (!iter_next(char_iter)) {
      return false;
    }
  }

  while (codepoint_is_zero_width(utf8_iter_state->codepoint)) {
    if (utf8_iter_state->codepoint == 0) {
      PBL_ASSERTN(utf8_iter_state->current == utf8_iter_state->bounds->end);
      return false;
    }

    if (!iter_next(char_iter)) {
      break;
    }
  }

  return true;
}

static bool prv_line_iter_is_vertical_overflow(const LineIterState* const line_iter_state,
                                               const TextBoxParams* const text_box_params) {
  int16_t next_line_y_extent;
  // Normally, we lay out the text one line below the regular cutoff so that it may be rendered,
  // albeit clipped.  But, if we're rendering in truncation mode (e.g. GTextOverflowModeFill or
  // GTextOverflowModeTrailingEllipsis), we can immediately cut the text off below the box height
  // if we're not rendering the first line.
  //    - This, because the user does not expect to see more text drawn below, after the '...'.
  //    - The first-line exception means that text, and therefore the telltale
  //      ellipsis, will always be visisble.
  if ((text_box_params->overflow_mode == GTextOverflowModeTrailingEllipsis ||
       text_box_params->overflow_mode == GTextOverflowModeFill) &&
      line_iter_state->current->origin.y != text_box_params->box.origin.y) {
    // We're in a truncation mode AND not on the first line.
    // So, include the full height of the current line in next_line_y_extent, so text will stop
    // being layed out immediately after it exceeds the height of the container.
    next_line_y_extent = line_iter_state->current->origin.y + prv_get_line_height(text_box_params);
  } else {
    // We're either in a non-truncating mode, or on the first line of a truncating mode.
    // So, only include the extent of the previous line in next_line_y_extent (making it more of
    // a "last_line_y_extent").
    // Putting aside the misleading variable name, this will cause us to lay out one more line than
    // will completely fit in the container - so that it may still be displayed, even if partially
    // or completely clipped.
    next_line_y_extent = line_iter_state->current->origin.y;
  }
  return (next_line_y_extent > (text_box_params->box.origin.y + text_box_params->box.size.h));
}

//! @return is_advanced
bool line_iter_next(IteratorState state) {
  LineIterState* line_iter_state = (LineIterState*) state;
  const TextBoxParams* const text_box_params = &line_iter_state->ctx->text_draw_state.text_box;

  if (prv_line_iter_is_vertical_overflow(line_iter_state, text_box_params)) {
    return false;
  }

  line_iter_state->current->origin.x = text_box_params->box.origin.x;
  line_iter_state->current->origin.y += prv_get_line_height(text_box_params);
  line_iter_state->current->width_px = 0;  // needs to be reset per line
  line_iter_state->current->max_width_px = text_box_params->box.size.w;
  line_iter_state->current->suffix_codepoint = 0;
  line_iter_state->current->start = NULL;

  return true;
}

//! @return is_advanced
bool word_iter_next(IteratorState state) {
  WordIterState* word_iter_state = (WordIterState*) state;

  Word* current_word = &word_iter_state->current;
  const TextBoxParams* const text_box_params = word_iter_state->text_box_params;
  GContext* ctx = word_iter_state->ctx;

  if (*current_word->end == NULL_CODEPOINT) {
    return false;
  }

  return word_init(ctx, current_word, text_box_params, current_word->end);
}

//! @return is_advanced
bool char_iter_next(IteratorState state) {
  CharIterState* char_iter_state = (CharIterState*) state;

  Codepoint codepoint;
  Iterator* utf8_iter = &char_iter_state->utf8_iter;
  Utf8IterState* utf8_iter_state = &char_iter_state->utf8_iter_state;

  while (true) {
    if (utf8_iter_state->current >= utf8_iter_state->bounds->end) {
      // EOS while searching for valid codepoint
      return false;
    }

    bool is_utf8_advanced = iter_next(utf8_iter);
    codepoint = utf8_iter_state->codepoint;

    if (!is_utf8_advanced) {
      return is_utf8_advanced;
    }

    PBL_ASSERTN(codepoint != 0);

    if (codepoint_is_formatting_indicator(codepoint)) {
      continue;
    }

    if (codepoint_should_skip(codepoint)) {
      continue;
    };

    return true;
  }
}

bool char_iter_prev(IteratorState state) {
  CharIterState* char_iter_state = (CharIterState*) state;

  Codepoint codepoint;
  Iterator* utf8_iter = &char_iter_state->utf8_iter;
  Utf8IterState* utf8_iter_state = &char_iter_state->utf8_iter_state;

  while (true) {
    if (utf8_iter_state->current <= utf8_iter_state->bounds->start) {
      // EOS while searching for valid codepoint
      return false;
    }

    bool is_utf8_advanced = iter_prev(utf8_iter);
    codepoint = utf8_iter_state->codepoint;

    if (!is_utf8_advanced) {
      return is_utf8_advanced;
    }

    PBL_ASSERTN(codepoint != 0);

    if (codepoint_is_formatting_indicator(codepoint)) {
      continue;
    }

    if (codepoint_should_skip(codepoint)) {
      continue;
    };

    return true;
  }
}

////////////////////////////////////////////////////////////
// Helper functions

//! Trim given codepoint from the start of the word
//! Used to remove whitespace and newlines
//! @return is_trimmed
bool word_trim_preceeding_codepoint(GContext* ctx, Word* word, const Codepoint codepoint,
                                    const TextBoxParams* const text_box_params) {
  Iterator char_iter;
  CharIterState char_iter_state;
  char_iter_init(&char_iter, &char_iter_state, text_box_params, word->start);

  Utf8IterState* utf8_iter_state = &char_iter_state.utf8_iter_state;

  if (utf8_iter_state->codepoint != codepoint) {
    return false;
  }

  bool is_advanced = iter_next(&char_iter);

  if (!is_advanced) {
    PBL_ASSERTN(*word->end == NULL_CODEPOINT);
    word->start = NULL;
    return false;
  }

  if (word->end == char_iter_state.utf8_iter_state.current) {
    // Word has been completely trimmed; init a new word
    bool is_end_of_text = (*word->end == NULL_CODEPOINT ||
        char_iter_state.utf8_iter_state.current >= text_box_params->utf8_bounds->end);

    if (!is_end_of_text) {
      word_init(ctx, word, text_box_params, word->end);
    }
    return false;
  }

  // Trim
  int advance = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
      text_box_params->font, codepoint);
  PBL_ASSERTN(advance <= word->width_px); // Negative-length word not allowed

  word->width_px -= advance;
  word->start = utf8_iter_state->current;
  return true;
}

// [INTL] whitespace is more than just the space character.
void word_trim_preceeding_whitespace(GContext* ctx, Word* word, const TextBoxParams* const text_box_params) {
  while (word_trim_preceeding_codepoint(ctx, word, SPACE_CODEPOINT, text_box_params));
}

////////////////////////////////////////////////////////////
// Walk Line

typedef void (*CharVisitorCallback)(GContext* ctx, const TextBoxParams* const text_box_params,
                                    Line* line, GRect cursor, const Codepoint codepoint);

void render_chars_char_visitor_cb(GContext* ctx, const TextBoxParams* const text_box_params,
                                  Line* line, GRect cursor, const Codepoint codepoint) {
  if (codepoint_is_zero_width(codepoint)) {
    return;
  }

  render_glyph(ctx, codepoint, text_box_params->font, cursor);
}

void update_dimensions_char_visitor_cb(GContext* ctx, const TextBoxParams* const text_box_params,
                                       Line* line, GRect cursor, const Codepoint codepoint) {
  (void) ctx;
  PBL_ASSERT(cursor.origin.x >= line->origin.x, "Text cursor x=<%u> ahead of line origin x=<%u>",
      cursor.origin.x, line->origin.x);

  const int glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
      text_box_params->font, codepoint);

  line->width_px = (cursor.origin.x + glyph_width_px) - line->origin.x;

  PBL_ASSERT(line->width_px <= text_box_params->box.size.w,
      "Line <%p>: max extent=<%" PRId16 "> exceeds text_box_params width=<%" PRId16 ">",
      line, line->width_px + line->origin.x, text_box_params->box.size.w);
}

//! Call char_visitor_cb on each character in the line 
//! Used to update line dimensions and render characters
//! Traverse until end of line->width_px if rendering chars, else text_box_params width
//! if updating line dimensions
//! @return utf8_t* pointer to last visited character
utf8_t* walk_line(GContext* ctx, Line* line, const TextBoxParams* const text_box_params,
                  CharVisitorCallback char_visitor_cb) {
  PBL_ASSERTN(char_visitor_cb);

  // We used to check that the line height was <= the container height here - no longer required,
  // as the vertical overflow is handled during layout.

  int available_horiz_px;
  if (char_visitor_cb == update_dimensions_char_visitor_cb) {
    // Line dimensions not yet set; use all available line space
    available_horiz_px = line->max_width_px;
  } else {
    available_horiz_px = line->width_px;
  }

  PBL_ASSERT(line->width_px <= text_box_params->box.size.w,
      "Line <%p>: max extent=<%" PRId16 "> exceeds text_box_params width=<%" PRId16 ">", line,
      line->width_px + line->origin.x, text_box_params->box.size.w);

  int suffix_width_px = 0;

  if (line->suffix_codepoint) {
    suffix_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
        text_box_params->font, line->suffix_codepoint);
  }

  if (available_horiz_px < suffix_width_px) {
    return NULL;
  }

  // Set up iterator
  Iterator char_iter;
  CharIterState char_iter_state;
  char_iter_init(&char_iter, &char_iter_state, text_box_params, line->start);
  Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state.utf8_iter_state;

  bool is_newline_as_space = text_box_params->overflow_mode == GTextOverflowModeFill;
  Codepoint current_codepoint = utf8_iter_state->codepoint;
  if (current_codepoint == NEWLINE_CODEPOINT) {
    if (is_newline_as_space) {
      current_codepoint = SPACE_CODEPOINT;
    } else {
      return utf8_iter_state->current;
    }
  }

  int walked_width_px = 0;
  int next_glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
      text_box_params->font, current_codepoint);

  utf8_t* last_visited_char = NULL;

  while (walked_width_px + next_glyph_width_px + suffix_width_px <= available_horiz_px) {
    GRect cursor = {
      .origin = line->origin,
      .size.w = next_glyph_width_px,
      .size.h = fonts_get_font_height(text_box_params->font)
    };
    cursor.origin.x += walked_width_px;

    char_visitor_cb(ctx, text_box_params, line, cursor, current_codepoint);

    walked_width_px += next_glyph_width_px;

    last_visited_char = utf8_iter_state->current;

    if (!iter_next(&char_iter)) {
      break;
    }

    current_codepoint = utf8_iter_state->codepoint;
    if (current_codepoint == NEWLINE_CODEPOINT) {
      if (is_newline_as_space) {
        current_codepoint = SPACE_CODEPOINT;
      } else {
        break;
      }
    }

    next_glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
        text_box_params->font, current_codepoint);
  }

  // Trim trailing whitespace
  if (last_visited_char) {
    while ((current_codepoint == NEWLINE_CODEPOINT || current_codepoint == SPACE_CODEPOINT)) {
      // Newlines should not adjust the width
      if (current_codepoint == NEWLINE_CODEPOINT) {
        next_glyph_width_px = 0;
      } else {
        next_glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
                                                               text_box_params->font,
                                                               current_codepoint);
      }

      // Safety check
      if (walked_width_px < next_glyph_width_px) {
        break;
      }
      walked_width_px -= next_glyph_width_px;

      if (!iter_prev(&char_iter)) {
        break;
      }
      current_codepoint = utf8_iter_state->codepoint;
    }
  }

  if (line->suffix_codepoint) {
    GRect cursor = {
      .origin = line->origin,
      .size.w = next_glyph_width_px,
      .size.h = fonts_get_font_height(text_box_params->font)
    };
    cursor.origin.x += walked_width_px;
    if (char_visitor_cb) {
      char_visitor_cb(ctx, text_box_params, line, cursor, line->suffix_codepoint);
    }
  }

  return last_visited_char;
}


////////////////////////////////////////////////////////////
// Walk Lines

void set_ellipsis_on_overflow_last_line_cb(GContext* ctx, Line* line,
                                           const TextBoxParams* const text_box_params,
                                           const bool is_text_remaining) {
  // Only set a trailing ellipsis if there is text remaining
  if (!is_text_remaining) {
    return;
  }

  // Check if outputting two lines extend beyond the text box height - then display the ellipsis
  // on the current line
  bool is_last_line = ((line->origin.y + (2 * prv_get_line_height(text_box_params))) >
                       (text_box_params->box.origin.y + text_box_params->box.size.h));
  // Check if this is the last line
  if (!is_last_line) {
    return;
  }

  line->suffix_codepoint = ELLIPSIS_CODEPOINT;

  // update the line dimensions
  walk_line(ctx, line, text_box_params, update_dimensions_char_visitor_cb);
}

void render_all_render_line_cb(GContext* ctx, Line* line, const TextBoxParams* const text_box_params) {
  walk_line(ctx, line, text_box_params, (CharVisitorCallback) render_chars_char_visitor_cb);
}

void update_all_layout_update_cb(TextLayout* layout, Line* line,
                                 const TextBoxParams* const text_box_params) {
  PBL_ASSERTN(line);
  if (layout) {
    layout->max_used_size.h = (line->origin.y - layout->box.origin.y) + line->height_px +
                              text_box_params->line_spacing_delta;
    layout->max_used_size.w = MAX(line->width_px, layout->max_used_size.w);
  }
}

//! @return is_overflow
bool is_clip_box_overflow_top_stop_condition_cb(GContext* ctx, Line* line,
                                                const TextBoxParams* const text_box_params) {
  int next_line_max_y = line->origin.y;
  int clip_box_min_y = ctx->draw_state.clip_box.origin.y;
  return (next_line_max_y < clip_box_min_y);
}

//! @return is_overflow
bool is_clip_box_overflow_bottom_stop_condition_cb(GContext* ctx, Line* line,
                                                   const TextBoxParams* const text_box_params) {
  int next_line_min_y = line->origin.y + line->height_px + text_box_params->line_spacing_delta;
  int clip_box_max_y = ctx->draw_state.clip_box.origin.y + ctx->draw_state.clip_box.size.h;
  return (next_line_min_y > clip_box_max_y);
}

//! @return is_overflow
bool is_clip_box_overflow_stop_condition_cb(GContext* ctx, Line* line,
                                            const TextBoxParams* const text_box_params) {
  return (is_clip_box_overflow_bottom_stop_condition_cb(ctx, line, text_box_params) ||
          is_clip_box_overflow_top_stop_condition_cb(ctx, line, text_box_params));
}

#define TEXT_LINE_BASE_LINE(line) ((line)->height_px)
#define TEXT_LINE_CAP_LINE(line) ((line)->height_px * 1 / 2)
// Based on Gothic fonts, DESCENDER is approx 1/5 of height (ascender + descender)
// Gothic 24 Bold ascent = 840, descent 168
// Gothic 18 Bold ascent = 840, descent 168
// Gothic 14 ascent = 864, descent 144
// Bitham ascent = 800, descend = 200
// DroidSerif Bold ascent = 1638, descent = 410
#define TEXT_LINE_DESCENDER_LINE(line) DIVIDE_CEIL((line)->height_px, 5)  // 1/5th rounded up

T_STATIC NOINLINE MOCKABLE void prv_debug_perimeter(GContext *ctx, const GRangeHorizontal *h_range,
                                                   const Line *line) {
  // PBL-23045 Eventually remove perimeter debugging
  // Draw a red horizontal line to show the range of the current lines perimeter
  if (app_state_get_text_perimeter_debugging_enabled()) {
#if !defined(UNITTEST) && !defined(PLATFORM_TINTIN)
    const Fixed_S16_3 fixed_x1 = (Fixed_S16_3) {
      .integer = h_range->origin_x,
    };
    const Fixed_S16_3 fixed_x2 = (Fixed_S16_3) {
      .integer = h_range->origin_x + h_range->size_w,
    };
    graphics_private_draw_horizontal_line_prepared(ctx, &ctx->dest_bitmap,
                                                   &ctx->dest_bitmap.bounds,
                                                   line->origin.y + TEXT_LINE_CAP_LINE(line),
                                                   fixed_x1, fixed_x2, GColorRed);
    graphics_private_draw_horizontal_line_prepared(ctx, &ctx->dest_bitmap,
                                                   &ctx->dest_bitmap.bounds,
                                                   line->origin.y + TEXT_LINE_BASE_LINE(line),
                                                   fixed_x1, fixed_x2, GColorRed);
#endif
  }
}

typedef struct {
  int16_t origin_x;
  int16_t width_px;
} OrphanLineState;

static OrphanLineState prv_capture_orphan_state(Line const* line) {
  return (OrphanLineState) {
    .origin_x = line->origin.x,
    .width_px = line->width_px,
  };
}

static void prv_apply_orphan_state(const OrphanLineState *state, Line *line) {
  line->origin.x = state->origin_x;
  line->width_px = state->width_px;
}

//! Iterate over lines in the text box
static inline void prv_walk_lines_down(Iterator* const line_iter, TextLayout* const layout,
                                       WalkLinesCallbacks* const callbacks) {
  LineIterState* line_iter_state = (LineIterState*) line_iter->state;
  GContext* ctx = line_iter_state->ctx;
  const GSize ctx_size = graphics_context_get_framebuffer_size(ctx);
  const TextBoxParams* const text_box_params = &ctx->text_draw_state.text_box;
  Line* line = line_iter_state->current;

  const TextLayoutFlowData *flow_data = graphics_text_layout_get_flow_data(layout);
  const bool uses_paging = flow_data->paging.page_on_screen.size_h != 0;
  const bool uses_perimeter = flow_data->perimeter.impl != NULL;
  const GPoint perimeter_paging_offset =
    uses_paging ? gpoint_sub(flow_data->paging.origin_on_screen, line->origin) : GPointZero;
  Word prev_line_word = WORD_EMPTY;
  while (!prv_line_iter_is_vertical_overflow(line_iter_state, text_box_params)) {
    GPoint line_in_perimeter_space = gpoint_add(line->origin, perimeter_paging_offset);

    if (uses_paging) {
      const int16_t page_max_y = flow_data->paging.page_on_screen.origin_y +
                                 flow_data->paging.page_on_screen.size_h;

      // TODO: optimize
      while (line_in_perimeter_space.y < flow_data->paging.page_on_screen.origin_y) {
        line_in_perimeter_space.y += flow_data->paging.page_on_screen.size_h;
      }
      while (line_in_perimeter_space.y >= page_max_y) {
        line_in_perimeter_space.y -= flow_data->paging.page_on_screen.size_h;
      }

      const int16_t distance_to_page_end = page_max_y - line_in_perimeter_space.y;

      if (distance_to_page_end < line->height_px + TEXT_LINE_DESCENDER_LINE(line)) {
        // If this line would exceed the page_height, shift the line origin to the next page
        line->origin.y += distance_to_page_end;
        continue;  // skip rendering this round, bypasses iter_next (no reset necessary)
      }
    }

    // If we are restricting the perimeter of the draw box, restrict per line region here
    if (uses_perimeter) {
      GRangeHorizontal text_horizontal_range = {.origin_x = line_in_perimeter_space.x,
                                                .size_w = line->max_width_px};
      const GRangeVertical vertical_range = {
        .origin_y = line_in_perimeter_space.y + TEXT_LINE_CAP_LINE(line),
        .size_h = TEXT_LINE_BASE_LINE(line) - TEXT_LINE_CAP_LINE(line)
      };
      GRangeHorizontal perimeter_horizontal_range =
        flow_data->perimeter.impl->callback(flow_data->perimeter.impl, &ctx_size, vertical_range,
                                            flow_data->perimeter.inset);

      prv_debug_perimeter(ctx, &perimeter_horizontal_range, line);

      // protect against range expanding: clip perimeter to the original text range
      grange_clip((GRange*)&perimeter_horizontal_range, (GRange*)&text_horizontal_range);
      text_horizontal_range = perimeter_horizontal_range;

      // convert range back to screen space
      text_horizontal_range.origin_x -= perimeter_paging_offset.x;

      // Update line parameters for restricted horizontal range
      line->origin.x = text_horizontal_range.origin_x;
      line->max_width_px = text_horizontal_range.size_w;
    }

    // reference into the iterator's current word to easily access this attribute here and
    // later without the complicated cast
    Word *const current_word_ref = &(((WordIterState*)line_iter_state->word_iter.state)->current);
    // state that needs to be captured so we can restore it in case of an orphan
    const Word word_before_rendering = *current_word_ref;
    const OrphanLineState orphan_state = prv_capture_orphan_state(line);

    // When repeating text to prevent orhpans we could run into the situation where repeating text
    // pushes down the remaining text far enough so it ends up on yet another page. This would
    // enter an infinite loop.
    // To avoid that, we only apply this strategy, when it's "safe" to do so (in theory, there's
    // still the propability to run into this scenario if the perimeter isn't vertically symmetric).
    // The chosen number should be large enough for the previous line, the orphan line plus some
    // buffer.
    const int num_safe_lines = 3;
    const bool page_contains_enough_lines =
      (flow_data->paging.page_on_screen.size_h >= num_safe_lines * line->height_px);
    bool avoiding_orphans = uses_paging && ctx->draw_state.avoid_text_orphans &&
                            page_contains_enough_lines;

render_line: {} // this {} is just an empty statement that both C and our linter accepts
    const bool is_text_remaining = line_add_words(
        line, &line_iter_state->word_iter, callbacks->last_line_cb);
    // NOTE: Account for descender - assume descender is no more than half the line height
    const int16_t line_spacing_delta = prv_layout_get_line_spacing_delta(layout);
    const int32_t line_max_y = line->origin.y + line->height_px +
                               TEXT_LINE_DESCENDER_LINE(line) + line_spacing_delta;
    const int32_t clip_box_min_y = ctx->draw_state.clip_box.origin.y;

    if (line_max_y > clip_box_min_y) {
      if (avoiding_orphans) {
        const bool line_is_first_line_page =
          (line_in_perimeter_space.y == flow_data->paging.page_on_screen.origin_y);
        const bool is_orphan =
          (line_is_first_line_page && prev_line_word.start && !is_text_remaining);

        if (is_orphan) {
          *current_word_ref = prev_line_word;
          prv_apply_orphan_state(&orphan_state, line);
          avoiding_orphans = false; // prevent infinte loops
          goto render_line;
        }
      }
      if (callbacks->render_line_cb) {
        callbacks->render_line_cb(ctx, line, text_box_params);
      }
    }
    prev_line_word = word_before_rendering;

    if (callbacks->layout_update_cb) {
      callbacks->layout_update_cb(layout, line, text_box_params);
    }

    if (callbacks->stop_condition_cb) {
      if (callbacks->stop_condition_cb(ctx, line, text_box_params)) {
        break;
      }
    }

    if (!is_text_remaining) {
      break;
    }

    // Shouldn't have rendered the line if there was insufficient space
    PBL_ASSERTN(iter_next(line_iter));
  }
}

////////////////////////////////////////////////////////////
// Text layout

//! @return is_success
bool line_add_word(GContext* ctx, Line* line, Word* word, const TextBoxParams* const text_box_params) {
  // Horizontal overflow
  if (line->width_px > line->max_width_px) {
    return false;
  }

  // Don't set the line height if there is a vertical overflow
  const int line_height = fonts_get_font_height(text_box_params->font);

  // We used to re-check for vertical overflow here
  // but this is protected by a call to prv_line_iter_is_vertical_overflow,
  // which will handle the truncation/clipping logic.

  PBL_ASSERTN(word->start);

  bool is_newline_first_codepoint = (*word->start == NEWLINE_CODEPOINT);

  line->height_px = line_height;

  if (is_newline_first_codepoint) {
    // This trims off leading \n's from word. If we reach the end of the text while doing this, it sets
    //  word->start to NULL. 
    word_trim_preceeding_codepoint(ctx, word, NEWLINE_CODEPOINT, text_box_params);
    if (text_box_params->overflow_mode != GTextOverflowModeFill) {
      return false;
    }
    // If there is word text left (we have \n's at the end of the text), we're done
    if (word->start == NULL) {
      return false;
    }
  }

  bool is_overflow = (line->width_px + word->width_px > line->max_width_px);
  bool is_start_of_line = (line->width_px == 0);
  bool should_hyphenate = (is_overflow && is_start_of_line);

  if (is_start_of_line) {
    line->start = word->start;
  }

  if (should_hyphenate) {
    // Set suffix character
    // [CJK] - when breaking a Katakana word, you probably don't want to add a hyphen. And to
    // a Japanese user, a hyphen with Katakana looks like a long (chou-on) sound mark.
    line->suffix_codepoint = HYPHEN_CODEPOINT;
    utf8_t* last_visited = walk_line(ctx, line, text_box_params,
        (CharVisitorCallback) update_dimensions_char_visitor_cb);
    last_visited = (last_visited == NULL) ? (word->start) : last_visited;

    // Trim the word
    int suffix_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
        text_box_params->font, HYPHEN_CODEPOINT);
    int truncated_word_length_px = (line->width_px - suffix_width_px);
    PBL_ASSERTN(word->width_px >= truncated_word_length_px);
    word->width_px -= truncated_word_length_px;
    word->start = utf8_get_next(last_visited);

    return false;
  }

  if (!is_overflow) {
    // Add entire word
    PBL_ASSERTN(line->suffix_codepoint == 0);
    line->width_px += word->width_px;
    return true;
  }

  // Word-wrap
  word_trim_preceeding_whitespace(ctx, word, text_box_params);
  return false;
}

static void prv_line_justify(Line* line, const TextBoxParams* const text_box_params) {
  PBL_ASSERTN(line->max_width_px >= line->width_px);

  int horiz_px_remaining = (line->max_width_px - line->width_px);

  // [RTL] in addition to left, right and center alignment, you want a "primary"
  // alignment that is left for LTR writing systems, and right for RTL.
  switch (text_box_params->alignment) {
    case GTextAlignmentCenter:
      line->origin.x = line->origin.x + (horiz_px_remaining / 2);
      break;
    case GTextAlignmentRight:
      line->origin.x = line->origin.x + horiz_px_remaining;
      break;
    case GTextAlignmentLeft:
      break;
  }
}

//! @return is_text_remaining
bool line_add_words(Line* line, Iterator* word_iter, LastLineCallback last_line_cb) {

  WordIterState* word_iter_state = (WordIterState*) word_iter->state;

  line->start = word_iter_state->current.start;

  bool is_text_remaining = (line->start != NULL);

  // PBL-22083 : max_width_px == 0 eats a character that should appear on next line
  while (is_text_remaining && line->max_width_px > 0) {
    Word next_word = word_iter_state->current;

    bool is_added = line_add_word(word_iter_state->ctx, line, &next_word,
                                  word_iter_state->text_box_params);

    if (!is_added) {
      word_iter_state->current = next_word;
      // Check if word was trimmed until the null termination
      if (next_word.start == NULL) {
        is_text_remaining = false;
      } else {
        is_text_remaining = true;
      }
      break;
    }

    is_text_remaining = iter_next(word_iter);
  }

  if (last_line_cb) {
    last_line_cb(word_iter_state->ctx, line, word_iter_state->text_box_params, is_text_remaining);
  }

  prv_line_justify(line, word_iter_state->text_box_params);

  return is_text_remaining;
}

static bool prv_text_layout_is_fresh(TextLayout* layout, GFont const font, const GRect box,
                                     const GTextOverflowMode overflow_mode,
                                     const GTextAlignment alignment, Codepoint text_hash) {
  PBL_ASSERTN(layout);

  if (text_hash != layout->hash) {
    return false;
  }

  if (!grect_equal(&box, &layout->box)) {
    return false;
  }

  if (overflow_mode != layout->overflow_mode) {
    return false;
  }

  if (alignment != layout->alignment) {
    return false;
  }

  if (font != layout->font) {
    return false;
  }

  return true;
}

static inline void prv_text_walk_lines(GContext* ctx, TextLayout* const layout,
                                       WalkLinesCallbacks* callbacks) {

  TextBoxParams *text_box = &ctx->text_draw_state.text_box;

  if (grect_is_empty(&text_box->box)) {
    return;
  }

  const Utf8Bounds *utf8_bounds = text_box->utf8_bounds;

  bool is_string_empty = (utf8_bounds->start == utf8_bounds->end);
  if (is_string_empty) {
    return;
  }

  const GTextOverflowMode overflow_mode = text_box->overflow_mode;
  bool is_ellipsis_on_overflow = (overflow_mode == GTextOverflowModeTrailingEllipsis ||
                                  overflow_mode == GTextOverflowModeFill);
  if (is_ellipsis_on_overflow) {
    callbacks->last_line_cb = set_ellipsis_on_overflow_last_line_cb;
  } else {
    callbacks->last_line_cb = NULL;
  }

  ctx->text_draw_state.line = (Line) {
    .start = utf8_bounds->start,
    // set initial bounding values for line
    .origin = text_box->box.origin, //<! Needs to be in global co-ords!
    .max_width_px = text_box->box.size.w,
    .height_px = fonts_get_font_height(text_box->font)
  };

  Iterator line_iter;
  line_iter_init(&line_iter, &ctx->text_draw_state.line_iter_state, ctx);

  prv_walk_lines_down(&line_iter, layout, callbacks);
}

static void prv_graphics_text_layout_update(GContext* ctx, const char* text, GFont const font,
                                            const GRect box, const GTextOverflowMode overflow_mode,
                                            const GTextAlignment alignment,
                                            TextLayout* const layout) {
  PBL_ASSERTN(layout);

  bool success = false;
  const Utf8Bounds utf8_bounds = utf8_get_bounds(&success, text);
  if (!success) {
    layout->max_used_size = GSizeZero;
    PBL_LOG(LOG_LEVEL_DEBUG, "Invalid UTF8");
    return;
  }

  int str_len_bytes = (utf8_bounds.end - utf8_bounds.start);
  Codepoint text_hash = hash((const uint8_t*) utf8_bounds.start, str_len_bytes);

  if (prv_text_layout_is_fresh(layout, font, box, overflow_mode, alignment, text_hash)) {
    return;
  }

  layout->max_used_size = GSizeZero;
  layout->hash = text_hash;
  layout->box = box;
  layout->overflow_mode = overflow_mode;
  layout->alignment = alignment;
  layout->font = font;

  WalkLinesCallbacks callbacks = {
    .layout_update_cb = update_all_layout_update_cb
  };

  int16_t line_spacing_delta = prv_layout_get_line_spacing_delta(layout);
  ctx->text_draw_state.text_box = (TextBoxParams) {
    .utf8_bounds = &utf8_bounds,
    .box = box,
    .font = font,
    .overflow_mode = overflow_mode,
    .alignment = alignment,
    .line_spacing_delta = line_spacing_delta,
  };

  prv_text_walk_lines(ctx, layout, &callbacks);
}

// helper macro to avoid source code duplication
// we call this instead of a true function to keep the stack as low as possible as this is
// on a critical path.
#define APP_TEXT_GET_CONTENT_SIZE(text, font, box, overflow_mode, alignment, text_attributes) \
  do { \
    GContext* ctx = app_state_get_graphics_context(); \
    return graphics_text_layout_get_max_used_size( \
      ctx, text, font, box, overflow_mode, alignment, text_attributes); \
  } while (0)

GSize app_graphics_text_layout_get_content_size_with_attributes(
  const char *text, GFont const font, const GRect box, const GTextOverflowMode overflow_mode,
  const GTextAlignment alignment, GTextAttributes *text_attributes) {
  APP_TEXT_GET_CONTENT_SIZE(text, font, box, overflow_mode, alignment, text_attributes);
}


GSize app_graphics_text_layout_get_content_size(const char *text, GFont const font, const GRect box,
                                                const GTextOverflowMode overflow_mode,
                                                const GTextAlignment alignment) {
  APP_TEXT_GET_CONTENT_SIZE(text, font, box, overflow_mode, alignment, NULL);
}

uint16_t graphics_text_layout_get_text_height(GContext *ctx, const char *text, GFont const font,
                                              uint16_t bounds_width,
                                              const GTextOverflowMode overflow_mode,
                                              const GTextAlignment alignment) {
  const int16_t LAYOUT_HEIGHT_IGNORE = SHRT_MAX;
  GRect box = {
        .origin = (GPoint) { .x = 0, .y = 0 },
        .size = (GSize) { .w = bounds_width, .h = LAYOUT_HEIGHT_IGNORE }
      };
  GSize size = graphics_text_layout_get_max_used_size(ctx, text, font,
      box, overflow_mode, alignment, NULL);
  return size.h;
}

GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text, GFont const font,
                                             const GRect box, const GTextOverflowMode overflow_mode,
                                             const GTextAlignment alignment,
                                             GTextLayoutCacheRef const layout) {
  TextLayoutExtended stack_layout = { 0 }; // Default use extended layout
  TextLayout* text_layout = layout ? (TextLayout*) layout : (TextLayout*) &stack_layout;
  prv_graphics_text_layout_update(ctx, text, font, box, overflow_mode, alignment, text_layout);
  return text_layout->max_used_size;
}

void graphics_draw_text(GContext* ctx, const char* text, GFont const font,
                        GRect box, const GTextOverflowMode overflow_mode,
                        const GTextAlignment alignment, GTextLayoutCacheRef const layout) {
  if (ctx->lock) {
    return;
  }

  bool success = false;
  const Utf8Bounds utf8_bounds = utf8_get_bounds(&success, text);
  if (!success) {
    PBL_LOG(LOG_LEVEL_DEBUG, "Invalid UTF8");
    return;
  }

  GRect global_box = grect_to_global_coordinates(box, ctx);

  GRect temp_box = global_box;
  grect_clip(&temp_box, &ctx->draw_state.clip_box);
  if (temp_box.size.h <= 0) {
    // the text is not ever going to make it on screen. Bail early.
    return;
  }


  if (layout) {
    layout->box.origin = global_box.origin;
  }

  WalkLinesCallbacks callbacks = {
    .render_line_cb = render_all_render_line_cb,
    .layout_update_cb = update_all_layout_update_cb,
    .stop_condition_cb = is_clip_box_overflow_bottom_stop_condition_cb
  };

  int16_t line_spacing_delta = prv_layout_get_line_spacing_delta(layout);
  ctx->text_draw_state.text_box = (TextBoxParams) {
    .utf8_bounds = &utf8_bounds,
    .box = global_box,
    .font = font,
    .overflow_mode = overflow_mode,
    .alignment = alignment,
    .line_spacing_delta = line_spacing_delta,
  };

  prv_text_walk_lines(ctx, layout, &callbacks);
}

void graphics_text_layout_cache_init(GTextLayoutCacheRef* layout) {
  if (process_manager_compiled_with_legacy2_sdk()) {
    *layout = applib_type_malloc(TextLayout);
    *((TextLayout*) *layout) = (TextLayout) { 0 };
  } else {
    *layout = applib_type_malloc(TextLayoutExtended);
    *((TextLayoutExtended*) *layout) = (TextLayoutExtended) { 0 };
  }
}

void graphics_text_layout_cache_deinit(GTextLayoutCacheRef* layout) {
  TextLayout* text_layout = (TextLayout*) *layout;
  applib_free(text_layout);
  *layout = NULL;
}

GTextAttributes *graphics_text_attributes_create(void) {
  GTextAttributes *result;
  graphics_text_layout_cache_init(&result);
  return result;
}

void graphics_text_attributes_destroy(GTextAttributes *text_attributes) {
  if (!text_attributes) {
    return;
  }

  graphics_text_layout_cache_deinit(&text_attributes);
}


static TextLayoutExtended* prv_get_writable_extended_layout(GTextLayoutCacheRef layout) {
  PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk()); // should not get here if 2.X
  PBL_ASSERTN(layout);
  // Invalidate the hash to ensure the layout gets updated when prv_graphics_text_layout_update is
  // called on the layout
  layout->hash = 0;
  return (TextLayoutExtended *)layout;
}

static TextLayoutExtended* prv_get_readable_extended_layout(GTextLayoutCacheRef layout) {
  if (!layout || process_manager_compiled_with_legacy2_sdk()) {
    return NULL;
  }
  return (TextLayoutExtended*)layout;
}


void graphics_text_layout_set_line_spacing_delta(GTextLayoutCacheRef layout, int16_t delta) {
  TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
  if (extended) {
    extended->line_spacing_delta = delta;
  }
}

int16_t graphics_text_layout_get_line_spacing_delta(const GTextLayoutCacheRef layout) {
  return prv_layout_get_line_spacing_delta(layout);
}

void graphics_text_attributes_restore_default_text_flow(GTextLayoutCacheRef layout) {
  TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
  if (!extended) {
    return;
  }
  extended->flow_data.perimeter.impl = NULL;
}

// this way, we don't need to pull in all the dependencies when doing unit-tests
// if you want to test this aspect, just define the symbol below in your test_*.c file
#if !defined(UNITTEST)
#define USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT
#endif

void graphics_text_attributes_enable_screen_text_flow(GTextLayoutCacheRef layout, uint8_t inset) {
  TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
  if (!extended) {
    return;
  }

#if defined(USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT)
  // on rectangular screens, we can just leave the perimeter blank when we don't need an inset
  const GPerimeter *shortcut_perimeter = PBL_IF_ROUND_ELSE(g_perimeter_for_display, NULL);
  const GPerimeter *perimeter = inset > 0 ? g_perimeter_for_display : shortcut_perimeter;
#else
  const GPerimeter *perimeter = NULL;
#endif

  extended->flow_data.perimeter = (TextLayoutFlowDataPerimeter) {
    .impl = perimeter,
    .inset = inset,
  };
}

void graphics_text_attributes_restore_default_paging(GTextLayoutCacheRef layout) {
  TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
  if (!extended) {
    return;
  }
  extended->flow_data.paging.page_on_screen.size_h = 0;
}

void graphics_text_attributes_enable_paging(
  GTextLayoutCacheRef layout, GPoint content_origin_on_screen, GRect paging_on_screen) {
  TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
  if (extended) {
    extended->flow_data.paging = (TextLayoutFlowDataPaging) {
      .origin_on_screen = content_origin_on_screen,
      .page_on_screen.origin_y = paging_on_screen.origin.y,
      .page_on_screen.size_h = paging_on_screen.size.h,
    };
  }
}

const TextLayoutFlowData *graphics_text_layout_get_flow_data(GTextLayoutCacheRef layout) {
  TextLayoutExtended *extended_layout = prv_get_readable_extended_layout(layout);
  if (extended_layout) {
    return &extended_layout->flow_data;
  } else {
    static const TextLayoutFlowData s_default_data = {
      // yes, this is basically just an empty struct but I want to be explicit here:
      .perimeter.impl = NULL, // no perimeter/inset configured
      .paging.page_on_screen.size_h = 0, // no paging or origin
    };
    return &s_default_data;
  }
}
