# LifeTableBuilder Shiny App (CRAN-friendly: no library(), uses pkg::fun)
# Place this file at: inst/shiny/app.R

# ----------------------------
# Core life-table calculation (nQx-based)
# ----------------------------
calc_life_table_nqx <- function(df, radix = 100000) {
  df <- df[order(df$x), ]
  n_int <- nrow(df)
  
  lx  <- numeric(n_int)
  ndx <- numeric(n_int)
  Lx  <- numeric(n_int)
  Tx  <- numeric(n_int)
  ex  <- numeric(n_int)
  
  lx[1] <- radix
  
  for (i in seq_len(n_int)) {
    ndx[i] <- lx[i] * df$nQx[i]
    if (i < n_int) lx[i + 1] <- lx[i] - ndx[i]
  }
  
  if (n_int > 1) {
    for (i in 1:(n_int - 1)) {
      Lx[i] <- df$n[i] * (lx[i] + lx[i + 1]) / 2
    }
  }
  Lx[n_int] <- df$n[n_int] * lx[n_int] / 2
  
  Tx[n_int] <- Lx[n_int]
  if (n_int > 1) {
    for (i in (n_int - 1):1) Tx[i] <- Tx[i + 1] + Lx[i]
  }
  
  for (i in seq_len(n_int)) ex[i] <- ifelse(lx[i] > 0, Tx[i] / lx[i], NA)
  
  df$lx  <- lx
  df$ndx <- ndx
  df$Lx  <- Lx
  df$Tx  <- Tx
  df$ex  <- ex
  
  list(
    table = df,
    summary = list(radix = radix, e0 = ex[1])
  )
}

# ----------------------------
# Example data
# ----------------------------
example_stages <- data.frame(
  Stage     = c("Egg", "L1", "L2", "L3", "L4", "Pupa"),
  N_initial = c(100, 80, 77, 65, 61, 60),
  N_pass    = c(80, 77, 65, 61, 60, 60),
  Duration  = c(3.7, 2.3, 2.7, 3.6, 3.8, 7.5),
  stringsAsFactors = FALSE
)

# ----------------------------
# UI
# ----------------------------
ui <- shiny::fluidPage(
  shiny::tags$head(
    shiny::tags$style(shiny::HTML('
      summary.btn-summary{
        display:inline-block;
        padding:6px 12px;
        margin:4px 0;
        font-weight:600;
        background:#e0e0e0;
        border-radius:4px;
        cursor:pointer;
        border:1px solid #b0b0b0
      }
      details[open]>summary.btn-summary{background:#c8c8c8}
    '))
  ),
  
  shiny::titlePanel("Stage-Structured Life Table"),
  
  shiny::sidebarLayout(
    shiny::sidebarPanel(
      shiny::h4("Data input"),
      shiny::fileInput("file", "Upload CSV or Excel",
                       accept = c(".csv", ".CSV", ".xls", ".xlsx")),
      shiny::checkboxInput("header", "CSV has header", TRUE),
      shiny::radioButtons(
        "sep", "CSV separator",
        choices = c(Comma = ",", Semicolon = ";", Tab = "\t"),
        selected = ","
      ),
      shiny::numericInput("radix", "Radix (lx0) / initial cohort size",
                          value = 100, min = 1, step = 1),
      
      shiny::tags$hr(),
      shiny::checkboxInput("use_example", "Use example data", TRUE),
      shiny::tags$small("If checked, the uploaded file is ignored."),
      
      shiny::tags$hr(),
      shiny::tags$details(
        shiny::tags$summary(class = "btn-summary", "Column mapping (optional)"),
        shiny::uiOutput("col_select")
      )
    ),
    
    shiny::mainPanel(
      shiny::tabsetPanel(
        shiny::tabPanel("Input data",
                        shiny::br(),
                        DT::DTOutput("table_input")),
        
        shiny::tabPanel(
          "Life table",
          shiny::br(),
          shiny::h4("Calculated life table"),
          DT::DTOutput("table_life"),
          shiny::br(),
          shiny::downloadButton("download_life_table", "Download life table (.pdf)"),
          shiny::br(), shiny::br(),
          shiny::h4("Summary"),
          shiny::verbatimTextOutput("summary_out")
        ),
        
        shiny::tabPanel(
          "Survival curve",
          shiny::br(),
          shiny::plotOutput("plot_ex"),
          shiny::br(),
          shiny::downloadButton("download_plot_ex", "Download survival curve (.png)")
        )
      ),
      shiny::tags$hr(),
      shiny::tags$p(
        shiny::tags$strong("How to cite: "),
        "Almaraz Valle, V. M., Rodríguez Maciel, J. C., Ramírez Valverde, G., Aguilar Castillo, C. E. & Pérez Panduro, A. (2026). LifeTableBuilder: Interactive Shiny Application for Constructing Life Tables. R package version 0.1.2."
      )
    )
  )
)

# ----------------------------
# Server
# ----------------------------
server <- function(input, output, session) {
  
  data_input <- shiny::reactive({
    if (isTRUE(input$use_example) || is.null(input$file)) return(example_stages)
    
    ext <- tools::file_ext(input$file$name)
    if (tolower(ext) %in% c("xls", "xlsx")) {
      as.data.frame(readxl::read_excel(input$file$datapath))
    } else {
      read.table(
        input$file$datapath,
        header = isTRUE(input$header),
        sep = input$sep,
        dec = ".",
        stringsAsFactors = FALSE
      )
    }
  })
  
  output$table_input <- DT::renderDT({
    df <- data_input()
    shiny::validate(shiny::need(!is.null(df), "No data available."))
    DT::datatable(df, options = list(pageLength = 10))
  })
  
  output$col_select <- shiny::renderUI({
    df <- data_input()
    if (is.null(df)) return(NULL)
    
    cols <- names(df)
    guess <- function(pattern, default_index) {
      m <- grep(pattern, cols, ignore.case = TRUE, value = TRUE)
      if (length(m) > 0) m[1] else cols[min(default_index, length(cols))]
    }
    
    shiny::tagList(
      shiny::selectInput("col_stage", "Column: Stage",
                         choices = cols, selected = guess("stage|estad", 1)),
      shiny::selectInput("col_Nini", "Column: N_initial",
                         choices = cols, selected = guess("n[_ ]?ini|initial|ini", 2)),
      shiny::selectInput("col_Nnext", "Column: N_pass",
                         choices = cols, selected = guess("pass|pasan|next", 3)),
      shiny::selectInput("col_dur", "Column: Duration (days)",
                         choices = cols, selected = guess("dur|duration", 4))
    )
  })
  
  life_results <- shiny::reactive({
    df <- data_input()
    shiny::validate(shiny::need(!is.null(df), "No data available."))
    
    cols <- names(df)
    guess <- function(pattern, default_index) {
      m <- grep(pattern, cols, ignore.case = TRUE, value = TRUE)
      if (length(m) > 0) m[1] else cols[min(default_index, length(cols))]
    }
    
    col_stage <- if (!is.null(input$col_stage)) input$col_stage else guess("stage|estad", 1)
    col_Nini  <- if (!is.null(input$col_Nini))  input$col_Nini  else guess("n[_ ]?ini|initial|ini", 2)
    col_Nnext <- if (!is.null(input$col_Nnext)) input$col_Nnext else guess("pass|pasan|next", 3)
    col_dur   <- if (!is.null(input$col_dur))   input$col_dur   else guess("dur|duration", 4)
    
    df_st <- data.frame(
      Stage     = as.character(df[[col_stage]]),
      N_initial = suppressWarnings(as.numeric(df[[col_Nini]])),
      N_pass    = suppressWarnings(as.numeric(df[[col_Nnext]])),
      Duration  = suppressWarnings(as.numeric(df[[col_dur]])),
      stringsAsFactors = FALSE
    )
    df_st <- df_st[!is.na(df_st$N_initial), ]
    
    shiny::validate(shiny::need(nrow(df_st) > 0, "No valid rows found after reading data."))
    shiny::validate(shiny::need(all(!is.na(df_st$N_pass)), "N_pass has missing values after numeric conversion."))
    shiny::validate(shiny::need(all(!is.na(df_st$Duration)), "Duration has missing values after numeric conversion."))
    shiny::validate(shiny::need(all(df_st$Duration >= 0), "Duration must be >= 0."))
    shiny::validate(shiny::need(all(df_st$N_initial >= 0), "N_initial must be >= 0."))
    shiny::validate(shiny::need(all(df_st$N_pass >= 0), "N_pass must be >= 0."))
    shiny::validate(shiny::need(all(df_st$N_pass <= df_st$N_initial), "Some rows have N_pass > N_initial."))
    
    df_st$nQx <- with(df_st, ifelse(N_initial > 0, (N_initial - N_pass) / N_initial, 0))
    df_st$n   <- df_st$Duration
    df_st$x   <- c(0, head(cumsum(df_st$n), -1))
    
    res <- calc_life_table_nqx(df_st[, c("x", "n", "nQx")], radix = input$radix)
    
    res$table$Stage     <- df_st$Stage
    res$table$N_initial <- df_st$N_initial
    res$table$N_pass    <- df_st$N_pass
    
    res$table <- res$table[, c("Stage", "x", "n", "nQx", "N_initial", "N_pass",
                               "lx", "ndx", "Lx", "Tx", "ex")]
    
    num_cols <- c("x", "n", "nQx", "lx", "ndx", "Lx", "Tx", "ex")
    for (cn in num_cols) res$table[[cn]] <- round(res$table[[cn]], 4)
    
    res
  })
  
  output$table_life <- DT::renderDT({
    DT::datatable(life_results()$table, options = list(pageLength = 20))
  })
  
  output$summary_out <- shiny::renderPrint({
    s <- life_results()$summary
    cat("Radix (lx0):", s$radix, "\n")
    cat("Initial life expectancy e(0):", sprintf("%.4f days\n", s$e0))
  })
  
  # --- PDF download: no citation legend, only title + table ---
  output$download_life_table <- shiny::downloadHandler(
    filename = function() paste0("life_table_", Sys.Date(), ".pdf"),
    content = function(file) {
      df_tab <- life_results()$table
      
      title_grob <- grid::textGrob(
        "Life table generated with LifeTableBuilder",
        gp = grid::gpar(fontsize = 16, fontface = "bold")
      )
      table_grob <- gridExtra::tableGrob(df_tab, rows = NULL)
      
      grDevices::pdf(file, width = 11, height = 8.5)
      gridExtra::grid.arrange(
        title_grob, table_grob,
        ncol = 1, heights = c(0.12, 0.88)
      )
      grDevices::dev.off()
    }
  )
  
  plot_ex_fun <- function(df) {
    dur_str <- format(round(df$n, 1), nsmall = 1)
    df$label_plot <- paste0(df$Stage, " (", dur_str, " d)")
    
    ggplot2::ggplot(df, ggplot2::aes(x = x, y = N_initial, label = label_plot)) +
      ggplot2::geom_point(size = 3) +
      ggplot2::geom_line() +
      ggplot2::geom_text(vjust = -0.7, size = 4.5) +
      ggplot2::theme_minimal() +
      ggplot2::labs(
        x = "Age (days since egg)",
        y = "Number of survivors",
        title = "Survival curve"
      ) +
      ggplot2::theme(
        plot.title = ggplot2::element_text(size = 18),
        axis.title = ggplot2::element_text(size = 16),
        axis.text  = ggplot2::element_text(size = 16)
      )
  }
  
  output$plot_ex <- shiny::renderPlot(plot_ex_fun(life_results()$table))
  
  output$download_plot_ex <- shiny::downloadHandler(
    filename = function() paste0("survival_curve_", Sys.Date(), ".png"),
    content = function(file) {
      df <- life_results()$table
      grDevices::png(file, width = 1600, height = 900, res = 150)
      print(plot_ex_fun(df))
      grDevices::dev.off()
    }
  )
}

shiny::shinyApp(ui, server)
