///|
pub(all) suberror ProcessError {
  InvalidLog(path~ : String, reason~ : String)
  Capacity(resource~ : String, limit~ : Int32)
} derive(@debug.Debug)

///|
pub extend ProcessError with @debug.Debug::{to_repr}

///|
fn process_label(value : String, path : String) -> Unit raise ProcessError {
  if value.is_empty() || value.length() > 256 {
    raise InvalidLog(path~, reason="expected 1..256 UTF-16 units")
  }
  for ch in value {
    if ch.to_int() < 32 || ch.to_int() == 127 {
      raise InvalidLog(path~, reason="control characters are forbidden")
    }
  }
}

///|
/// A completed activity occurrence. Intervals may overlap within a case.
pub struct LogEvent {
  id : String
  case_id : String
  activity : String
  started : Int64
  completed : Int64
  resource : String?
} derive(Eq, @debug.Debug)

///|
pub extend LogEvent with Eq::{equal, not_equal}

///|
pub extend LogEvent with @debug.Debug::{to_repr}

///|
pub fn LogEvent::new(
  id : String,
  case_id : String,
  activity : String,
  started : Int64,
  completed : Int64,
  resource~ : String? = None,
) -> LogEvent raise ProcessError {
  process_label(id, "event.id")
  process_label(case_id, "event.case_id")
  process_label(activity, "event.activity")
  if started < 0L || completed < started || completed > 9007199254740991L {
    raise InvalidLog(
      path="event.interval",
      reason="expected 0 <= started <= completed <= 9007199254740991",
    )
  }
  if resource is Some(value) {
    process_label(value, "event.resource")
  }
  { id, case_id, activity, started, completed, resource, }
}

///|
pub fn LogEvent::id(self : LogEvent) -> String {
  self.id
}

///|
pub fn LogEvent::case_id(self : LogEvent) -> String {
  self.case_id
}

///|
pub fn LogEvent::activity(self : LogEvent) -> String {
  self.activity
}

///|
pub fn LogEvent::started(self : LogEvent) -> Int64 {
  self.started
}

///|
pub fn LogEvent::completed(self : LogEvent) -> Int64 {
  self.completed
}

///|
pub fn LogEvent::resource(self : LogEvent) -> String? {
  self.resource
}

///|
pub struct CaseTrace {
  id : String
  events : Array[LogEvent]
}

///|
pub fn CaseTrace::new(
  id : String,
  events : Array[LogEvent],
) -> CaseTrace raise ProcessError {
  process_label(id, "trace.id")
  for event in events {
    if event.case_id != id {
      raise InvalidLog(
        path="trace.events",
        reason="event belongs to another case",
      )
    }
  }
  if events.is_empty() {
    return { id, events: [], }
  }
  EventLog::new(events, max_cases=1).traces[0]
}

///|
pub fn CaseTrace::id(self : CaseTrace) -> String {
  self.id
}

///|
pub fn CaseTrace::events(self : CaseTrace) -> Array[LogEvent] {
  self.events.copy()
}

///|
pub fn CaseTrace::activities(self : CaseTrace) -> Array[String] {
  self.events.map(event => event.activity)
}

///|
pub fn CaseTrace::cycle_time(self : CaseTrace) -> Int64 {
  if self.events.is_empty() {
    return 0L
  }
  let mut completed = self.events[0].completed
  for event in self.events {
    if event.completed > completed {
      completed = event.completed
    }
  }
  completed - self.events[0].started
}

///|
pub struct EventLog {
  traces : Array[CaseTrace]
  event_count : Int32
}

///|
/// Sort by start time, completion time, then original input occurrence.
/// Event IDs are globally unique; case IDs are opaque and sorted lexically.
pub fn EventLog::new(
  events : Array[LogEvent],
  max_events~ : Int32 = 100000,
  max_cases~ : Int32 = 10000,
  max_trace~ : Int32 = 4096,
) -> EventLog raise ProcessError {
  if max_events < 1 ||
    max_events > 1000000 ||
    max_cases < 1 ||
    max_cases > 100000 ||
    max_trace < 1 ||
    max_trace > 10000 {
    raise InvalidLog(path="log.limits", reason="invalid bounded log capacity")
  }
  if events.length() > max_events {
    raise Capacity(resource="log events", limit=max_events)
  }
  let ids : Map[String, Bool] = Map([])
  let groups : Map[String, Array[(Int32, LogEvent)]] = Map([])
  for index, event in events {
    if ids.contains(event.id) {
      raise InvalidLog(path="event.id", reason="duplicate event id")
    }
    ids.set(event.id, true)
    let group = match groups.get(event.case_id) {
      Some(group) => group
      None => {
        if groups.length() >= max_cases {
          raise Capacity(resource="log cases", limit=max_cases)
        }
        let group : Array[(Int32, LogEvent)] = []
        groups.set(event.case_id, group)
        group
      }
    }
    if group.length() >= max_trace {
      raise Capacity(resource="case events", limit=max_trace)
    }
    group.push((index, event))
  }
  let keys = groups.keys().collect()
  keys.sort_by((a, b) => a.lexical_compare(b))
  let traces : Array[CaseTrace] = []
  for id in keys {
    let group = groups.get(id).unwrap()
    group.sort_by((a, b) => {
      let start = a.1.started.compare(b.1.started)
      if start != 0 {
        start
      } else {
        let end = a.1.completed.compare(b.1.completed)
        if end != 0 {
          end
        } else {
          a.0.compare(b.0)
        }
      }
    })
    traces.push({ id, events: group.map(item => item.1), })
  }
  { traces, event_count: events.length(), }
}

///|
pub fn EventLog::traces(self : EventLog) -> Array[CaseTrace] {
  self.traces.map(trace => { ..trace, events: trace.events.copy(), })
}

///|
pub fn EventLog::event_count(self : EventLog) -> Int32 {
  self.event_count
}

///|
pub fn EventLog::case_count(self : EventLog) -> Int32 {
  self.traces.length()
}