///|
pub(all) struct CampaignConfig {
  seed : Int64
  executions : Int
  initial_inputs : Int
  crossover_percent : Int
  generation_budget : GenerationBudget
  mutation_config : MutationConfig
  crossover_config : CrossoverConfig
  corpus_limits : CorpusLimits
  max_events : Int
} derive(Debug, Eq)

///|
pub fn CampaignConfig::default(seed? : Int64 = 1L) -> CampaignConfig {
  {
    seed,
    executions: 10000,
    initial_inputs: 16,
    crossover_percent: 10,
    generation_budget: GenerationBudget::default(),
    mutation_config: MutationConfig::default(),
    crossover_config: CrossoverConfig::default(),
    corpus_limits: CorpusLimits::default(),
    max_events: 4096,
  }
}

///|
pub(all) enum CampaignEventKind {
  Generated
  Mutated(MutationStrategy)
  Crossed
  Admitted(AdmissionReason)
  RejectedInput(RejectionReason)
  FailureFound(FailureFingerprint)
  MutationSkipped
} derive(Debug, Eq)

///|
pub(all) struct CampaignEvent {
  execution : Int
  input_bytes : Int
  corpus_entries : Int
  kind : CampaignEventKind
} derive(Debug, Eq)

///|
pub(all) struct CampaignStatistics {
  requested_executions : Int
  completed_executions : Int
  generated_inputs : Int
  mutated_inputs : Int
  crossed_inputs : Int
  admitted_inputs : Int
  rejected_inputs : Int
  unique_failures : Int
  mutation_failures : Int
  final_corpus : CorpusStatistics
} derive(Debug, Eq)

///|
pub(all) struct CampaignResult {
  statistics : CampaignStatistics
  corpus : Array[CorpusEntry]
  failures : Array[ReplayRecord]
  events : Array[CampaignEvent]
} derive(Debug, Eq)

///|
pub(all) enum CampaignError {
  InvalidCampaignConfig(String)
  InitialGenerationFailed(GenerationError)
  CorpusCouldNotBeSeeded
} derive(Debug, Eq)

///|
fn CampaignConfig::validate(
  self : CampaignConfig,
) -> Result[Unit, CampaignError] {
  if self.executions <= 0 {
    Err(InvalidCampaignConfig("executions must be positive"))
  } else if self.initial_inputs <= 0 {
    Err(InvalidCampaignConfig("initial_inputs must be positive"))
  } else if self.crossover_percent < 0 || self.crossover_percent > 100 {
    Err(InvalidCampaignConfig("crossover_percent must be within 0..100"))
  } else if self.max_events < 0 {
    Err(InvalidCampaignConfig("max_events must not be negative"))
  } else {
    Ok(())
  }
}

///|
priv struct CampaignCounters {
  mut completed : Int
  mut generated : Int
  mut mutated : Int
  mut crossed : Int
  mut admitted : Int
  mut rejected : Int
  mut mutation_failures : Int
}

///|
fn CampaignCounters::new() -> CampaignCounters {
  {
    completed: 0,
    generated: 0,
    mutated: 0,
    crossed: 0,
    admitted: 0,
    rejected: 0,
    mutation_failures: 0,
  }
}

///|
fn push_event(
  events : Array[CampaignEvent],
  maximum : Int,
  event : CampaignEvent,
) -> Unit {
  if events.length() < maximum {
    events.push(event)
  }
}

///|
fn record_failure(
  failures : Array[ReplayRecord],
  seen : @hashmap.HashMap[FailureFingerprint, Unit],
  maximum : Int,
  target : Target,
  input : Bytes,
  observation : TargetObservation,
  seed : Int64,
  budget : GenerationBudget,
) -> FailureFingerprint? {
  match observation.failure_fingerprint() {
    None => None
    Some(fingerprint) =>
      if !seen.contains(fingerprint) && failures.length() < maximum {
        seen.set(fingerprint, ())
        failures.push(
          ReplayRecord::new(
            target_name=target.name(),
            seed~,
            input~,
            observation~,
            generation_budget=budget,
          ),
        )
        Some(fingerprint)
      } else {
        None
      }
  }
}

///|
fn execute_candidate(
  corpus : Corpus,
  target : Target,
  tree : DerivationTree,
  origin : CorpusOrigin,
  seed : Int64,
  budget : GenerationBudget,
  counters : CampaignCounters,
  failures : Array[ReplayRecord],
  seen_failures : @hashmap.HashMap[FailureFingerprint, Unit],
  events : Array[CampaignEvent],
  max_events : Int,
) -> AdmissionDecision {
  let input = tree.render()
  let observation = target.run(input)
  counters.completed += 1
  let new_failure = record_failure(
    failures,
    seen_failures,
    corpus.limits.max_failures,
    target,
    input,
    observation,
    seed,
    budget,
  )
  match new_failure {
    None => ()
    Some(fingerprint) =>
      push_event(events, max_events, {
        execution: counters.completed,
        input_bytes: input.length(),
        corpus_entries: corpus.length(),
        kind: FailureFound(fingerprint),
      })
  }
  let decision = corpus.consider(tree, observation, origin)
  match decision {
    Admitted(_, reason) => {
      counters.admitted += 1
      push_event(events, max_events, {
        execution: counters.completed,
        input_bytes: input.length(),
        corpus_entries: corpus.length(),
        kind: Admitted(reason),
      })
    }
    Rejected(reason) => {
      counters.rejected += 1
      push_event(events, max_events, {
        execution: counters.completed,
        input_bytes: input.length(),
        corpus_entries: corpus.length(),
        kind: RejectedInput(reason),
      })
    }
  }
  decision
}

///|
fn campaign_seed(config : CampaignConfig, ordinal : Int) -> Int64 {
  config.seed + Int64::from_int(ordinal) * 104729L
}

///|
fn generated_candidate(
  grammar : Grammar,
  config : CampaignConfig,
  ordinal : Int,
) -> Result[DerivationTree, GenerationError] {
  grammar.generate(
    seed=campaign_seed(config, ordinal),
    budget=config.generation_budget,
  )
}

///|
fn evolved_candidate(
  grammar : Grammar,
  corpus : Corpus,
  scheduler : EnergyScheduler,
  random : Random,
  config : CampaignConfig,
  ordinal : Int,
  counters : CampaignCounters,
) -> (DerivationTree, CorpusOrigin, CampaignEventKind, CorpusEntry?)? {
  let decision = match scheduler.select(corpus) {
    None => return None
    Some(value) => value
  }
  let parent = corpus.entry(decision.entry_id).unwrap()
  parent.record_execution()
  let seed = campaign_seed(config, ordinal)
  if corpus.length() > 1 && random.chance(config.crossover_percent, 100) {
    let mut donor_id = random.below(corpus.length() - 1)
    if donor_id >= parent.id {
      donor_id += 1
    }
    let donor = corpus.entry(donor_id).unwrap()
    match
      crossover(parent.tree, donor.tree, seed~, config=config.crossover_config) {
      Ok(result) => {
        counters.crossed += 1
        return Some(
          (result.tree, Crossed(parent.id, donor.id), Crossed, Some(parent)),
        )
      }
      Err(_) => ()
    }
  }
  match grammar.mutate(parent.tree, seed~, config=config.mutation_config) {
    Ok(result) => {
      counters.mutated += 1
      Some(
        (
          result.tree,
          Mutated(parent.id, result.detail),
          Mutated(result.strategy),
          Some(parent),
        ),
      )
    }
    Err(_) => {
      counters.mutation_failures += 1
      match generated_candidate(grammar, config, ordinal) {
        Err(_) => None
        Ok(tree) => {
          counters.generated += 1
          Some((tree, Generated(seed), MutationSkipped, Some(parent)))
        }
      }
    }
  }
}

///|
pub fn run_campaign(
  grammar : Grammar,
  target : Target,
  config : CampaignConfig,
) -> Result[CampaignResult, CampaignError] {
  match config.validate() {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let corpus = Corpus::new(limits=config.corpus_limits)
  let counters = CampaignCounters::new()
  let failures : Array[ReplayRecord] = []
  let seen_failures : @hashmap.HashMap[FailureFingerprint, Unit] = @hashmap.HashMap([],
  )
  let events : Array[CampaignEvent] = []
  let initial_count = config.initial_inputs.min(config.executions)
  for ordinal = 0; ordinal < initial_count; ordinal = ordinal + 1 {
    let seed = campaign_seed(config, ordinal)
    let tree = match generated_candidate(grammar, config, ordinal) {
      Err(error) => return Err(InitialGenerationFailed(error))
      Ok(value) => value
    }
    counters.generated += 1
    push_event(events, config.max_events, {
      execution: counters.completed + 1,
      input_bytes: tree.byte_length(),
      corpus_entries: corpus.length(),
      kind: Generated,
    })
    ignore(
      execute_candidate(
        corpus,
        target,
        tree,
        Generated(seed),
        seed,
        config.generation_budget,
        counters,
        failures,
        seen_failures,
        events,
        config.max_events,
      ),
    )
  }
  if corpus.is_empty() {
    return Err(CorpusCouldNotBeSeeded)
  }
  let scheduler = EnergyScheduler::new(config.seed + 17L)
  let random = Random::new(config.seed + 31L)
  let mut ordinal = initial_count
  while counters.completed < config.executions {
    match
      evolved_candidate(
        grammar, corpus, scheduler, random, config, ordinal, counters,
      ) {
      None => {
        counters.mutation_failures += 1
        let tree = match generated_candidate(grammar, config, ordinal) {
          Err(error) => return Err(InitialGenerationFailed(error))
          Ok(value) => value
        }
        counters.generated += 1
        ignore(
          execute_candidate(
            corpus,
            target,
            tree,
            Generated(campaign_seed(config, ordinal)),
            campaign_seed(config, ordinal),
            config.generation_budget,
            counters,
            failures,
            seen_failures,
            events,
            config.max_events,
          ),
        )
      }
      Some((tree, origin, event_kind, parent)) => {
        push_event(events, config.max_events, {
          execution: counters.completed + 1,
          input_bytes: tree.byte_length(),
          corpus_entries: corpus.length(),
          kind: event_kind,
        })
        let decision = execute_candidate(
          corpus,
          target,
          tree,
          origin,
          campaign_seed(config, ordinal),
          config.generation_budget,
          counters,
          failures,
          seen_failures,
          events,
          config.max_events,
        )
        match (decision, parent) {
          (Admitted(_, _), Some(value)) => value.record_discovery()
          _ => ()
        }
      }
    }
    ordinal += 1
  }
  let statistics : CampaignStatistics = {
    requested_executions: config.executions,
    completed_executions: counters.completed,
    generated_inputs: counters.generated,
    mutated_inputs: counters.mutated,
    crossed_inputs: counters.crossed,
    admitted_inputs: counters.admitted,
    rejected_inputs: counters.rejected,
    unique_failures: failures.length(),
    mutation_failures: counters.mutation_failures,
    final_corpus: corpus.statistics(),
  }
  Ok({ statistics, corpus: corpus.entries(), failures, events, })
}