///|
/// Event persisted by the in-memory store.
pub(all) struct StoredEvent {
  sequence : Int
  event : Event
  source : String
  stored_tick : Int
} derive(Eq, Debug)

///|
/// Query criteria. Empty strings disable the corresponding filter.
pub(all) struct EventQuery {
  name : String
  topic_pattern : String
  tag : String
  correlation_id : String
  min_priority : Int
  after_sequence : Int
  limit : Int
} derive(Eq, Debug)

///|
pub(all) struct EventStore {
  records : Array[StoredEvent]
  next_sequence : Int
  max_entries : Int
} derive(Eq, Debug)

///|
pub(all) struct StoreStats {
  entries : Int
  first_sequence : Int
  last_sequence : Int
  next_sequence : Int
} derive(Eq, Debug)

///|
pub fn EventQuery::all() -> EventQuery {
  {
    name: "",
    topic_pattern: "",
    tag: "",
    correlation_id: "",
    min_priority: 0,
    after_sequence: -1,
    limit: 0,
  }
}

///|
pub fn EventQuery::with_name(self : EventQuery, name : String) -> EventQuery {
  { ..self, name, }
}

///|
pub fn EventQuery::with_topic(
  self : EventQuery,
  topic_pattern : String,
) -> EventQuery {
  { ..self, topic_pattern, }
}

///|
pub fn EventQuery::with_tag(self : EventQuery, tag : String) -> EventQuery {
  { ..self, tag, }
}

///|
pub fn EventQuery::with_correlation(
  self : EventQuery,
  correlation_id : String,
) -> EventQuery {
  { ..self, correlation_id, }
}

///|
pub fn EventQuery::with_min_priority(
  self : EventQuery,
  min_priority : Int,
) -> EventQuery {
  { ..self, min_priority, }
}

///|
pub fn EventQuery::after(self : EventQuery, after_sequence : Int) -> EventQuery {
  { ..self, after_sequence, }
}

///|
pub fn EventQuery::take(self : EventQuery, limit : Int) -> EventQuery {
  { ..self, limit, }
}

///|
pub fn EventQuery::matches(self : EventQuery, record : StoredEvent) -> Bool {
  if record.sequence <= self.after_sequence {
    return false
  }
  if self.name != "" && record.event.name != self.name {
    return false
  }
  if !topic_matches(self.topic_pattern, record.event.topic) {
    return false
  }
  if self.tag != "" && !record.event.has_tag(self.tag) {
    return false
  }
  if self.correlation_id != "" &&
    record.event.correlation_id != self.correlation_id {
    return false
  }
  record.event.priority >= self.min_priority
}

///|
pub fn EventStore::new(max_entries? : Int = 0) -> EventStore {
  { records: [], next_sequence: 1, max_entries }
}

///|
fn trim_records(
  records : Array[StoredEvent],
  max_entries : Int,
) -> Array[StoredEvent] {
  if max_entries <= 0 || records.length() <= max_entries {
    return records
  }
  let kept : Array[StoredEvent] = []
  let skip = records.length() - max_entries
  let mut index = 0
  for record in records {
    if index >= skip {
      kept.push(record)
    }
    index = index + 1
  }
  kept
}

///|
pub fn EventStore::append(
  self : EventStore,
  event : Event,
  source? : String = "",
  stored_tick? : Int = 0,
) -> (EventStore, StoredEvent) {
  let record = { sequence: self.next_sequence, event, source, stored_tick }
  let records : Array[StoredEvent] = []
  for old in self.records {
    records.push(old)
  }
  records.push(record)
  (
    {
      records: trim_records(records, self.max_entries),
      next_sequence: self.next_sequence + 1,
      max_entries: self.max_entries,
    },
    record,
  )
}

///|
pub fn EventStore::append_many(
  self : EventStore,
  events : Array[Event],
  source? : String = "",
  stored_tick? : Int = 0,
) -> (EventStore, Array[StoredEvent]) {
  let added : Array[StoredEvent] = []
  let mut store = self
  for event in events {
    let (next, record) = store.append(event, source~, stored_tick~)
    store = next
    added.push(record)
  }
  (store, added)
}

///|
pub fn EventStore::length(self : EventStore) -> Int {
  self.records.length()
}

///|
pub fn EventStore::is_empty(self : EventStore) -> Bool {
  self.records.length() == 0
}

///|
pub fn EventStore::query(
  self : EventStore,
  query : EventQuery,
) -> Array[StoredEvent] {
  let result : Array[StoredEvent] = []
  for record in self.records {
    if query.matches(record) &&
      (query.limit <= 0 || result.length() < query.limit) {
      result.push(record)
    }
  }
  result
}

///|
pub fn EventStore::get(self : EventStore, sequence : Int) -> StoredEvent? {
  for record in self.records {
    if record.sequence == sequence {
      return Some(record)
    }
  }
  None
}

///|
pub fn EventStore::latest(self : EventStore) -> StoredEvent? {
  if self.records.length() == 0 {
    None
  } else {
    self.records.get(self.records.length() - 1)
  }
}

///|
pub fn EventStore::events(self : EventStore) -> Array[Event] {
  let events : Array[Event] = []
  for record in self.records {
    events.push(record.event)
  }
  events
}

///|
pub fn EventStore::remove_before(
  self : EventStore,
  sequence : Int,
) -> EventStore {
  let records : Array[StoredEvent] = []
  for record in self.records {
    if record.sequence >= sequence {
      records.push(record)
    }
  }
  { ..self, records, }
}

///|
pub fn EventStore::clear(self : EventStore) -> EventStore {
  { ..self, records: [] }
}

///|
pub fn EventStore::stats(self : EventStore) -> StoreStats {
  let mut first = 0
  let mut last = 0
  match self.records.get(0) {
    Some(record) => first = record.sequence
    None => ()
  }
  match self.latest() {
    Some(record) => last = record.sequence
    None => ()
  }
  {
    entries: self.records.length(),
    first_sequence: first,
    last_sequence: last,
    next_sequence: self.next_sequence,
  }
}

///|
pub fn EventStore::replay(
  self : EventStore,
  bus : EventBus,
  query? : EventQuery = EventQuery::all(),
) -> (EventBus, Array[DispatchReport]) {
  let events : Array[Event] = []
  for record in self.query(query) {
    events.push(record.event)
  }
  bus.publish_many(events)
}

///|
pub fn EventStore::topic_count(
  self : EventStore,
  topic_pattern : String,
) -> Int {
  self.query(EventQuery::all().with_topic(topic_pattern)).length()
}

///|
pub fn EventStore::correlation_count(
  self : EventStore,
  correlation_id : String,
) -> Int {
  self.query(EventQuery::all().with_correlation(correlation_id)).length()
}