#' Clean Typst style string
#' @keywords internal
#' @noRd
typst_clean_css <- function(css) {
  css <- gsub(" +", " ", trimws(css))
  css <- sub("^,", "", trimws(css))
  css <- gsub(",+", ",", trimws(css))
  return(css)
}

#' Vectorized CSS builder for the typst style-array
#'
#' Replaces the per-cell for-loop that called typst_insert_field() (which does
#' regex sub() on a growing string per property) with a single pass that builds
#' each cell's "key: value, key: value, ..." string directly. Output is
#' byte-identical to the concatenation of sequential typst_insert_field()
#' calls followed by typst_clean_css().
#'
#' @param other Filtered @style_other data frame (only rows with at least one style)
#' @param color_map Named character vector mapping original color names to typst rgb() strings
#' @return Character vector of css strings (one per row of `other`), un-trimmed
#' @keywords internal
#' @noRd
.typst_build_css_vectorized <- function(other, color_map) {
  n <- nrow(other)
  if (n == 0) return(character(0))

  bold        <- !is.na(other$bold)       & other$bold
  italic      <- !is.na(other$italic)     & other$italic
  underline   <- !is.na(other$underline)  & other$underline
  strikeout   <- !is.na(other$strikeout)  & other$strikeout
  monospace   <- !is.na(other$monospace)  & other$monospace
  smallcap    <- !is.na(other$smallcap)   & other$smallcap
  color_set   <- !is.na(other$color)
  bg_set      <- !is.na(other$background)
  fs_set      <- !is.na(other$fontsize)
  indent_set  <- !is.na(other$indent)     & other$indent > 0
  align_h_set <- !is.na(other$align)
  align_v_set <- !is.na(other$alignv)

  css <- character(n)

  # scalar emission: same string for every matching cell
  add_scalar <- function(mask, key, val) {
    if (!any(mask)) return()
    idx <- which(mask)
    fragment <- sprintf("%s: %s", key, val)
    sep <- ifelse(nzchar(css[idx]), ", ", "")
    css[idx] <<- paste0(css[idx], sep, fragment)
  }
  # vector emission: val is parallel to `other`; only used where mask is TRUE
  add_vec <- function(mask, key, val) {
    if (!any(mask)) return()
    idx <- which(mask)
    fragment <- sprintf("%s: %s", key, val[idx])
    sep <- ifelse(nzchar(css[idx]), ", ", "")
    css[idx] <<- paste0(css[idx], sep, fragment)
  }

  add_scalar(bold, "bold", "true")
  add_scalar(italic, "italic", "true")
  add_scalar(underline, "underline", "true")
  add_scalar(strikeout, "strikeout", "true")
  add_scalar(monospace, "mono", "true")
  add_scalar(smallcap, "smallcaps", "true")

  if (any(color_set)) {
    cv <- character(n)
    cv[color_set] <- color_map[other$color[color_set]]
    add_vec(color_set, "color", cv)
  }
  if (any(bg_set)) {
    bv <- character(n)
    bv[bg_set] <- color_map[other$background[bg_set]]
    add_vec(bg_set, "background", bv)
  }
  if (any(fs_set)) {
    fv <- character(n)
    fv[fs_set] <- vapply(
      other$fontsize[fs_set],
      format_markup_unit,
      character(1),
      "em"
    )
    add_vec(fs_set, "fontsize", fv)
  }

  if (any(align_h_set) || any(align_v_set)) {
    ah <- ifelse(is.na(other$align),  "", other$align)
    av <- ifelse(is.na(other$alignv), "", other$alignv)
    combined <- ifelse(nchar(ah) > 0 & nchar(av) > 0,
                       paste(ah, av, sep = " + "),
                       paste0(ah, av))
    mask <- align_h_set | align_v_set
    add_vec(mask, "align", combined)
  }

  if (any(indent_set)) {
    iv <- character(n)
    iv[indent_set] <- vapply(
      other$indent[indent_set],
      format_markup_unit,
      character(1),
      "em"
    )
    add_vec(indent_set, "indent", iv)
  }

  # Each cell becomes a comma-separated list with a trailing comma, matching
  # the original typst_insert_field() + typst_clean_css() output format.
  ifelse(nzchar(css), paste0(css, ","), css)
}

#' Apply Typst styles and generate style-dict/array
#' @keywords internal
#' @noRd
typst_apply_styles <- function(x, rec) {
  # Filter out empty styles
  idx <- rec$css != ""
  rec <- rec[idx, , drop = FALSE]
  if (nrow(rec) == 0) {
    return(x)
  }

  # Get unique styles. split(rec, rec$css) sorted by CSS value; preserve that
  # order without copying one data frame per style group.
  style_keys <- sort(unique(rec$css))
  style_idx <- match(rec$css, style_keys)

  # Create style-array (unique styles)
  style_array_entries <- sprintf("(%s),", style_keys)

  # Create style-dict (cell positions -> style array indices) using vectorized
  # sprintf instead of growing a vector inside a for loop. Each unique style
  # group can produce many cell-position keys, so we pre-allocate a list and
  # unlist() once.
  style_dict_list <- vector("list", length(style_keys))
  for (idx in seq_along(style_keys)) {
    rows <- which(style_idx == idx)
    style_dict_list[[idx]] <- sprintf('"%s_%s": %s', rec$i[rows], rec$j[rows], idx - 1)
  }
  style_dict_entries <- unlist(style_dict_list, use.names = FALSE)

  # Insert style-dict entries as single line
  if (length(style_dict_entries) > 0) {
    combined_dict <- paste0(
      "    ",
      paste(style_dict_entries, collapse = ", ")
    )
    x@table_string <- lines_insert(
      x@table_string,
      combined_dict,
      "tinytable style-dict after",
      "after"
    )
  }

  # Insert all style-array entries in one pass over the table string. The old
  # loop inserted rev(style_array_entries) after the same marker, which
  # produced style_array_entries order in the final output.
  if (length(style_array_entries) > 0) {
    x@table_string <- lines_insert(
      x@table_string,
      paste0("    ", style_array_entries, collapse = "\n"),
      "tinytable cell style after",
      "after"
    )
  }

  return(x)
}




#' Convert tinytable indexing to Typst 0-based indexing
#'
#' tinytable: 0=colnames, -1=group1, -2=group2, ..., 1,2,3=data
#' Typst: group headers come first (0,1,...), then colnames, then data
#' Need to account for the fact that group headers are inserted at the top
#' @param df Data frame with `i` (row) and `j` (column) tinytable indices
#' @param nhead Number of header rows (colnames + column group rows)
#' @keywords internal
#' @noRd
typst_shift_indices <- function(df, nhead) {
  if (nhead > 0) {
    # Case with headers/colnames: normal conversion
    df$i <- ifelse(df$i < 0,
                   nhead + df$i - 1,  # Headers: -1 becomes nhead-2, -2 becomes nhead-3
                   df$i + nhead - 1)  # Column names (0) and data: 0 becomes nhead-1, 1 becomes nhead
  } else {
    # Case with no headers (nhead = 0): filter out i=0 (non-existent colnames), then convert
    df <- df[df$i > 0, , drop = FALSE]  # Remove i=0 entries
    df$i <- df$i - 1  # Data rows: 1 becomes 0, 2 becomes 1, etc.
  }
  df$j <- df$j - 1
  return(df)
}

typst_split_chunks <- function(x) {
  x <- sort(x)
  breaks <- c(0, which(diff(x) != 1), length(x))
  result <- list()
  for (i in seq_along(breaks)[-length(breaks)]) {
    chunk <- x[(breaks[i] + 1):breaks[i + 1]]
    result[[i]] <- c(min = min(chunk), max = max(chunk))
  }
  out <- data.frame(do.call(rbind, result))
  out$max <- out$max + 1
  return(out)
}

#' Build a Typst stroke specification for a rule
#'
#' Solid rules keep the compact `<width> + <color>` form so that existing
#' output is unchanged. Dashed and dotted rules require the dictionary form,
#' which is the only way to pass `dash:`.
#' @keywords internal
#' @noRd
typst_line_type <- function(lin) {
  out <- if ("line_type" %in% names(lin)) lin$line_type else NA_character_
  ifelse(is.na(out), "solid", out)
}

# A `color` of NA means the user did not request a specific paint. We then omit
# it from the stroke entirely: Typst stroke folding fills the missing field from
# the surrounding `#set` rules, so the document's global styling wins instead of
# being overridden by a hard-coded black. See Issue #679.
typst_stroke <- function(width, color, type) {
  if (is.na(type) || identical(type, "solid")) {
    if (is.na(color)) width else sprintf("%s + %s", width, color)
  } else if (is.na(color)) {
    sprintf('(thickness: %s, dash: "%s")', width, type)
  } else {
    sprintf('(thickness: %s, paint: %s, dash: "%s")', width, color, type)
  }
}

# split() drops NA levels, which would silently discard every line whose paint
# is left to the document. Map NA to a sentinel before using it as a split key.
typst_color_key <- function(color) {
  ifelse(is.na(color), "__inherit__", color)
}

# When the very same rule is declared twice -- once with an inherited paint (by
# theme_tinytable() or group_tt()) and once with an explicit color (by the user)
# -- the explicit declaration wins. Without this, the two entries land in
# different split() groups and two overlapping table.hline() calls are emitted
# for one visual rule. See test-bugfix.R.
typst_drop_inherited_dupes <- function(lin) {
  inherited <- is.na(lin$line_color_mapped)
  if (!any(inherited) || all(inherited)) {
    return(lin)
  }
  key_cols <- intersect(
    c("i", "j", "line", "line_width", "line_type", "line_trim"),
    colnames(lin)
  )
  key <- do.call(paste, c(lin[key_cols], sep = "\r"))
  lin[!(inherited & key %in% key[!inherited]), , drop = FALSE]
}

typst_hlines <- function(x, lin) {
  if (nrow(lin) == 0) {
    return(x)
  }

  # Normalize colors once before splitting
  lin$line_color_mapped <- normalize_colors(lin$line_color, "typst", default = NA_character_)

  lin$line_type <- typst_line_type(lin)

  lin <- typst_drop_inherited_dupes(lin)

  tmp <- split(
    lin,
    list(lin$i, lin$line, typst_color_key(lin$line_color_mapped), lin$line_width, lin$line_type),
    drop = TRUE
  )
  tmp <- lapply(tmp, function(k) {
    # Drop duplicate cell-level entries. These arise routinely whenever
    # theme_tinytable() and a user style_tt() declare the same line (same
    # i, j, line, color, width, trim). Without deduplication,
    # typst_split_chunks() sees phantom breaks in diff(x) != 1 and emits a
    # chain of fragmented table.hline()/table.vline() segments that cover
    # the same span as a single line. See test-typst-line-dedup.R.
    k <- k[!duplicated(k), , drop = FALSE]

    # Split chunks based on consecutive columns AND line_trim boundaries
    # line_trim marks group boundaries - split chunks at these points
    chunks <- typst_split_chunks(k$j)

    # Further split chunks based on line_trim markers
    # line_trim="l" or "lr" means start of a new group
    # line_trim="r" or "lr" means end of current group
    final_chunks <- list()
    for (chunk_idx in seq_len(nrow(chunks))) {
      chunk_start <- chunks$min[chunk_idx]
      chunk_end <- chunks$max[chunk_idx]
      chunk_cols <- chunk_start:(chunk_end - 1)  # -1 because max is exclusive

      # Find trim boundaries within this chunk
      chunk_rows <- k[k$j %in% chunk_cols, , drop = FALSE]
      chunk_rows <- chunk_rows[order(chunk_rows$j), ]

      # Split on line_trim boundaries
      current_start <- chunk_start
      for (i in seq_len(nrow(chunk_rows))) {
        trim_val <- chunk_rows$line_trim[i]
        col_val <- chunk_rows$j[i]

        # Check if this is a boundary
        if (!is.na(trim_val)) {
          if (grepl("r", trim_val)) {
            # End of current segment; the next one starts after this column
            final_chunks <- c(final_chunks, list(c(min = current_start, max = col_val + 1)))
            current_start <- col_val + 1
          } else if (grepl("l", trim_val) && col_val > current_start) {
            # Start of new segment (close previous if any)
            final_chunks <- c(final_chunks, list(c(min = current_start, max = col_val)))
            current_start <- col_val
          }
        }

        # If last column in chunk, close the segment
        if (i == nrow(chunk_rows) && col_val + 1 >= chunk_end) {
          if (current_start < chunk_end) {
            final_chunks <- c(final_chunks, list(c(min = current_start, max = chunk_end)))
          }
        }
      }
    }

    # If no trim markers found, use original chunks
    if (length(final_chunks) == 0) {
      final_chunks <- lapply(seq_len(nrow(chunks)), function(i) c(min = chunks$min[i], max = chunks$max[i]))
    }

    # Drop zero-width segments and duplicates so each span is drawn once
    final_chunks <- Filter(function(chunk) chunk["min"] < chunk["max"], final_chunks)
    final_chunks <- unique(final_chunks)

    ymin <- k$i[1]
    ymax <- k$i[1] + 1
    line <- k$line[1]
    color <- k$line_color_mapped[1]
    width <- if (is.na(k$line_width[1])) 0.1 else k$line_width[1]
    width <- format_markup_unit(width, "em")
    stroke <- typst_stroke(width, color, k$line_type[1])
    out <- ""

    # Generate hlines for each chunk
    for (chunk in final_chunks) {
      if (grepl("t", line)) {
        tmp <- "table.hline(y: %s, start: %s, end: %s, stroke: %s),"
        tmp <- sprintf(tmp, ymin, chunk["min"], chunk["max"], stroke)
        out <- paste(out, tmp)
      }
      if (grepl("b", line)) {
        tmp <- "table.hline(y: %s, start: %s, end: %s, stroke: %s),"
        tmp <- sprintf(tmp, ymax, chunk["min"], chunk["max"], stroke)
        out <- paste(out, tmp)
      }
    }
    return(out)
  })
  if (length(tmp) > 0) {
    x@table_string <- lines_insert(
      x@table_string,
      paste(unlist(tmp, use.names = FALSE), collapse = "\n"),
      "tinytable lines before",
      "before"
    )
  }
  return(x)
}

# =============================================================================
# NEW SIMPLIFIED FUNCTIONS USING expand_style()
# =============================================================================

#' Process Typst line styles using expanded data
#' @keywords internal
#' @noRd
process_typst_lines <- function(x, lines) {
  if (is.null(lines) || nrow(lines) == 0) {
    return(x)
  }

  lines <- typst_shift_indices(lines, x@nhead)

  # Process horizontal and vertical lines
  x <- typst_hlines(x, lines)
  x <- typst_vlines(x, lines)

  return(x)
}

#' Process Typst other styles using expanded data
#' @keywords internal
#' @noRd
process_typst_other_styles <- function(x, other) {
  if (is.null(other) || nrow(other) == 0) {
    return(x)
  }

  # Map alignments to Typst format
  other$align <- map_align(other$align, "typst")
  other$alignv <- map_alignv(other$alignv, "typst")

  # Normalize colors once
  color_map <- build_color_map(c(other$color, other$background), "typst")

  # Generate CSS for each cell. The reference implementation builds one cell
  # at a time by repeatedly calling typst_insert_field(), which uses regex
  # sub() on a growing string. The vectorized version below produces an
  # identical result but is much faster on tables with many styled cells
  # (heat-map backgrounds, per-cell alignment, etc.). The output strings are
  # joined with ", " and given a trailing comma to match the original
  # typst_insert_field() format exactly (typst_clean_css() collapses runs
  # of commas and trims whitespace, so byte-identical output is achievable).
  css <- .typst_build_css_vectorized(other, color_map)

  # Clean CSS and add to data frame
  other$css <- typst_clean_css(css)

  other <- typst_shift_indices(other, x@nhead)

  # Generate style-dict and style-array for optimized lookup
  x <- typst_apply_styles(x, other)

  return(x)
}

typst_vlines <- function(x, lin) {
  lin <- lin[grepl("l|r", lin$line), , drop = FALSE]
  if (nrow(lin) == 0) {
    return(x)
  }

  # Normalize colors once before splitting
  lin$line_color_mapped <- normalize_colors(lin$line_color, "typst", default = NA_character_)

  lin$line_type <- typst_line_type(lin)

  lin <- typst_drop_inherited_dupes(lin)

  lin <- split(
    lin,
    list(lin$j, lin$line, typst_color_key(lin$line_color_mapped), lin$line_width, lin$line_type),
    drop = TRUE
  )
  lin <- lapply(lin, function(k) {
    # Drop duplicate cell-level entries; see typst_hlines() for rationale.
    k <- k[!duplicated(k), , drop = FALSE]
    chunks <- typst_split_chunks(k$i)
    ymin <- chunks$min
    ymax <- chunks$max
    xmin <- k$j[1]
    xmax <- xmin + 1
    line <- k$line[1]
    color <- k$line_color_mapped[1]
    width <- if (is.na(k$line_width[1])) 0.1 else k$line_width[1]
    width <- format_markup_unit(width, "em")
    stroke <- typst_stroke(width, color, k$line_type[1])
    out <- ""
    if (grepl("l", line)) {
      tmp <- "table.vline(x: %s, start: %s, end: %s, stroke: %s),"
      tmp <- sprintf(tmp, xmin, ymin, ymax, stroke)
      out <- paste(out, tmp)
    }
    if (grepl("r", line)) {
      tmp <- "table.vline(x: %s, start: %s, end: %s, stroke: %s),"
      tmp <- sprintf(tmp, xmax, ymin, ymax, stroke)
      out <- paste(out, tmp)
    }
    return(out)
  })
  if (length(lin) > 0) {
    x@table_string <- lines_insert(
      x@table_string,
      paste(unlist(lin, use.names = FALSE), collapse = "\n"),
      "tinytable lines before",
      "before"
    )
  }
  return(x)
}


#' Internal styling function
#'
#' @inheritParams style_tt
#' @keywords internal
#' @noRd
setMethod(
  f = "style_eval",
  signature = "tinytable_typst",
  definition = function(
    x,
    i = NULL,
    j = NULL,
    bold = FALSE,
    italic = FALSE,
    monospace = FALSE,
    underline = FALSE,
    strikeout = FALSE,
    color = NULL,
    background = NULL,
    fontsize = NULL,
    align = NULL,
    alignv = NULL,
    line = NULL,
    line_color = NA_character_,
    line_width = 0.1,
    indent = 0,
    midrule = FALSE, # undocumented, only used by `group_tt()`
    ...
  ) {
    # Use populated @style_other / @style_lines from build_tt()
    inputs <- style_backend_inputs(x, STYLE_PROPS_TYPST)
    other <- inputs$other
    lines <- inputs$lines

    # Column-group underlines are drawn inside the header cells by
    # typst_build_group_header(); skip them here to avoid drawing them twice.
    consumed <- typst_group_line_rules(x)$consumed
    if (!is.null(lines) && length(consumed) == nrow(lines) && any(consumed)) {
      lines <- lines[!consumed, , drop = FALSE]
      if (nrow(lines) == 0) {
        lines <- NULL
      }
    }

    # Process lines using the expanded data
    x <- process_typst_lines(x, lines)

    # Process other styles using the expanded data
    x <- process_typst_other_styles(x, other)

    return(x)
  }
)
