sign_grammar <- function(x, dic, mapping = NULL) {

  if (is.null(mapping)) {
    path    <- system.file("extdata", "etcsl_mapping.txt", package = "sumer")
    mapping <- read.csv2(path, sep = ";", na.strings = "")
  }

  # --- Input: single character string ---
  if (length(x) != 1L) stop("x must be a single character string.")

  # --- Convert to cuneiform, then tokenize into individual signs ---
  cune <- as.cuneiform(x, mapping = mapping)
  cune_str <- as.character(cune)

  # Tokenize: extract each cuneiform character (U+12000..U+1254F)
  # and bracket tokens (angle bracket sequences)
  chars <- strsplit(cune_str, "")[[1]]
  n_ch  <- length(chars)
  tokens <- character(0)
  i <- 1L
  while (i <= n_ch) {
    cp <- utf8ToInt(chars[i])
    if (cp == 0x27E8L) {
      # Bracket token
      bracket <- chars[i]
      i <- i + 1L
      while (i <= n_ch && utf8ToInt(chars[i]) != 0x27E9L) {
        bracket <- paste0(bracket, chars[i])
        i <- i + 1L
      }
      if (i <= n_ch) {
        bracket <- paste0(bracket, chars[i])
        i <- i + 1L
      }
      tokens <- c(tokens, bracket)
    } else if (cp >= 0x12000L && cp <= 0x1254FL) {
      tokens <- c(tokens, chars[i])
      i <- i + 1L
    } else {
      i <- i + 1L
    }
  }

  n <- length(tokens)
  if (n == 0L) {
    return(data.frame(position   = integer(0),
                      sign_name  = character(0),
                      cuneiform  = character(0),
                      type       = character(0),
                      n          = integer(0),
                      stringsAsFactors = FALSE))
  }

  # --- Get sign name for each token ---
  sign_names <- vapply(tokens, function(tok) {
    as.character(as.sign_name(tok, mapping = mapping))
  }, character(1), USE.NAMES = FALSE)

  # --- Look up each sign in the dictionary ---
  trans <- dic[dic$row_type == "trans.", ]
  base_types <- c("S", "A", "V")

  # --- Build result: only rows with n > 0, or base types for unknown signs ---
  rows <- list()

  for (i in seq_len(n)) {
    s <- sign_names[i]
    entries <- trans[trans$sign_name == s, ]
    entries <- entries[!is.na(entries$type) & trimws(entries$type) != "", ]

    if (nrow(entries) > 0L) {
      # Known sign: collect counts per type, keep only n > 0
      type_counts <- list()
      for (j in seq_len(nrow(entries))) {
        t <- trimws(entries$type[j])
        cnt <- if (is.na(entries$count[j])) 1L else as.integer(entries$count[j])
        if (is.null(type_counts[[t]])) type_counts[[t]] <- 0L
        type_counts[[t]] <- type_counts[[t]] + cnt
      }
      for (t in names(type_counts)) {
        if (type_counts[[t]] > 0L) {
          rows[[length(rows) + 1L]] <- data.frame(
            position = i, sign_name = s, cuneiform = tokens[i],
            type = t, n = type_counts[[t]],
            stringsAsFactors = FALSE, row.names = NULL
          )
        }
      }
    } else {
      # Unknown sign: output base types with n = 0
      for (bt in base_types) {
        rows[[length(rows) + 1L]] <- data.frame(
          position = i, sign_name = s, cuneiform = tokens[i],
          type = bt, n = 0L,
          stringsAsFactors = FALSE, row.names = NULL
        )
      }
    }
  }

  out <- do.call(rbind, rows)
  rownames(out) <- NULL
  out
}
