#' Remove embedded pagedtable HTML artifacts from a vector of lines
#'
#' VS Code's interactive chunk execution can cache a chunk's rendered output
#' directly back into the .qmd/.Rmd source file. When the chunk's output is a
#' tibble, that cached output is a pagedtable HTML widget (a `<div
#' data-pagedtable="false">` block wrapping a `<script
#' data-pagedtable-source>` JSON blob). Because this cached output can end up
#' inside the same fenced region as the code itself, show_file()'s chunk
#' extraction can otherwise return this HTML noise instead of clean source
#' code. This helper strips any such blocks out of a character vector of
#' lines before they are printed.
#'
#' @param lines A character vector of lines (as returned by readLines()).
#' @return A character vector with any pagedtable HTML blocks removed.
#' @keywords internal
strip_pagedtable_html <- function(lines) {
  if (length(lines) == 0) {
    return(lines)
  }

  keep <- rep(TRUE, length(lines))
  depth <- 0
  in_block <- FALSE

  for (i in seq_along(lines)) {
    line <- lines[i]

    # A pagedtable block always starts with a <div data-pagedtable...> tag.
    if (!in_block && grepl("^\\s*<div\\s+data-pagedtable", line)) {
      in_block <- TRUE
      depth <- 0
    }

    if (in_block) {
      keep[i] <- FALSE
      # Track nested <div ...> / </div> tags so we find the *matching*
      # closing tag rather than the first </div> we see.
      depth <- depth + lengths(regmatches(line, gregexpr("<div\\b", line)))
      depth <- depth - lengths(regmatches(line, gregexpr("</div>", line)))
      if (depth <= 0) {
        in_block <- FALSE
      }
    }
  }

  lines[keep]
}

#' Display the contents of a text file that match a pattern
#'
#' This function reads the contents of a text file and either prints the specified range of rows
#' that match a given regular expression pattern, prints the code lines within code chunks,
#' or extracts the YAML header. If start is a negative number, it prints the last abs(start) lines,
#' ignoring missing lines at the end of the file. If start is 0, it prints the entire file.
#'
#' The arguments are resolved in a fixed order of precedence: `chunk = "YAML"`
#' is handled first, then `start == 0` (whole file), then `start < 0` (last
#' abs(start) lines), then `chunk %in% c("All", "Last")` (code chunks), and
#' finally the `start`/`end` row range. The `pattern` filter is applied within
#' the whole-file, last-lines, and row-range cases, but is ignored when `chunk`
#' selects code chunks or the YAML header.
#'
#' @param path A character vector representing the path to the text file.
#' @param start An integer specifying the starting row number (inclusive) to consider. Default is 1.
#'              If negative, it represents the number of lines to print from the end of the file.
#'              If 0, prints the entire file.
#' @param end An integer specifying the ending row number (inclusive) to consider. Default is the last row.
#' @param pattern A regular expression pattern to match against each row. Default is NULL (no pattern matching).
#'              Applied to the whole-file (`start == 0`), last-lines (`start < 0`), and row-range cases;
#'              ignored when `chunk` is "All", "Last", or "YAML".
#' @param chunk A character string indicating what content to extract.
#'              Possible values are "None" (default - no chunk processing),
#'              "All" (print all code chunks), "Last" (print only the last code chunk),
#'              or "YAML" (extract the YAML header without delimiters). Code chunks of
#'              any language are recognized (e.g. `r`, `python`, `bash`), not just R.
#' @return The function prints the contents of the specified range of rows that match the pattern (if provided),
#'         the code lines within R code chunks (if chunk is "All" or "Last"),
#'         or the YAML header content (if chunk is "YAML") to the console. If no rows match the pattern,
#'         nothing is printed. If start is negative, the function prints the last abs(start) lines, ignoring
#'         missing lines at the end of the file. If start is 0, the function prints the entire file.
#'
#' @examples
#' \dontrun{
#' # Display all rows of a text file
#' show_file("path/to/your/file.txt")
#'
#' # Display the entire file
#' show_file("path/to/your/file.txt", start = 0)
#'
#' # Display rows 5 to 10 of a text file
#' show_file("path/to/your/file.txt", start = 5, end = 10)
#'
#' # Display all rows of a text file that contain the word "example"
#' show_file("path/to/your/file.txt", pattern = "example")
#'
#' # Print all code lines within R code chunks
#' show_file("path/to/your/file.txt", chunk = "All")
#'
#' # Print only the last R code chunk
#' show_file("path/to/your/file.txt", chunk = "Last")
#'
#' # Extract the YAML header
#' show_file("path/to/your/file.Rmd", chunk = "YAML")
#'
#' # Display the last 5 lines of a text file, ignoring missing lines at the end
#' show_file("path/to/your/file.txt", start = -5)
#' }
#'
#' @export

show_file <- function(path, start = 1, end = NULL, pattern = NULL, chunk = "None") {
  # Check if the file exists
  if (!file.exists(path)) {
    stop("File does not exist.")
  }
  
  # Validate chunk parameter
  allowed_chunks <- c("None", "All", "Last", "YAML")
  if (!chunk %in% allowed_chunks) {
    stop("chunk must be one of: ", paste(allowed_chunks, collapse = ", "))
  }
  
  # Read the contents of the file
  contents <- readLines(path)

  # VS Code's interactive chunk execution can cache a chunk's rendered
  # output directly back into the .qmd/.Rmd source file. When the chunk's
  # output is a tibble, that cached output is a pagedtable HTML widget (a
  # `<div data-pagedtable="false">` block wrapping a `<script
  # data-pagedtable-source>` JSON blob), and it can end up inside the same
  # fenced region as the code itself. Strip any such blocks here, up front,
  # so every code path below (YAML, whole-file, tail, chunk extraction, and
  # row-range) sees clean source text rather than rendered-output noise.
  contents <- strip_pagedtable_html(contents)
  
  # Remove trailing empty lines from the contents
  while (length(contents) > 0 && contents[length(contents)] == "") {
    contents <- contents[-length(contents)]
  }

  if (length(contents) == 0) {
    cat("File is empty.\n")
    return(invisible(NULL))
  }

  # If chunk is "YAML", extract and return the YAML header
  if (chunk == "YAML") {
    if (length(contents) == 0 || !grepl("^---\\s*$", contents[1])) {
      stop("No YAML header found.")
    }
    
    # Find the closing --- for the YAML header. seq_len(length(contents))[-1]
    # gives the indices from 2 to the end, and is empty (rather than c(2, 1))
    # when the file has only a single line.
    yaml_end <- NULL
    for (i in seq_len(length(contents))[-1]) {
      if (grepl("^---\\s*$", contents[i])) {
        yaml_end <- i
        break
      }
    }
    
    if (is.null(yaml_end)) {
      stop("No YAML header found.")
    }
    
    # Extract YAML content (excluding the --- delimiters)
    if (yaml_end > 2) {
      yaml_content <- contents[2:(yaml_end - 1)]
      cat(yaml_content, sep = "\n")
    }
    return(invisible(NULL))
  }
  
  # If start is 0, print the entire file
  if (start == 0) {
    selected_contents <- contents
    if (!is.null(pattern)) {
      selected_contents <- selected_contents[grepl(pattern, selected_contents)]
    }
    if (length(selected_contents) > 0) {
      cat(selected_contents, sep = "\n")
    }
    return(invisible(NULL))
  }

  # If start is negative, print the last abs(start) lines
  if (start < 0) {
    selected_contents <- utils::tail(contents, abs(start))
    if (!is.null(pattern)) {
      selected_contents <- selected_contents[grepl(pattern, selected_contents)]
    }
    if (length(selected_contents) > 0) {
      cat(selected_contents, sep = "\n")
    }
    return(invisible(NULL))
  }
  
  # If chunk is "All" or "Last", print code lines within code chunks. We match
  # the opening fence of any language (```{r}, ```{python}, ```{bash}, ...),
  # not just R, so that "Last"/"All" work for every kind of tutorial chunk.
  if (chunk %in% c("All", "Last")) {
    in_chunk <- FALSE
    code_chunks <- list()
    current_chunk <- character()

    for (line in contents) {
      if (grepl("^```\\{", line)) {
        in_chunk <- TRUE
        if (length(current_chunk) > 0) {
          code_chunks <- c(code_chunks, list(current_chunk))
          current_chunk <- character()
        }
      } else if (grepl("^```\\s*$", line)) {
        in_chunk <- FALSE
        if (length(current_chunk) > 0) {
          code_chunks <- c(code_chunks, list(current_chunk))
          current_chunk <- character()
        }
      } else if (in_chunk) {
        current_chunk <- c(current_chunk, line)
      }
    }
    
    if (chunk == "All") {
      for (i in seq_along(code_chunks)) {
        cat(code_chunks[[i]], sep = "\n")
        if (i < length(code_chunks)) {
          cat("\n")
        }
      }
    } else if (chunk == "Last") {
      if (length(code_chunks) > 0) {
        cat(code_chunks[[length(code_chunks)]], sep = "\n")
      }
    }
    return(invisible(NULL))
  }
  
  # Get the total number of rows
  total_rows <- length(contents)
  
  # Set default value for end if not provided
  if (is.null(end)) {
    end <- total_rows
  }
  
  # Check if start and end are within the valid range
  if (start < 1 || start > total_rows || end < 1 || end > total_rows) {
    stop("start and end must be within the valid range of rows.")
  }
  
  # Check if start is smaller or equal to end
  if (start > end) {
    stop("start must be smaller or equal to end.")
  }
  
  # Extract the specified range of rows
  selected_contents <- contents[start:end]
  
  # Filter the selected rows based on the pattern (if provided)
  if (!is.null(pattern)) {
    selected_contents <- selected_contents[grepl(pattern, selected_contents)]
  }
  
  # Print the selected contents to the console if there are matching rows
  if (length(selected_contents) > 0) {
    cat(selected_contents, sep = "\n")
  }
}