library(tidyverse)
library(lubridate)
library(hms)
library(glue)
library(jsonlite)
library(sp)
library(sf)
library(fs)
library(readxl)
library(janitor)
library(zoo)

options(pillar.sigfig = 8)
options(digits = 22)

## From global.r:

# GLOBAL REFERENCE DATA
ref_species_codes <- read_csv(
  "../references/SeaLog-Species_CodeList.csv",
  col_types = "ccc",
  col_names = c(
    "common_name",
    "code",
    "scientific_name"
  ),
  skip = 1
) %>%
  select(code, common_name, scientific_name)

nppsd_reference_data <- read_csv(
  "../references/nppsd_reference_table_data.csv",
  col_types = "cic"
)

MIN_COUNT <- 1
MAX_COUNT <- 250

ref_behavior <- nppsd_reference_data %>%
  filter(reference_table == "behavior") %>%
  transmute(
    behavior = code,
    in_ref_table = TRUE
  )

ref_weather <- nppsd_reference_data %>%
  filter(reference_table == "weather") %>%
  transmute(
    weather = code,
    in_ref_table = TRUE
  )

unique_str_agg <- function(x) {
  paste(unique(x[!is.na(x)]), collapse = ",")
}

Modes <- function(x) {
  ux <- unique(x)
  tab <- tabulate(match(x, ux))
  ux[tab == max(tab)]
}

ui_sort_options <- function(all_column_names, sort_column_names) {
  sort_columns <- list()
  for (i in 1:length(sort_column_names)) {
    name <- sort_column_names[i]
    for (j in 1:length(all_column_names)) {
      if (name == all_column_names[j]) {
        sort_columns[[length(sort_columns) + 1]] <- list(j - 1, "asc")
      }
    }
  }

  sort_columns
}

ui_column_vector <- function(all_column_names, target_column_names) {
  target_columns <- c()
  for (i in 1:length(all_column_names)) {
    name <- all_column_names[i]
    if (any(name %in% target_column_names)) {
      target_columns <- c(target_columns, i - 1)
    }
  }

  target_columns
}

console_log <- function(r_file, message) {
  cat(
    file = stderr(),
    glue("{r_file}: {message}"),
    "\n"
  )
}

## From server/validate.r
validate_obs <- function(obs_data, valid_species_list) {
  # VALIDATE Spp
  valid_species <- tibble(
    Species = unique(valid_species_list),
    in_ref_table = TRUE
  )
  obs_data <- obs_data %>%
    left_join(valid_species, by = "Species") %>%
    mutate(spp_valid = !is.na(in_ref_table)) %>%
    select(-in_ref_table)

  # VALIDATE Count
  obs_data <- obs_data %>%
    mutate(
      count_valid = (as.numeric(Count) >= MIN_COUNT &
        as.numeric(Count) <= MAX_COUNT &
        !is.na(Species))
    )

  # VALIDATE Behavior
  obs_data <- obs_data %>%
    mutate(primary_behavior_lower = str_to_lower(PrimaryBehavior)) %>%
    left_join(
      ref_behavior %>%
        mutate(behavior = str_to_lower(behavior)),
      by = c("primary_behavior_lower" = "behavior")
    ) %>%
    mutate(
      primary_behavior_valid = (!is.na(in_ref_table) |
        is.na(PrimaryBehavior) |
        PrimaryBehavior == "")
    ) %>%
    select(-c(in_ref_table, primary_behavior_lower))

  obs_data <- obs_data %>%
    mutate(secondary_behavior_lower = str_to_lower(SecondaryBehavior)) %>%
    left_join(
      ref_behavior %>%
        mutate(behavior = str_to_lower(behavior)),
      by = c("secondary_behavior_lower" = "behavior")
    ) %>%
    mutate(
      secondary_behavior_valid = (!is.na(in_ref_table) |
        is.na(SecondaryBehavior) |
        SecondaryBehavior == "")
    ) %>%
    select(-c(in_ref_table, secondary_behavior_lower))

  # VALIDATE Weather
  obs_data <- obs_data %>%
    left_join(ref_weather, by = c("Weather" = "weather")) %>%
    mutate(
      weather_valid = (!is.na(in_ref_table) |
        is.na(Weather) |
        Weather == "")
    ) %>%
    select(-in_ref_table)

  # ANY ERROR
  obs_data <- obs_data %>%
    rowwise() %>%
    mutate(
      invalid_columns = paste(
        c(
          if_else(!spp_valid, "Species", ""),
          if_else(!count_valid, "Count", ""),
          if_else(!primary_behavior_valid, "PrimaryBehavior", ""),
          if_else(!secondary_behavior_valid, "SecondaryBehavior", ""),
          if_else(!weather_valid, "Weather", "")
        ),
        collapse = " "
      ),
      invalid_columns = gsub(" +", " ", invalid_columns),
      invalid_columns = gsub(" $", "", invalid_columns),
      invalid_columns = gsub("^ ", "", invalid_columns),
      invalid_columns = case_when(
        invalid_columns == "" ~ NA_character_,
        invalid_columns == "NA" ~ NA_character_,
        TRUE ~ invalid_columns
      )
    ) %>%
    ungroup() %>%
    select(
      -c(
        spp_valid,
        count_valid,
        primary_behavior_valid,
        secondary_behavior_valid,
        weather_valid
      )
    )

  obs_data$invalid_columns
}

validate_transects <- function(transects_data) {
  # VALIDATE TransWidth
  transects_data <- transects_data %>%
    mutate(trans_width_valid = !is.na(TxWidth))

  # ANY ERROR
  transects_data <- transects_data %>%
    rowwise() %>%
    mutate(
      invalid_columns = paste(
        c(
          if_else(!trans_width_valid, "TxWidth", "")
        ),
        collapse = " "
      ),
      invalid_columns = gsub(" +", " ", invalid_columns),
      invalid_columns = gsub(" $", "", invalid_columns),
      invalid_columns = gsub("^ ", "", invalid_columns),
      invalid_columns = case_when(
        invalid_columns == "" ~ NA_character_,
        invalid_columns == "NA" ~ NA_character_,
        TRUE ~ invalid_columns
      )
    ) %>%
    ungroup() %>%
    select(-c(trans_width_valid))

  transects_data$invalid_columns
}

sfc_as_cols <- function(x, names = c("x", "y")) {
  stopifnot(inherits(x, "sf") && inherits(sf::st_geometry(x), "sfc_POINT"))
  ret <- sf::st_coordinates(x)
  ret <- tibble::as_tibble(ret)
  stopifnot(length(names) == ncol(ret))
  x <- x[, !names(x) %in% names]
  ret <- stats::setNames(ret, names)
  dplyr::bind_cols(x, ret)
}

## From server/process_raw_data.r

# --- Repair malformed SeaLog CSV quoting -------------------------------------
# The logging software sometimes writes an inch-mark (") in the free-text
# Comments column without escaping it: either left undoubled mid-value
# ("... 9" VIZ ...") or doubled at the end of a value but with the field's
# closing quote dropped ("... 8""<newline>). Either way the line ends up with an
# odd number of quotes and readr merges the following lines into a runaway
# quoted field, silently dropping rows.
#
# Comments is always the last column and the only free-text field; every column
# before it is a code, number, date, or time and never contains a comma or
# quote. So the raw Comments text is everything after the (ncol - 1)th comma and
# can be re-emitted with valid RFC 4180 quoting. ncol comes from the header row
# rather than a hardcoded count.
fix_comment_field <- function(raw) {
  if (is.na(raw) || raw == "") {
    return(raw)
  }
  txt <- raw
  if (str_starts(txt, '"')) {
    txt <- str_remove(txt, '^"') # opening quote
    txt <- str_remove(txt, '"$') # closing quote, if the writer emitted one
    txt <- str_replace_all(txt, '""', '"') # collapse escaped quotes
  }
  if (str_detect(txt, '[",\n]')) {
    txt <- str_c('"', str_replace_all(txt, '"', '""'), '"')
  }
  txt
}

# Rewrite only the malformed lines to a temporary copy and return its path plus
# the number of rows repaired. A well-formed field has an even number of quotes
# (two delimiters plus two per escaped inner quote); the logging bug leaves an
# odd count, so untouched rows stay byte-for-byte identical.
repair_sealog_csv <- function(path) {
  lines <- read_lines(path)
  header <- lines[1]
  body <- lines[-1]
  n_col <- str_count(header, ",") + 1L
  pat <- str_c("^((?:[^,]*,){", n_col - 1L, "})(.*)$")
  m <- str_match(body, pat)

  comment <- m[, 3]
  broken <- !is.na(comment) & (str_count(comment, '"') %% 2L == 1L)
  body[broken] <- str_c(
    m[broken, 2],
    vapply(comment[broken], fix_comment_field, character(1))
  )

  out_path <- tempfile(fileext = ".csv")
  write_lines(c(header, body), out_path)
  list(path = out_path, n_fixed = sum(broken))
}

read_sealog_file <- function(f) {
  col_names <- c(
    "Vessel",
    "TxWidth",
    "TripID",
    "Transect",
    "Record",
    "PortObs",
    "StarboardObs",
    "PortCondition",
    "StarboardCondition",
    "Beaufort",
    "Weather",
    "CloudCover",
    "FogConc",
    "IceType",
    "IceConc",
    "On/OffTx",
    "Type",
    "ObsSideIO",
    "ObsSidePS",
    "Species",
    "Count",
    "PrimaryBehavior",
    "SecondaryBehavior",
    "Age",
    "Sex",
    "Plumage",
    "Distance",
    "Angle",
    "Bin",
    "UserSelect1",
    "UserSelect2",
    "UserText",
    "UserNumeric",
    "Latitude",
    "Longitude",
    "Date",
    "Time",
    "GPSTime",
    "Comments"
  )

  if (path_ext(f) == "csv") {
    repaired <- repair_sealog_csv(f)
    if (repaired$n_fixed > 0) {
      console_log(
        "read_sealog_data_no_shiny.R",
        glue(
          "repaired {repaired$n_fixed} Comments field(s) with unescaped ",
          "quotes in {basename(f)}"
        )
      )
    }
    d <- read_csv(
      repaired$path,
      col_names = col_names,
      col_types = "ccccccccccccccccccccccccccccccccccccccc",
      skip = 1,
      na = c("null", "")
    )
  } else if (path_ext(f) == "xlsx") {
    d <- read_excel(
      f,
      col_names = col_names,
      col_types = "text",
      skip = 1,
      na = c("null", "")
    ) |>
      mutate(
        ts = convert_to_datetime(as.numeric(Date) + as.numeric(Time)),
        Date = as.character(convert_to_date(as.numeric(Date))),
        Time = as.character(hms::hms(
          hours = hour(ts),
          minutes = minute(ts),
          seconds = second(ts)
        ))
      )
  }
  d |>
    mutate(
      across(
        c(
          TxWidth,
          Record,
          PortCondition,
          StarboardCondition,
          Beaufort,
          CloudCover,
          FogConc,
          IceConc,
          Count,
          Distance,
          Angle
        ),
        as.integer
      ),
      across(c(UserNumeric, Latitude, Longitude), as.numeric),
      GPSTime = ymd_hms(GPSTime, tz = "UTC"),
      ts = parse_date_time(
        glue("{Date} {Time}"),
        c("ymd HMS", "mdy HMS"),
        tz = x$timezone
      ),
      Date = as.Date(ts, tz = x$timezone),
      Time = hms::hms(
        hours = hour(ts),
        minutes = minute(ts),
        seconds = second(ts)
      ),
      Species = str_trim(Species),
      PrimaryBehavior = str_trim(PrimaryBehavior),
      SecondaryBehavior = str_trim(SecondaryBehavior),
      `On/OffTx` = str_trim(`On/OffTx`),
      timestamp = force_tz(Date + seconds(Time), tz = x$timezone),
      TripID = coalesce(TripID, "NULL"),
      Transect = coalesce(Transect, "NULL"),
      TxWidth = coalesce(TxWidth, as.integer(9999))
    ) |>
    select(-ts) |>
    rename(ONOFFTX = `On/OffTx`) |>
    filter(
      !is.na(Latitude),
      Latitude > 0,
      Latitude < 90,
      !is.na(Longitude),
      Longitude >= -180,
      Longitude <= 180
    )
}

read_files <- function(
  input_files,
  species_json_file,
  optional_fields_json_file,
  nppsd_species_crosswalk_file,
  x
) {
  x$timezone <- Sys.timezone()
  timezone <- x$timezone

  r <- map(
    input_files,
    read_sealog_file
  ) |>
    list_rbind()

  median_longitude <- median((360 + r$Longitude) %% 360, na.rm = TRUE)
  median_longitude <- if_else(
    median_longitude > 180,
    median_longitude - 360,
    median_longitude
  )
  srid_prefix <- 32600 # northern hemisphere; 32700 is southern
  zone <- floor((median_longitude + 180) / 6.0) + 1
  x$crs <- as.character(glue("EPSG:{srid_prefix + zone}"))

  # fill and correct position of records missing Date/Time
  s <- r %>%
    st_as_sf(coords = c("Longitude", "Latitude"), crs = 4326) %>%
    st_transform(x$crs) %>%
    sfc_as_cols() %>%
    st_drop_geometry() %>%
    mutate(
      x = if_else(is.na(Date), as.numeric(NA), x),
      y = if_else(is.na(Date), as.numeric(NA), y)
    ) %>%
    mutate(
      Date = if_else(is.na(Date), date(with_tz(GPSTime, timezone)), Date),
      Time = if_else(
        is.na(Time),
        hms(
          hours = hour(with_tz(GPSTime, timezone)),
          minutes = minute(with_tz(GPSTime, timezone)),
          seconds = second(with_tz(GPSTime, timezone))
        ),
        Time
      ),
      timestamp = if_else(
        is.na(timestamp),
        force_tz(Date + seconds(Time), tz = timezone),
        timestamp
      ),
      x = na.approx(x),
      y = na.approx(y)
    ) %>%
    st_as_sf(coords = c("x", "y"), crs = x$crs) %>%
    st_transform(4326) %>%
    sfc_as_cols(names = c("Longitude", "Latitude")) %>%
    st_drop_geometry() %>%
    select(names(r))

  x$unfiltered_obs_data <- s
  species_raw <- read_lines(species_json_file)
  species_list <- fromJSON(species_raw)
  x$valid_species_list <- species_list

  nppsd_invalid_codes <- anti_join(
    tibble(code = species_list),
    ref_species_codes,
    by = "code"
  ) %>%
    pull(code)
  x$nppsd_invalid_code_list <- nppsd_invalid_codes

  column_mapping <- tibble(
    json_field = c(
      "vessel",
      "tx_width",
      "trip_id",
      "transect",
      "record",
      "port_observer",
      "starboard_observer",
      "port_condition",
      "starboard_condition",
      "beaufort",
      "weather",
      "cloud_cover",
      "fog_conc",
      "ice_type",
      "ice_conc",
      "on_off_transect",
      "type",
      "obs_side_io",
      "obs_side_ps",
      "species",
      "count",
      "behavior_primary",
      "behavior_secondary",
      "age",
      "sex",
      "plumage",
      "distance",
      "angle",
      "bin",
      "user_select_1",
      "user_select_2",
      "user_text",
      "user_numeric",
      "lat",
      "lon",
      "date",
      "time",
      "gps_time",
      "comments"
    ),
    field = c(
      "Vessel",
      "TxWidth",
      "TripID",
      "Transect",
      "Record",
      "PortObs",
      "StarboardObs",
      "PortCondition",
      "StarboardCondition",
      "Beaufort",
      "Weather",
      "CloudCover",
      "FogConc",
      "IceType",
      "IceConc",
      "ONOFFTX",
      "Type",
      "ObsSideIO",
      "ObsSidePS",
      "Species",
      "Count",
      "PrimaryBehavior",
      "SecondaryBehavior",
      "Age",
      "Sex",
      "Plumage",
      "Distance",
      "Angle",
      "Bin",
      "UserSelect1",
      "UserSelect2",
      "UserText",
      "UserNumeric",
      "Latitude",
      "Longitude",
      "Date",
      "Time",
      "GPSTime",
      "Comments"
    )
  )
  optional_fields_raw <- read_lines(optional_fields_json_file)
  optional_fields <- fromJSON(optional_fields_raw) %>%
    as_tibble() %>%
    pivot_longer(
      cols = everything(),
      names_to = "json_field",
      values_to = "enabled"
    ) %>%
    inner_join(column_mapping, by = "json_field") %>%
    select(-json_field) %>%
    mutate(
      enabled = if_else(
        field == "Latitude" | field == "Longitude",
        TRUE,
        enabled
      )
    )
  x$optional_fields <- optional_fields

  if (!is.null(input$nppsd_species_crosswalk_file)) {
    console_log(
      "process_raw_data.r",
      glue(
        "nppsd_species_crosswalk <- read_csv({nppsd_species_crosswalk_file}, ...)"
      )
    )
    nppsd_species_crosswalk <- read_csv(
      nppsd_species_crosswalk_file,
      col_names = c(
        "custom_species_code",
        "custom_common_name",
        "nppsd_species_code"
      ),
      col_types = "ccc",
      skip = 1,
      na = "null"
    ) %>%
      full_join(
        tibble(custom_species_code = nppsd_invalid_codes),
        by = "custom_species_code"
      )
    x$nppsd_species_crosswalk <- nppsd_species_crosswalk
  } else {
    console_log(
      "process_raw_data.r",
      glue("nppsd_species_crosswalk_file empty")
    )
    x$nppsd_species_crosswalk <- tibble(
      custom_species_code = nppsd_invalid_codes,
      custom_common_name = NA_character_,
      nppsd_species_code = NA_character_
    )
  }

  x
}

get_unique_values <- function(x) {
  unique_text <- x$unfiltered_obs_data %>%
    filter(
      ONOFFTX != "OFF"
    ) %>%
    select(where(is.character)) %>%
    pivot_longer(everything()) %>%
    group_by(name, value) %>%
    summarize(count = n(), .groups = "drop") %>%
    filter(
      name %in%
        c(
          "Species",
          x$optional_fields %>%
            filter(enabled) %>%
            pull(field)
        )
    ) %>%
    filter(
      !is.na(value),
      name != "Comments"
    ) %>%
    arrange(name, desc(count))

  x$unique_text <- unique_text

  unique_integer <- x$unfiltered_obs_data %>%
    filter(
      ONOFFTX != "OFF"
    ) %>%
    select(where(is.integer)) %>%
    pivot_longer(everything()) %>%
    group_by(name, value) %>%
    summarize(count = n(), .groups = "drop") %>%
    filter(
      name %in%
        c(
          x$optional_fields %>%
            filter(enabled) %>%
            pull(field)
        )
    ) %>%
    filter(
      !is.na(value),
      name != "Record"
    ) %>%
    arrange(name, desc(count))

  x$unique_integer <- unique_integer

  x
}

generate_tracks <- function(x) {
  track_points <- x$unfiltered_obs_data %>%
    filter(ONOFFTX != "OFF") %>%
    # select(Latitude, Longitude, timestamp, Date, Type, ONOFFTX, TripID,
    #        Transect, Record, Species, Count, PrimaryBehavior,
    #        Comments, Beaufort, NumberObs, LObs, Lcond, RObs, Rcond,
    #        TransType, TransWidth) %>%
    arrange(TripID, Transect, Date, timestamp) %>%
    mutate(
      seconds = as.numeric(as.duration(timestamp - lag(timestamp))),
      seconds = if_else(is.na(seconds), 0, seconds),
      rleid = as.integer(
        seconds > as.numeric(input$max_point_interval_seconds)
      ),
      rleid = cumsum(rleid)
    ) %>%
    mutate(
      next_lat = lead(Latitude),
      next_lon = lead(Longitude),
      prev_lat = lag(Latitude),
      prev_lon = lag(Longitude),
      jump = (abs(next_lat - Latitude) > 0.2 & abs(prev_lat - Latitude) > 0.2) |
        (abs(next_lon - Longitude) > 0.2 & abs(prev_lon - Longitude) > 0.2) &
          rleid == lead(rleid)
    ) %>%
    filter(!jump | !is.na(Species)) %>%
    mutate(
      Latitude = if_else(jump & !is.na(next_lat), next_lat, Latitude),
      Longitude = if_else(jump & !is.na(next_lon), next_lon, Longitude)
    ) %>%
    select(-c(next_lat, next_lon, prev_lat, prev_lon, jump))

  raw_track_points <- track_points %>%
    filter(is.na(Species))

  track_points <- track_points %>%
    st_as_sf(
      coords = c("Longitude", "Latitude"),
      crs = 4326
    )

  x$raw_track_points <- raw_track_points
  x$track_points <- track_points

  observation_points <- track_points %>%
    filter(!is.na(Species))

  x$observation_points <- observation_points

  track_lines <- track_points %>%
    filter(!is.na(Transect)) %>%
    arrange(timestamp) %>%
    group_by(TripID, Transect, Date, rleid) %>%
    summarize(
      do_union = FALSE,
      start_time = min(timestamp),
      end_time = max(timestamp),
      points = n(),
      .groups = "drop"
    ) %>%
    filter(points > 1) %>%
    st_cast("LINESTRING") %>%
    st_transform(x$crs) %>%
    st_segmentize(dfMaxLength = 50) %>%
    group_by(TripID, Transect, Date) %>%
    # select(-do_union) %>%
    summarize(
      do_union = FALSE,
      start_time = min(start_time),
      end_time = max(end_time),
      .groups = "drop"
    ) %>%
    st_cast("MULTILINESTRING") %>%
    mutate(length_km = map_dbl(geometry, ~ st_length(.x)) / 1000) %>%
    st_transform(4326)

  x$track_lines <- track_lines

  segmentized_points <- track_lines %>%
    st_cast("POINT", warn = FALSE) %>%
    st_join(track_points, join = st_nearest_feature) %>%
    rename(
      TripID = TripID.y,
      Transect = Transect.y,
      Date = Date.y
    ) %>%
    select(
      -c(
        start_time,
        end_time,
        length_km,
        TripID.x,
        Transect.x,
        Date.x
      )
    )

  x$segmentized_points <- segmentized_points

  track_centroids <- track_points %>%
    st_transform(x$crs) %>%
    group_by(TripID, Transect, Date) %>%
    summarize(.groups = "drop") %>%
    mutate(
      x = map_dbl(geometry, ~ st_centroid(.x)[[1]]),
      y = map_dbl(geometry, ~ st_centroid(.x)[[2]])
    ) %>%
    st_drop_geometry() %>%
    st_as_sf(
      coords = c("x", "y"),
      crs = x$crs
    ) %>%
    st_transform(4326) %>%
    mutate(
      Latitude = map_dbl(geometry, ~ st_centroid(.x)[[2]]),
      Longitude = map_dbl(geometry, ~ st_centroid(.x)[[1]])
    )

  x$track_centroids <- track_centroids

  valid_transects <- x$unfiltered_obs_data %>%
    filter(ONOFFTX != "OFF") %>%
    group_by(TripID, Transect, Date) %>%
    summarize(.groups = "drop")

  x$valid_transects <- valid_transects

  x
}

generate_nppsd_splits <- function(x) {
  if (input$use_nps_length_for_nppsd) {
    nppsd_split_m <- input$nps_split_km * 1000
  } else {
    nppsd_split_m <- 3300
  }

  x$nppsd_split_km <- nppsd_split_m / 1000

  suppressWarnings(
    nppsd_points <- x$segmentized_points %>%
      inner_join(x$valid_transects, by = c("Date", "TripID", "Transect")) %>%
      st_transform(x$crs) %>%
      filter(!is.na(TripID), !is.na(Transect), !is.na(Date), !is.na(rleid)) %>%
      mutate(
        x = map_dbl(geometry, ~ st_centroid(.x)[[1]]),
        y = map_dbl(geometry, ~ st_centroid(.x)[[2]])
      ) %>%
      group_by(TripID, Transect, Date) %>%
      arrange(TripID, Transect, timestamp) %>%
      mutate(
        next_m = if_else(
          rleid == lead(rleid),
          sqrt(((lead(x) - x)**2) + (lead(y) - y)**2),
          0
        ),
        cumsum_m = cumsum(next_m),
        nppsd_split_id = floor(cumsum_m / nppsd_split_m),
        max_split_id = max(nppsd_split_id, na.rm = TRUE),
        nppsd_split_id = coalesce(nppsd_split_id, max_split_id),
        nppsd_split_id = if_else(
          nppsd_split_id == max_split_id & max_split_id > 0,
          nppsd_split_id - 1,
          nppsd_split_id
        )
      )
  )

  next_points <- nppsd_points %>%
    group_by(TripID, Transect, Date, rleid) %>%
    mutate(prev_point_id = lag(nppsd_split_id)) %>%
    filter(nppsd_split_id != prev_point_id) %>%
    mutate(nppsd_split_id = prev_point_id) %>%
    select(-prev_point_id)

  nppsd_split <- nppsd_points %>%
    bind_rows(next_points) %>%
    group_by(TripID, Transect, Date, rleid, nppsd_split_id) %>%
    summarize(
      do_union = FALSE,
      start_time = min(timestamp),
      end_time = max(timestamp),
      .groups = "drop"
    ) %>%
    st_cast("LINESTRING")

  nppsd_split$vertices <- nppsd_split %>%
    st_coordinates() %>%
    as_tibble() %>%
    group_by(L1) %>%
    summarize(
      n = n(),
      .groups = "drop"
    ) %>%
    pull(n)

  nppsd_split <- nppsd_split %>%
    filter(vertices > 1) %>%
    group_by(TripID, Transect, Date, nppsd_split_id) %>%
    # select(-do_union) %>%
    summarize(
      do_union = FALSE,
      start_time = min(start_time),
      end_time = max(end_time),
      .groups = "drop"
    ) %>%
    st_cast("MULTILINESTRING") %>%
    st_transform(x$crs) %>%
    mutate(length_km = map_dbl(geometry, ~ st_length(.x)) / 1000) %>%
    st_transform(4326)

  x$nppsd_split <- nppsd_split

  nppsd_track_centroids <- nppsd_points %>%
    group_by(TripID, Transect, Date, nppsd_split_id) %>%
    summarize(.groups = "drop") %>%
    mutate(
      x = map_dbl(geometry, ~ st_centroid(.x)[[1]]),
      y = map_dbl(geometry, ~ st_centroid(.x)[[2]])
    ) %>%
    st_drop_geometry() %>%
    st_as_sf(coords = c("x", "y"), crs = x$crs) %>%
    st_transform(4326) %>%
    mutate(
      Latitude = map_dbl(geometry, ~ st_centroid(.x)[[2]]),
      Longitude = map_dbl(geometry, ~ st_centroid(.x)[[1]])
    )

  x$nppsd_track_centroids <- nppsd_track_centroids

  x
}

generate_nps_splits <- function(x) {
  nps_split_m <- input$nps_split_km * 1000
  if (input$nps_split_km == 0) {
    nps_split_m <- 1000000
  }

  x$nps_split_km <- nps_split_m / 1000

  suppressWarnings(
    nps_points <- x$segmentized_points %>%
      inner_join(x$valid_transects, by = c("Date", "TripID", "Transect")) %>%
      st_transform(x$crs) %>%
      filter(!is.na(TripID), !is.na(Transect), !is.na(Date), !is.na(rleid)) %>%
      mutate(
        x = map_dbl(geometry, ~ st_centroid(.x)[[1]]),
        y = map_dbl(geometry, ~ st_centroid(.x)[[2]])
      ) %>%
      group_by(TripID, Transect, Date) %>%
      arrange(TripID, Transect, timestamp) %>%
      mutate(
        next_m = if_else(
          rleid == lead(rleid),
          sqrt(((lead(x) - x)**2) + (lead(y) - y)**2),
          0
        ),
        cumsum_m = cumsum(next_m),
        split_id = floor(cumsum_m / nps_split_m),
        max_split_id = max(split_id, na.rm = TRUE),
        split_id = coalesce(split_id, max_split_id),
        split_id = if_else(
          split_id == max_split_id & max_split_id > 0,
          split_id - 1,
          split_id
        )
      )
  )

  next_points <- nps_points %>%
    group_by(TripID, Transect, Date, rleid) %>%
    mutate(prev_point_id = lag(split_id)) %>%
    filter(split_id != prev_point_id) %>%
    mutate(split_id = prev_point_id) %>%
    select(-prev_point_id)

  nps_split <- nps_points %>%
    bind_rows(next_points) %>%
    group_by(TripID, Transect, Date, rleid, split_id) %>%
    summarize(
      do_union = FALSE,
      start_time = min(timestamp),
      end_time = max(timestamp),
      .groups = "drop"
    ) %>%
    st_cast("LINESTRING")

  nps_split$vertices <- nps_split %>%
    st_coordinates() %>%
    as_tibble() %>%
    group_by(L1) %>%
    summarize(
      n = n(),
      .groups = "drop"
    ) %>%
    pull(n)

  nps_split <- nps_split %>%
    filter(vertices > 1) %>%
    group_by(TripID, Transect, Date, split_id) %>%
    # select(-do_union) %>%
    summarize(
      do_union = FALSE,
      start_time = min(start_time),
      end_time = max(end_time),
      .groups = "drop"
    ) %>%
    st_cast("MULTILINESTRING") %>%
    st_transform(x$crs) %>%
    mutate(length_km = map_dbl(geometry, ~ st_length(.x)) / 1000) %>%
    st_transform(4326)

  x$nps_split <- nps_split

  nps_track_centroids <- nps_points %>%
    group_by(TripID, Transect, Date, split_id) %>%
    summarize(.groups = "drop") %>%
    mutate(
      x = map_dbl(geometry, ~ st_centroid(.x)[[1]]),
      y = map_dbl(geometry, ~ st_centroid(.x)[[2]])
    ) %>%
    st_drop_geometry() %>%
    st_as_sf(coords = c("x", "y"), crs = x$crs) %>%
    st_transform(4326) %>%
    mutate(
      Latitude = map_dbl(geometry, ~ st_centroid(.x)[[2]]),
      Longitude = map_dbl(geometry, ~ st_centroid(.x)[[1]])
    )

  x$nps_track_centroids <- nps_track_centroids

  x
}

generate_transects <- function(x) {
  suppressWarnings(
    transects <- x$unfiltered_obs_data %>%
      filter(
        ONOFFTX != "OFF"
      ) %>%
      group_by(TripID, Transect) %>%
      summarize(
        port_obs = max(PortObs, na.rm = TRUE),
        all_port_obss = unique_str_agg(PortObs),
        starboard_obs = max(StarboardObs, na.rm = TRUE),
        all_starboard_obss = unique_str_agg(StarboardObs),
        TxWidth = max(TxWidth, na.rm = TRUE),
        all_trans_width = unique_str_agg(TxWidth),
        Vessel = max(Vessel, na.rm = TRUE),
        all_vessel = unique_str_agg(Vessel),
        Weather = max(Weather, na.rm = TRUE),
        all_weather = unique_str_agg(Weather),
        CloudCover = max(CloudCover, na.rm = TRUE),
        all_cloud_cover = unique_str_agg(CloudCover),
        FogConc = max(FogConc, na.rm = TRUE),
        all_fog_conc = unique_str_agg(FogConc),
        IceType = max(IceType, na.rm = TRUE),
        all_ice_type = unique_str_agg(IceType),
        IceConc = max(IceConc, na.rm = TRUE),
        all_ice_conc = unique_str_agg(IceConc),
        .groups = "drop"
      ) %>%
      ungroup() %>%
      mutate(
        po_all = if_else(port_obs != all_port_obss, all_port_obss, ""),
        so_all = if_else(
          starboard_obs != all_starboard_obss,
          all_starboard_obss,
          ""
        ),
        tw_all = if_else(TxWidth != all_trans_width, all_trans_width, ""),
        vessel_all = if_else(Vessel != all_vessel, all_vessel, ""),
        wx_all = if_else(Weather != all_weather, all_weather, ""),
        cloud_all = if_else(CloudCover != all_cloud_cover, all_cloud_cover, ""),
        fog_all = if_else(FogConc != all_fog_conc, all_fog_conc, ""),
        icet_all = if_else(IceType != all_ice_type, all_ice_type, ""),
        icec_all = if_else(IceConc != all_ice_conc, all_ice_conc, ""),
      ) %>%
      inner_join(x$track_centroids, by = c("TripID", "Transect")) %>%
      select(-geometry) %>% # remove points
      inner_join(x$track_lines, by = c("TripID", "Transect", "Date")) %>%
      mutate(
        area_sampled_km2 = as.numeric(gsub(
          " *([0-9]+).*",
          "\\1",
          TxWidth
        )) /
          1000 *
          length_km,
        invalid_columns = NA
      ) %>%
      select(-c(starts_with("all_"))) %>%
      select(
        TripID,
        Transect,
        start_time,
        end_time,
        TxWidth,
        tw_all,
        length_km,
        area_sampled_km2,
        port_obs,
        po_all,
        starboard_obs,
        so_all,
        invalid_columns,
        Vessel,
        vessel_all,
        Weather,
        wx_all,
        CloudCover,
        cloud_all,
        FogConc,
        fog_all,
        IceType,
        icet_all,
        IceConc,
        icec_all
      )
  )

  if (
    x$optional_fields %>%
      filter(field == "Vessel") %>%
      pull(enabled) ==
      FALSE
  ) {
    transects <- transects %>%
      select(-c(Vessel, vessel_all))
  }
  if (
    x$optional_fields %>%
      filter(field == "Weather") %>%
      pull(enabled) ==
      FALSE
  ) {
    transects <- transects %>%
      select(-c(Weather, wx_all))
  }
  if (
    x$optional_fields %>%
      filter(field == "CloudCover") %>%
      pull(enabled) ==
      FALSE
  ) {
    transects <- transects %>%
      select(-c(CloudCover, cloud_all))
  }
  if (
    x$optional_fields %>%
      filter(field == "FogConc") %>%
      pull(enabled) ==
      FALSE
  ) {
    transects <- transects %>%
      select(-c(FogConc, fog_all))
  }
  if (
    x$optional_fields %>%
      filter(field == "IceType") %>%
      pull(enabled) ==
      FALSE
  ) {
    transects <- transects %>%
      select(-c(IceType, icet_all))
  }
  if (
    x$optional_fields %>%
      filter(field == "IceConc") %>%
      pull(enabled) ==
      FALSE
  ) {
    transects <- transects %>%
      select(-c(IceConc, icec_all))
  }

  transects$invalid_columns <- validate_transects(transects)
  transects$edited <- date(NA)
  transects$delete <- NA

  double_column_names <- transects %>%
    select(where(
      ~ is_double(.) &
        !inherits(., "Date") &
        !inherits(., "POSIXt") &
        !inherits(., "hms")
    )) %>%
    names()
  columns_to_round <- ui_column_vector(names(transects), double_column_names)

  disable_column_names <- c(
    "TripID",
    "Transect",
    "length_km",
    "area_sampled_km2",
    "invalid_columns",
    "nppsd_split_id",
    "split_id",
    "edited"
  )
  columns_to_disable <- ui_column_vector(names(transects), disable_column_names)

  sort_column_names <- c(
    "invalid_columns",
    "TripID",
    "Transect"
  )
  sort_columns <- ui_sort_options(names(transects), sort_column_names)

  x$transects <- transects
  x$transects_columns_to_round <- columns_to_round
  x$transects_columns_to_disable <- columns_to_disable
  x$transects_sort_columns <- sort_columns
  x$original_transects <- transects %>%
    filter(FALSE)

  x
}

generate_observations <- function(x) {
  all_points_data <- x$unfiltered_obs_data %>%
    select(c(
      timestamp,
      TripID,
      Transect,
      ONOFFTX,
      Type,
      Latitude,
      Longitude,
      Species,
      Count,
      PrimaryBehavior,
      SecondaryBehavior,
      Weather,
      Beaufort,
      PortCondition,
      UserSelect1,
      UserSelect2,
      UserText,
      UserNumeric,
      StarboardCondition,
      Comments,
      x$optional_fields %>%
        filter(enabled) %>%
        pull(field),
      GPSTime
    ))

  # Only SCAN PrimaryBehavior for "Snapshot", otherwise OFF TX
  if (input$fly_bird_method == "Snapshot") {
    all_points_data <- all_points_data %>%
      mutate(
        ONOFFTX = if_else(
          !is.na(PrimaryBehavior) & PrimaryBehavior == "FLY",
          "OFF",
          ONOFFTX
        )
      )
  }

  # "bin 9"
  if (x$optional_fields %>% filter(field == "Bin") %>% pull(enabled)) {
    all_points_data <- all_points_data %>%
      mutate(
        ONOFFTX = case_when(
          Bin == "OFF-TX" ~ "OFF",
          TRUE ~ ONOFFTX
        )
      )
  }

  coordinates(all_points_data) <- ~ Longitude + Latitude

  all_points_data <- cbind(all_points_data, coordinates(all_points_data))

  if (sum(names(x$transects) == "Weather") == 0) {
    transects_w_weather <- x$transects %>%
      mutate(Weather = NA_character_)
  } else {
    transects_w_weather <- x$transects
  }

  joined_obs_data <- all_points_data %>%
    st_as_sf() %>%
    st_set_crs(4326) %>%
    st_transform(x$crs) %>%
    st_join(x$nppsd_split %>% st_transform(x$crs) %>% st_buffer(5)) %>%
    filter(
      Transect.x == Transect.y,
      TripID.x == TripID.y
    ) %>%
    rename(
      Transect = Transect.x,
      TripID = TripID.x
    ) %>%
    st_join(x$nps_split %>% st_transform(x$crs) %>% st_buffer(5)) %>%
    filter(
      Transect.x == Transect.y,
      TripID.x == TripID.y
    ) %>%
    rename(
      Transect = Transect.x,
      TripID = TripID.x
    ) %>%
    as_tibble() %>%
    group_by(
      timestamp,
      TripID,
      Transect,
      ONOFFTX,
      Latitude,
      Longitude,
      Species,
      Count,
      PrimaryBehavior,
      SecondaryBehavior,
      UserSelect1,
      UserSelect2,
      UserText,
      UserNumeric,
      Weather,
      Beaufort,
      Type,
      PortCondition,
      StarboardCondition,
      Comments,
      GPSTime
    ) %>%
    summarize(
      nppsd_split_id = max(nppsd_split_id),
      split_id = max(split_id),
      .groups = "drop"
    ) %>%
    # From non-split MASTER: adds transect sample_area_km2, calculates density
    mutate(Date = date(timestamp)) %>%
    inner_join(
      transects_w_weather %>% mutate(Date = date(start_time)),
      by = c("TripID", "Transect", "Date")
    ) %>%
    mutate(
      density = if_else(
        ONOFFTX == "ON",
        as.numeric(Count) / area_sampled_km2,
        as.numeric(NA)
      ),
      Weather = Weather.x
    ) %>%
    select(
      timestamp,
      TripID,
      Transect,
      ONOFFTX,
      Type,
      Latitude,
      Longitude,
      Species,
      Count,
      density,
      PrimaryBehavior,
      SecondaryBehavior,
      UserSelect1,
      UserSelect2,
      UserText,
      UserNumeric,
      Weather,
      Beaufort,
      PortCondition,
      StarboardCondition,
      Comments,
      GPSTime,
      nppsd_split_id,
      split_id
    )

  # rejoin with all_points_data to handle cases where there are multiple obs
  # at exactly the same time and location due to GPS/dLOG failure
  all_points_data <- all_points_data %>%
    as_tibble() %>%
    select(-c("Longitude.1", "Latitude.1")) %>%
    inner_join(
      joined_obs_data,
      by = c(
        "timestamp",
        "TripID",
        "Transect",
        "ONOFFTX",
        "Type",
        "Species",
        "Count",
        "PrimaryBehavior",
        "SecondaryBehavior",
        "UserSelect1",
        "UserSelect2",
        "UserText",
        "UserNumeric",
        "Weather",
        "Beaufort",
        "PortCondition",
        "StarboardCondition",
        "Comments",
        "Longitude",
        "Latitude",
        "GPSTime"
      )
    ) %>%
    mutate(invalid_columns = NA) %>%
    select(c(
      timestamp,
      TripID,
      Transect,
      ONOFFTX,
      Type,
      Latitude,
      Longitude,
      Species,
      Count,
      density,
      PrimaryBehavior,
      SecondaryBehavior,
      Weather,
      Beaufort,
      PortCondition,
      StarboardCondition,
      UserSelect1,
      UserSelect2,
      UserText,
      UserNumeric,
      invalid_columns,
      Comments,
      x$optional_fields %>%
        filter(enabled) %>%
        pull(field),
      GPSTime,
      nppsd_split_id,
      split_id
    ))

  all_obs_data <- all_points_data %>%
    filter(
      str_to_upper(Type) == "USER",
      !is.na(Species),
      str_length(Species) > 0,
      # ONOFFTX != "OFF"
    )

  all_obs_data$invalid_columns <- validate_obs(
    all_obs_data,
    x$valid_species_list
  )
  all_obs_data$edited <- date(NA)
  all_obs_data$delete <- NA

  double_column_names <- all_obs_data %>%
    select(where(
      ~ is_double(.) &
        !inherits(., "Date") &
        !inherits(., "POSIXt") &
        !inherits(., "hms")
    )) %>%
    names()
  columns_to_round <- ui_column_vector(names(all_obs_data), double_column_names)

  disable_column_names <- c(
    "TripID",
    "Transect",
    "density",
    "invalid_columns",
    "nppsd_split_id",
    "split_id",
    "edited"
  )
  columns_to_disable <- ui_column_vector(
    names(all_obs_data),
    disable_column_names
  )

  sort_column_names <- c(
    "invalid_columns",
    "TripID",
    "Transect",
    "timestamp"
  )
  sort_columns <- ui_sort_options(names(all_obs_data), sort_column_names)

  x$all_points_data <- all_points_data
  x$all_obs_data <- all_obs_data
  x$obs_columns_to_round <- columns_to_round
  x$obs_columns_to_disable <- columns_to_disable
  x$obs_sort_columns <- sort_columns
  x$original_obs_data <- all_obs_data %>%
    filter(FALSE)

  x
}

## From server/make_nppsd.r
make_tbl_location <- function(x) {
  # Required fields
  if (!("Weather" %in% colnames(x$all_points_data))) {
    x$all_points_data$Weather <- as.character(NA)
  }
  if (!("Beaufort" %in% colnames(x$all_points_data))) {
    x$all_points_data$Beaufort <- as.integer(NA)
  }
  suppressWarnings(
    odata <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', nppsd_split_id)}"
        ),
        beaufort = Beaufort,
        weather = Weather
      ) %>%
      group_by(master_key) %>%
      summarize(
        beaufort = max(
          Modes(as.numeric(beaufort)),
          na.rm = TRUE
        ),
        weather = max(
          Modes(weather),
          na.rm = TRUE
        ),
        .groups = "drop"
      )
  )

  x$tbl_location <- x$transects %>%
    mutate(Date = date(start_time)) %>%
    inner_join(x$nppsd_split, by = c("TripID", "Transect", "Date")) %>%
    inner_join(
      x$nppsd_track_centroids,
      by = c("TripID", "Transect", "Date", "nppsd_split_id")
    ) %>%
    mutate(
      master_key = glue(
        "{TripID}_",
        "{str_pad(Transect, 4, pad = '0')}_",
        "{year(start_time.y)}",
        "{sprintf('%02d', month(start_time.y))}",
        "{sprintf('%02d', day(start_time.y))}_",
        "{sprintf('%02d', nppsd_split_id)}"
      )
    ) %>%
    # This preserves transects without obs,
    # but beaufort/weather will be NA
    left_join(odata, by = "master_key") %>%
    transmute(
      master_key = master_key,
      modified_platform_type = if_else(
        str_length(input$modified_platform_type) > 0,
        input$modified_platform_type,
        as.character(NA)
      ),
      modified_survey_type = if_else(
        str_length(input$modified_survey_type) > 0,
        input$modified_survey_type,
        as.character(NA)
      ),
      location_local_date_time = format(
        start_time.y,
        "%Y-%m-%d %H:%M:%S"
      ),
      location_utc_date_time = format(
        with_tz(start_time.y, tzone = "UTC"),
        "%Y-%m-%d %H:%M:%S"
      ),
      survey_target = if_else(
        str_length(input$survey_target) > 0,
        input$survey_target,
        as.character(NA)
      ),
      fly_bird_method = if_else(
        str_length(input$fly_bird_method) > 0,
        input$fly_bird_method,
        as.character(NA)
      ),
      loc_method = "Centroid",
      latitude = Latitude,
      longitude = Longitude,
      transect_width = as.numeric(gsub("([0-9.]+).*", "\\1", TxWidth)),
      sea_state = beaufort,
      weather = weather,
      sample_area = transect_width / 1000 * length_km.y,
      transect_length_km = length_km.y,
      source_name = if_else(
        str_length(input$source) > 0,
        input$source,
        as.character(NA)
      ),
      pi_credit = if_else(
        str_length(input$pi_credit) > 0,
        input$pi_credit,
        as.character(NA)
      )
    )

  x
}

make_tbl_data_obs <- function(x) {
  tdata <- x$transects %>%
    mutate(Date = date(start_time)) %>%
    inner_join(x$nppsd_split, by = c("TripID", "Transect", "Date")) %>%
    transmute(
      master_key = glue(
        "{TripID}_",
        "{str_pad(Transect, 4, pad = '0')}_",
        "{year(start_time.y)}",
        "{sprintf('%02d', month(start_time.y))}",
        "{sprintf('%02d', day(start_time.y))}_",
        "{sprintf('%02d', nppsd_split_id)}"
      ),
      transect_width = as.numeric(gsub("([0-9.]+).*", "\\1", TxWidth)),
      length_km = length_km.y,
      sample_area = transect_width / 1000 * length_km
    ) %>%
    select(-transect_width)

  str_agg <- function(v, collapse_) {
    v <- v[!is.na(v)]
    paste0(v, collapse = collapse_)
  }

  tbl_data_obs <- x$all_obs_data %>%
    mutate(
      master_key = glue(
        "{TripID}_",
        "{str_pad(Transect, 4, pad = '0')}_",
        "{year(timestamp)}",
        "{sprintf('%02d', month(timestamp))}",
        "{sprintf('%02d', day(timestamp))}_",
        "{sprintf('%02d', nppsd_split_id)}"
      )
    ) %>%
    inner_join(tdata, by = "master_key") %>%
    left_join(
      x$nppsd_species_crosswalk,
      by = c("Species" = "custom_species_code")
    ) %>%
    mutate(
      Species = coalesce(nppsd_species_code, Species)
    ) %>%
    left_join(ref_species_codes, by = c("Species" = "code")) %>%
    transmute(
      master_key = master_key,
      sample_area = sample_area,
      number = as.numeric(Count),
      nppsd_4_letter_code = Species,
      primary_behavior = str_to_title(PrimaryBehavior),
      secondary_behaviory = str_to_title(SecondaryBehavior),
      species_obs_comments = Comments,
      ot_obs = ONOFFTX
    ) %>%
    group_by(
      master_key,
      sample_area,
      nppsd_4_letter_code,
      primary_behavior,
      secondary_behaviory,
      ot_obs
    ) %>%
    summarize(
      number = sum(number),
      species_obs_comments = unique_str_agg(species_obs_comments),
      .groups = "drop"
    ) %>%
    mutate(
      density = if_else(ot_obs == "ON", number / sample_area, as.numeric(NA))
    ) %>%
    select(
      master_key,
      nppsd_4_letter_code,
      primary_behavior,
      secondary_behaviory,
      number,
      density,
      ot_obs,
      species_obs_comments
    )

  x$tbl_data_obs <- tbl_data_obs

  x
}

# From make_nps.r
make_nps_transects <- function(x) {
  suppressWarnings(
    beaufort_weather <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        ),
        Beaufort = Beaufort,
        Weather = Weather
      ) %>%
      group_by(master_key) %>%
      summarize(
        Beaufort = max(Modes(as.numeric(Beaufort)), na.rm = TRUE),
        Weather = max(Modes(Weather), na.rm = TRUE),
        .groups = "drop"
      )
  )

  if (
    x$optional_fields %>%
      filter(field == "Vessel") %>%
      pull(enabled) ==
      TRUE
  ) {
    vessel <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        ),
        Vessel = Vessel
      ) %>%
      group_by(master_key) %>%
      summarize(
        Vessel = max(Modes(Vessel), na.rm = TRUE),
        .groups = "drop"
      )
  } else {
    vessel <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        )
      ) %>%
      group_by(master_key) %>%
      summarize(
        Vessel = NA,
        .groups = "drop"
      )
  }
  if (
    x$optional_fields %>%
      filter(field == "CloudCover") %>%
      pull(enabled) ==
      TRUE
  ) {
    cloud_cover <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        ),
        CloudCover = CloudCover
      ) %>%
      group_by(master_key) %>%
      summarize(
        CloudCover = max(Modes(as.numeric(CloudCover)), na.rm = TRUE),
        .groups = "drop"
      )
  } else {
    cloud_cover <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        )
      ) %>%
      group_by(master_key) %>%
      summarize(
        CloudCover = NA,
        .groups = "drop"
      )
  }
  if (
    x$optional_fields %>%
      filter(field == "FogConc") %>%
      pull(enabled) ==
      TRUE
  ) {
    fog_conc <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        ),
        FogConc = FogConc
      ) %>%
      group_by(master_key) %>%
      summarize(
        FogConc = max(Modes(as.numeric(FogConc)), na.rm = TRUE),
        .groups = "drop"
      )
  } else {
    fog_conc <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        )
      ) %>%
      group_by(master_key) %>%
      summarize(
        FogConc = NA,
        .groups = "drop"
      )
  }
  if (
    x$optional_fields %>%
      filter(field == "IceType") %>%
      pull(enabled) ==
      TRUE
  ) {
    ice_type <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        ),
        IceType = IceType
      ) %>%
      group_by(master_key) %>%
      summarize(
        IceType = max(Modes(IceType), na.rm = TRUE),
        .groups = "drop"
      )
  } else {
    ice_type <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        )
      ) %>%
      group_by(master_key) %>%
      summarize(
        IceType = NA,
        .groups = "drop"
      )
  }
  if (
    x$optional_fields %>%
      filter(field == "IceConc") %>%
      pull(enabled) ==
      TRUE
  ) {
    ice_conc <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        ),
        IceConc = IceConc
      ) %>%
      group_by(master_key) %>%
      summarize(
        IceConc = max(Modes(as.numeric(IceConc)), na.rm = TRUE),
        .groups = "drop"
      )
  } else {
    ice_conc <- x$all_points_data %>%
      transmute(
        master_key = glue(
          "{TripID}_",
          "{str_pad(Transect, 4, pad = '0')}_",
          "{year(timestamp)}",
          "{sprintf('%02d', month(timestamp))}",
          "{sprintf('%02d', day(timestamp))}_",
          "{sprintf('%02d', split_id)}"
        )
      ) %>%
      group_by(master_key) %>%
      summarize(
        IceConc = NA,
        .groups = "drop"
      )
  }

  odata <- vessel %>%
    inner_join(beaufort_weather, by = "master_key") %>%
    inner_join(cloud_cover, by = "master_key") %>%
    inner_join(fog_conc, by = "master_key") %>%
    inner_join(ice_type, by = "master_key") %>%
    inner_join(ice_conc, by = "master_key")

  transect_tbl <- x$transects %>%
    mutate(Date = date(start_time)) %>%
    inner_join(x$nps_split, by = c("TripID", "Transect", "Date")) %>%
    inner_join(
      x$nps_track_centroids,
      by = c("TripID", "Transect", "Date", "split_id")
    ) %>%
    mutate(
      master_key = glue(
        "{TripID}_",
        "{str_pad(Transect, 4, pad = '0')}_",
        "{year(start_time.y)}",
        "{sprintf('%02d', month(start_time.y))}",
        "{sprintf('%02d', day(start_time.y))}_",
        "{sprintf('%02d', split_id)}"
      )
    ) %>%
    select(
      master_key,
      TripID,
      Transect,
      start_time.y,
      end_time.y,
      TxWidth,
      length_km.y,
      port_obs,
      starboard_obs,
      Latitude,
      Longitude
    ) %>%
    left_join(odata, by = "master_key") %>%
    transmute(
      master_key = master_key,
      TripID = TripID,
      Transect = Transect,
      Latitude = Latitude,
      Longitude = Longitude,
      start_time = start_time.y,
      end_time = end_time.y,
      transect_width_m = as.numeric(gsub(
        "([0-9.]+).*",
        "\\1",
        TxWidth
      )),
      transect_length_km = length_km.y,
      sample_area_km2 = transect_width_m / 1000 * transect_length_km,
      Vessel = Vessel,
      Beaufort = Beaufort,
      Weather = Weather,
      CloudCover = CloudCover,
      FogConc = FogConc,
      IceType = IceType,
      IceConc = IceConc,
      PortObs = port_obs,
      StarboardObs = starboard_obs
    )
  columns_to_remove <- intersect(
    names(transect_tbl),
    x$optional_fields %>%
      filter(enabled == FALSE) %>%
      pull(field)
  )

  nps_transects <- transect_tbl %>%
    select(-any_of(columns_to_remove))

  double_column_names <- nps_transects %>%
    select(where(
      ~ is_double(.) &
        !inherits(., "Date") &
        !inherits(., "POSIXt") &
        !inherits(., "hms")
    )) %>%
    names()
  columns_to_round <- ui_column_vector(
    names(nps_transects),
    double_column_names
  )

  x$nps_transects <- nps_transects
  x$nps_transects_round <- columns_to_round

  x
}

make_nps_obs <- function(x) {
  tdata <- x$transects %>%
    mutate(Date = date(start_time)) %>%
    inner_join(x$nps_split, by = c("TripID", "Transect", "Date")) %>%
    transmute(
      master_key = glue(
        "{TripID}_",
        "{str_pad(Transect, 4, pad = '0')}_",
        "{year(start_time.y)}",
        "{sprintf('%02d', month(start_time.y))}",
        "{sprintf('%02d', day(start_time.y))}_",
        "{sprintf('%02d', split_id)}"
      ),
      transect_width = as.numeric(gsub(
        "([0-9.]+).*",
        "\\1",
        TxWidth
      )),
      length_km = length_km.y,
      sample_area = transect_width / 1000 * length_km
    ) %>%
    select(-transect_width)

  nps_obs <- x$all_obs_data %>%
    mutate(
      master_key = glue(
        "{TripID}_",
        "{str_pad(Transect, 4, pad = '0')}_",
        "{year(timestamp)}",
        "{sprintf('%02d', month(timestamp))}",
        "{sprintf('%02d', day(timestamp))}_",
        "{sprintf('%02d', split_id)}"
      )
    ) %>%
    inner_join(tdata, by = "master_key") %>%
    mutate(
      Count = as.numeric(Count),
      density = if_else(
        ONOFFTX == "ON",
        Count / sample_area,
        as.numeric(NA)
      )
    ) %>%
    select(c(
      master_key,
      timestamp,
      TripID,
      Transect,
      Latitude,
      Longitude,
      Species,
      Count,
      PrimaryBehavior,
      sample_area,
      density,
      Comments,
      x$optional_fields %>%
        filter(enabled == TRUE) %>%
        pull(field)
    ))

  double_column_names <- nps_obs %>%
    select(where(
      ~ is_double(.) &
        !inherits(., "Date") &
        !inherits(., "POSIXt") &
        !inherits(., "hms")
    )) %>%
    names()
  columns_to_round <- ui_column_vector(names(nps_obs), double_column_names)

  x$nps_obs <- nps_obs
  x$nps_obs_round <- columns_to_round

  x
}

# From server/species_summary.r, output$dl_spp_summary
generate_species_summary <- function(x) {
  transect_area <- x$transects %>%
    group_by(TripID, Transect) %>%
    summarize(
      area_sampled_km2 = sum(area_sampled_km2),
      .groups = "drop"
    )

  all_table_data <- x$all_obs_data %>%
    filter(ONOFFTX == "ON") %>%
    mutate(Count = as.numeric(Count)) %>%
    rename(code = Species) %>%
    left_join(ref_species_codes, by = "code") %>%
    left_join(
      x$nppsd_species_crosswalk,
      by = c("code" = "custom_species_code")
    ) %>%
    mutate(
      common_name = coalesce(common_name, custom_common_name)
    ) %>%
    group_by(TripID, Transect, code, common_name) %>%
    summarize(
      total = sum(Count, na.rm = TRUE),
      observations = n(),
      .groups = "drop"
    ) %>%
    inner_join(transect_area) %>%
    mutate(density = total / area_sampled_km2) %>%
    select(-area_sampled_km2) %>%
    arrange(TripID, Transect, desc(total), desc(observations))

  off_transect_data <- x$all_obs_data %>%
    filter(ONOFFTX == "OFF") %>%
    mutate(Count = as.numeric(Count)) %>%
    rename(code = Species) %>%
    left_join(ref_species_codes, by = "code") %>%
    left_join(
      x$nppsd_species_crosswalk,
      by = c("code" = "custom_species_code")
    ) %>%
    mutate(
      common_name = coalesce(common_name, custom_common_name)
    ) %>%
    group_by(code, common_name) %>%
    summarize(
      total = sum(Count, na.rm = TRUE),
      observations = n(),
      density = as.numeric(NA),
      .groups = "drop"
    ) %>%
    mutate(
      TripID = NA_character_,
      Transect = "off transect"
    ) %>%
    arrange(desc(total), desc(observations))

  x$spp_summary <- bind_rows(all_table_data, off_transect_data)

  x
}

### START here

# shiny global
x <- list()

# shiny inputs
input <- list()
input$nps_split_km <- 1.0
input$use_nps_length_for_nppsd <- FALSE
input$modified_platform_type <- "Small Boat"
input$modified_survey_type <- "Pelagic Survey"
input$fly_bird_method <- "Continuous" # Or "Snapshot"
input$survey_target <- "Birds"
input$source <- "CSS"
input$pi_credit <- "CSS"
input$max_point_interval_seconds <- "120"

# input$input_files$datapath <- "../../test_data/2022-06-29_noaa/HB2204 Compiled Data.csv"
# input$input_files$datapath <- "../../test_data/2022-06-29_noaa/HB2204 Compiled Data One Track.csv"
# input$input_files$datapath <- "../../test_data/2024-09-03_kefj/Goldeneye/observations_v2.2.8_20240607_111856.csv"
# input$input_files$datapath <- "../../test_data/2025-02-02_katm/observations_v2.2.8_20240708_133403.csv"
input$input_files$datapath <- "../../test_data/2025-02-02_katm/observations_v2.2.8_20240607_111856.csv"
input$optional_fields_json_file$datapath <- "../../test_data/2025-02-02_katm/optionalFields_Nearshore_2024_with_bins_for_distance_sampling.json"
input$species_json_file$datapath <- "../../test_data/2025-02-02_katm/speciesValidationList_Nearshore_2024_with_bins_for_distance_sampling.json"
# input$nppsd_species_crosswalk_file$datapath <- "../../test_data/2022-05-09/nppsd_species_crosswalk_2022-05-09.csv"
input$nppsd_species_crosswalk_file$datapath <- NULL

input_files <- input$input_files$datapath
species_json_file <- input$species_json_file$datapath
optional_fields_json_file <- input$optional_fields_json_file$datapath
# nppsd_species_crosswalk_file <- input$nppsd_species_crosswalk_file$datapath

x$timezone <- Sys.timezone()

x <- read_files(
  input$input_files$datapath,
  input$species_json_file$datapath,
  input$optional_fields_json_file$datapath,
  input$nppsd_species_crosswalk_file$datapath,
  x
)

x <- get_unique_values(x)

x <- generate_tracks(x)

x <- generate_nppsd_splits(x)

x <- generate_nps_splits(x)

x <- generate_transects(x)

x <- generate_observations(x)

x <- make_tbl_location(x)

x <- make_tbl_data_obs(x)

x <- make_nps_transects(x)

x <- make_nps_obs(x)

x <- generate_species_summary(x)

# vim: set ft=r fenc=utf-8 tw=80 ts=2 sw=2 sts=2:
