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
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") {
    d <- read_csv(
      f,
      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$dlog_format <- FALSE

# 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/2023-08-24/short.csv"
input$optional_fields_json_file$datapath <- "../../test_data/2023-08-24/optionalFields_Nearshore_2022.json"
input$species_json_file$datapath <- "../../test_data/2023-08-24/speciesValidationList_Nearshore_2022.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 <- read_files(
  input$input_files$datapath,
  input$dlog_format,
  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:
