## ----setup, include=FALSE-----------------------------------------------------
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(cdCAT)
set.seed(42)

## ----item-bank----------------------------------------------------------------
# Q-matrix: 12 items x 2 attributes
# Items 1-4:  domain "Algebra"   (attribute 1 only)
# Items 5-8:  domain "Geometry"  (attribute 2 only)
# Items 9-12: domain "Mixed"     (both attributes)
Q <- matrix(c(
  1, 0,   # item 1
  1, 0,   # item 2
  1, 0,   # item 3
  1, 0,   # item 4
  0, 1,   # item 5
  0, 1,   # item 6
  0, 1,   # item 7
  0, 1,   # item 8
  1, 1,   # item 9
  1, 1,   # item 10
  1, 1,   # item 11
  1, 1    # item 12
), nrow = 12, ncol = 2, byrow = TRUE)

slip  <- c(0.10, 0.12, 0.08, 0.11,   # Algebra
           0.10, 0.09, 0.12, 0.11,   # Geometry
           0.10, 0.11, 0.09, 0.12)   # Mixed

guess <- c(0.20, 0.18, 0.22, 0.19,   # Algebra
           0.20, 0.21, 0.18, 0.20,   # Geometry
           0.15, 0.17, 0.16, 0.18)   # Mixed

items <- cdcat_items(
  q_matrix = Q,
  model    = "DINA",
  slip     = slip,
  guess    = guess
)

# Content domain vector (one label per item)
content <- c(
  rep("Algebra",  4),
  rep("Geometry", 4),
  rep("Mixed",    4)
)

print(items)

## ----content-setup------------------------------------------------------------
# Target: 33% from each domain
content_prop <- c(
  Algebra  = 1/3,
  Geometry = 1/3,
  Mixed    = 1/3
)

## ----content-session----------------------------------------------------------
session_cb <- CdcatSession$new(
  items        = items,
  criterion    = "PWKL",
  method       = "MAP",
  min_items    = 9L,    # force all items to be administered for illustration
  max_items    = 9L,
  content      = content,
  content_prop = content_prop
)

print(session_cb)

# Simulate a respondent who masters both attributes
simulated_responses <- c(1, 1, 1, 1,   # Algebra items  (correct)
                         0, 0, 0, 0,   # Geometry items (incorrect)
                         1, 0, 1, 0)   # Mixed items    (mixed)

repeat {
  item <- session_cb$next_item()
  if (item == 0) break
  session_cb$update(item, simulated_responses[item])
}

res_cb <- session_cb$result()

## ----content-results----------------------------------------------------------
domain_counts <- table(content[res_cb$administered])
domain_prop   <- round(domain_counts / res_cb$n_items, 2)

cat("Items administered:", res_cb$administered, "\n")
cat("Domain counts     :\n")
print(domain_counts)
cat("Domain proportions:\n")
print(domain_prop)
cat("Target proportions:", round(content_prop, 2), "\n")

## ----content-direct-----------------------------------------------------------
# After administering items 1 and 2 (both Algebra),
# the gap favours Geometry or Mixed
candidates <- apply_content_balancing(
  candidate_items = 3:12,
  administered    = c(1L, 2L),
  content         = content,
  content_prop    = content_prop
)
cat("Filtered candidates:", candidates, "\n")
cat("Their domains      :", content[candidates], "\n")

## ----sh-session---------------------------------------------------------------
# Items 9-12 (Mixed) are very informative; limit their exposure to 60%
exposure_sh        <- rep(0.9, 12)
exposure_sh[9:12]  <- 0.8

session_sh <- CdcatSession$new(
  items    = items,
  criterion = "PWKL",
  method   = "MAP",
  min_items = 6L,
  max_items = 6L,
  exposure  = exposure_sh
)

print(session_sh)

repeat {
  item <- session_sh$next_item()
  if (item == 0) break
  session_sh$update(item, simulated_responses[item])
}

res_sh <- session_sh$result()
cat("Items administered:", res_sh$administered, "\n")
cat("Estimated profile :", res_sh$alpha_hat, "\n")

## ----rq-session---------------------------------------------------------------
# At positions 1-3 draw from top-3; positions 4-6 draw from top-2
exposure_rq      <- rep(1L, 12)
exposure_rq[1:3] <- 3L
exposure_rq[4:6] <- 2L

session_rq <- CdcatSession$new(
  items     = items,
  criterion = "PWKL",
  method    = "MAP",
  min_items = 6L,
  max_items = 6L,
  exposure  = exposure_rq
)

print(session_rq)

repeat {
  item <- session_rq$next_item()
  if (item == 0) break
  session_rq$update(item, simulated_responses[item])
}

res_rq <- session_rq$result()
cat("Items administered:", res_rq$administered, "\n")
cat("Estimated profile :", res_rq$alpha_hat, "\n")

## ----exposure-direct----------------------------------------------------------
# Sympson-Hetter: item 10 has score 0.9 but only 20% acceptance probability
scores    <- c(0.4, 0.6, 0.7, 0.9, 0.3, 0.5)
available <- 7:12

# Global exposure vector (length = total items in bank)
p_sh <- rep(0.9, 12)
p_sh[10] <- 0.2   # item with score 0.9

set.seed(123)
selected <- apply_sympson_hetter(scores, available, p_sh)
cat("Selected item (Sympson-Hetter):", selected, "\n")

# Randomesque: draw from top-2
selected_rq <- apply_randomesque(scores, available, n = 2L)
cat("Selected item (Randomesque)   :", selected_rq, "\n")

## ----shadow-greedy------------------------------------------------------------
greedy_shadow <- function(scores, items, administered) {
  scores[administered] <- -Inf
  which.max(scores)
}

session_shadow_greedy <- CdcatSession$new(
  items      = items,
  criterion  = "PWKL",
  method     = "MAP",
  min_items  = 6L,
  max_items  = 6L,
  constr_fun = greedy_shadow
)

print(session_shadow_greedy)

## ----shadow-custom------------------------------------------------------------
make_constrained_shadow <- function(content, enemy_pairs) {

  function(scores, items, administered) {

    J         <- items$n_items
    available <- setdiff(seq_len(J), administered)

    if (length(available) == 0)
      return(NA_integer_)

    # --- Enemy item constraint
    for (pair in enemy_pairs) {
      if (pair[1] %in% administered)
        available <- setdiff(available, pair[2])
      if (pair[2] %in% administered)
        available <- setdiff(available, pair[1])
    }

    if (length(available) == 0)
      available <- setdiff(seq_len(J), administered)  # fallback

    # --- Domain cap: at most 2 items per domain in any window of 4
    if (length(administered) > 0) {
      domain_counts <- table(content[administered])
      capped_domains <- names(domain_counts[domain_counts >= 2])
      if (length(capped_domains) > 0 && length(available) > 1) {
        filtered <- available[!content[available] %in% capped_domains]
        if (length(filtered) > 0)
          available <- filtered
      }
    }

    # --- Select highest-scoring item from filtered pool
    available[which.max(scores[available])]
  }
}

constr_fn <- make_constrained_shadow(
  content     = content,
  enemy_pairs = list(c(3L, 7L))  # items 3 and 7 cannot coexist
)

session_shadow <- CdcatSession$new(
  items      = items,
  criterion  = "PWKL",
  method     = "MAP",
  min_items  = 8L,
  max_items  = 8L,
  constr_fun = constr_fn
)

repeat {
  item <- session_shadow$next_item()
  if (item == 0) break
  session_shadow$update(item, simulated_responses[item])
}

res_shadow <- session_shadow$result()
cat("Items administered:", res_shadow$administered, "\n")
cat("Domains           :", content[res_shadow$administered], "\n")

# Verify enemy constraint: items 3 and 7 do not coexist
has_3 <- 3L %in% res_shadow$administered
has_7 <- 7L %in% res_shadow$administered
cat("Enemy pair (3, 7) both present:", has_3 & has_7, "\n")

## ----shadow-lp, eval=FALSE----------------------------------------------------
# # This example requires: install.packages("lpSolve")
# 
# make_lp_shadow <- function(content, content_prop, n_items_total) {
# 
#   function(scores, items, administered) {
# 
#     J    <- items$n_items
#     resp <- integer(J)
#     resp[administered] <- 1L
# 
#     # Build constraint matrix
#     # Row 1: total items == n_items_total
#     # Rows 2-4: domain proportions (each domain gets floor(n_items_total/3) items)
#     n_per_domain <- floor(n_items_total / length(content_prop))
#     domains      <- names(content_prop)
#     n_constr     <- 1L + length(domains)
# 
#     lhs  <- matrix(0, nrow = n_constr, ncol = J)
#     dirs <- character(n_constr)
#     rhs  <- numeric(n_constr)
# 
#     # Already-administered items must stay
#     lhs  <- rbind(lhs, resp)
#     dirs <- c(dirs, "==")
#     rhs  <- c(rhs, sum(resp))
# 
#     # Row 1: total items
#     lhs[1, ]  <- 1
#     dirs[1]   <- "=="
#     rhs[1]    <- n_items_total
# 
#     # Rows 2+: per-domain counts
#     for (i in seq_along(domains)) {
#       lhs[i + 1L, content == domains[i]] <- 1
#       dirs[i + 1L] <- ">="
#       rhs[i + 1L]  <- n_per_domain
#     }
# 
#     obj <- scores
#     obj[administered] <- obj[administered] * resp[administered]
# 
#     out <- lpSolve::lp(
#       direction  = "max",
#       objective.in  = obj,
#       const.mat  = lhs,
#       const.dir  = dirs,
#       const.rhs  = rhs,
#       all.bin    = TRUE
#     )
# 
#     if (out$status != 0L)
#       stop("lpSolve could not find a feasible solution.")
# 
#     solution <- out$solution
#     solution[administered] <- 0
#     if (sum(solution) == 0L) return(NA_integer_)
#     as.integer(which.max(solution * scores))
#   }
# }
# 
# session_lp <- CdcatSession$new(
#   items      = items,
#   criterion  = "PWKL",
#   method     = "MAP",
#   min_items  = 9L,
#   max_items  = 9L,
#   constr_fun = make_lp_shadow(content, content_prop, n_items_total = 9L)
# )
# 
# repeat {
#   item <- session_lp$next_item()
#   if (item == 0) break
#   session_lp$update(item, simulated_responses[item])
# }
# 
# res_lp <- session_lp$result()
# cat("Items administered:", res_lp$administered, "\n")
# cat("Domains           :", content[res_lp$administered], "\n")

## ----combined-----------------------------------------------------------------
# Content balancing + Sympson-Hetter exposure
exposure_combined        <- rep(0.9, 12)
exposure_combined[9:12]  <- 0.5   # limit Mixed items

session_combined <- CdcatSession$new(
  items        = items,
  criterion    = "PWKL",
  method       = "MAP",
  min_items    = 6L,
  max_items    = 6L,
  content      = content,
  content_prop = content_prop,
  exposure     = exposure_combined
)

print(session_combined)

repeat {
  item <- session_combined$next_item()
  if (item == 0) break
  session_combined$update(item, simulated_responses[item])
}

res_combined <- session_combined$result()
cat("Items administered:", res_combined$administered, "\n")
cat("Domains           :", content[res_combined$administered], "\n")
cat("Estimated profile :", res_combined$alpha_hat, "\n")

