///|
pub(all) struct ScheduleConfig {
  base_energy : Int
  maximum_energy : Int
  novelty_bonus : Int
  discovery_bonus : Int
  failure_bonus : Int
  cost_penalty : Int
  selection_penalty : Int
} derive(Debug, Eq)

///|
pub fn ScheduleConfig::default() -> ScheduleConfig {
  {
    base_energy: 4,
    maximum_energy: 64,
    novelty_bonus: 16,
    discovery_bonus: 4,
    failure_bonus: 8,
    cost_penalty: 1,
    selection_penalty: 1,
  }
}

///|
pub(all) struct ScheduleDecision {
  entry_id : Int
  energy : Int
  reason : String
} derive(Debug, Eq)

///|
pub struct EnergyScheduler {
  config : ScheduleConfig
  random : Random
  mut rounds : Int
}

///|
pub fn EnergyScheduler::new(
  seed : Int64,
  config? : ScheduleConfig = ScheduleConfig::default(),
) -> EnergyScheduler {
  { config, random: Random::new(seed), rounds: 0, }
}

///|
fn rare_feature_score(corpus : Corpus, entry : CorpusEntry) -> Int {
  let mut score = 0
  for item in entry.observation.canonical_features() {
    let frequency = corpus.feature_frequency(item)
    if frequency <= 1 {
      score += 8
    } else if frequency <= 3 {
      score += 4
    } else if frequency <= 8 {
      score += 2
    } else if frequency <= 32 {
      score += 1
    }
  }
  score
}

///|
fn signature_score(corpus : Corpus, entry : CorpusEntry) -> Int {
  let frequency = corpus.signature_frequency(entry.signature)
  if frequency <= 1 {
    8
  } else if frequency <= 4 {
    4
  } else if frequency <= 16 {
    2
  } else {
    0
  }
}

///|
pub fn EnergyScheduler::energy_for(
  self : EnergyScheduler,
  corpus : Corpus,
  entry : CorpusEntry,
) -> Int {
  let mut energy = self.config.base_energy
  energy += rare_feature_score(corpus, entry)
  energy += signature_score(corpus, entry)
  if entry.executions == 0 {
    energy += self.config.novelty_bonus
  }
  energy += entry.discoveries.min(16) * self.config.discovery_bonus
  if entry.is_failure() {
    energy += self.config.failure_bonus
  }
  energy -= entry.observation.cost_class * self.config.cost_penalty
  energy -= entry.selections / 4 * self.config.selection_penalty
  energy.max(1).min(self.config.maximum_energy.max(1))
}

///|
pub fn EnergyScheduler::refresh(
  self : EnergyScheduler,
  corpus : Corpus,
) -> Array[Int] {
  let energies : Array[Int] = []
  for entry in corpus.entries() {
    let energy = self.energy_for(corpus, entry)
    entry.set_energy(energy)
    energies.push(energy)
  }
  energies
}

///|
fn decision_reason(entry : CorpusEntry, energy : Int) -> String {
  let category = if entry.is_failure() {
    "failure"
  } else if entry.executions == 0 {
    "unexplored"
  } else if entry.discoveries > 0 {
    "productive"
  } else {
    "scheduled"
  }
  category + " energy=" + energy.to_string()
}

///|
pub fn EnergyScheduler::select(
  self : EnergyScheduler,
  corpus : Corpus,
) -> ScheduleDecision? {
  if corpus.is_empty() {
    return None
  }
  let energies = self.refresh(corpus)
  let selected = self.random.weighted_index(energies).unwrap()
  let entry = corpus.entry(selected).unwrap()
  entry.record_selection()
  self.rounds += 1
  Some({
    entry_id: entry.id,
    energy: energies[selected],
    reason: decision_reason(entry, energies[selected]),
  })
}

///|
pub fn EnergyScheduler::rounds(self : EnergyScheduler) -> Int {
  self.rounds
}

///|
pub fn EnergyScheduler::seed(self : EnergyScheduler) -> Int64 {
  self.random.seed()
}

///|
pub fn EnergyScheduler::rank(
  self : EnergyScheduler,
  corpus : Corpus,
) -> Array[ScheduleDecision] {
  let result : Array[ScheduleDecision] = []
  let energies = self.refresh(corpus)
  for entry in corpus.entries() {
    result.push({
      entry_id: entry.id,
      energy: energies[entry.id],
      reason: decision_reason(entry, energies[entry.id]),
    })
  }
  let ordered : Array[ScheduleDecision] = []
  for item in result {
    let mut inserted = false
    for index = 0; index < ordered.length(); index = index + 1 {
      if item.energy > ordered[index].energy ||
        (
          item.energy == ordered[index].energy &&
          item.entry_id < ordered[index].entry_id
        ) {
        ordered.insert(index, item)
        inserted = true
        break
      }
    }
    if !inserted {
      ordered.push(item)
    }
  }
  ordered
}