///| Represents a value located at a specific location.
enum Located[T, _] {
  Wrap(T)
  Empty
}

///| 
fn unwrap[T, L](self : Located[T, L]) -> T {
  match self {
    Located::Wrap(t) => t
    Located::Empty =>
      abort(
        "Bug: unwrapping an empty value. This should never happen. " +
        CONTACT_AUTHOR,
      )
  }
}

///|
pub(open) trait Location: Show + Hash {
  name(Self) -> String
}

///|
impl Eq for &Location with op_equal(self, other) {
  self.name() == other.name()
}

///|
struct ChoreoContext {
  location : &Location
  backend : Backend
  logger : &Logger
}

///| Run a choreography in the given location and backend.
/// Default logger is the backend logger.
pub async fn run_choreo[T, L : Location](
  backend : Backend,
  choreography : (ChoreoContext) -> T!Async,
  role : L,
  logger? : &Logger
) -> T {
  let logger = logger.or(backend.get_logger())
  let context = ChoreoContext::{ location: role, backend, logger }
  if not(backend.check_location(role)) {
    abort(
      "Bug: location " + role.name() + " is not registered. " + CONTACT_AUTHOR,
    )
  }
  choreography!(context)
}

///| Local computation at the given location.
pub fn locally[T, L : Location](
  self : ChoreoContext,
  location : L,
  computation : (Unwrapper[L]) -> T
) -> Located[T, L] {
  self.logger.info("|CHOREO|LOCAL computation at \{location.name()}")
  if location as &Location == self.location {
    let result = self.backend.run(location, computation)
    return Located::Wrap(result)
  } else {
    return Located::Empty
  }
}

///| Communication between two locations.
pub async fn comm[T : @json.FromJson + ToJson, From : Location, To : Location](
  self : ChoreoContext,
  from : From,
  to : To,
  value : Located[T, From]
) -> Located[T, To] {
  self.logger.info("|CHOREO|COMM message from \{from.name()} to \{to.name()}")
  if self.location == from {
    self.backend.send!(from, to, unwrap(value))
    Located::Empty
  } else if self.location == to {
    Located::Wrap(self.backend.recv!(from, to))
  } else {
    return Located::Empty
  }
}

///| Make a located boolean as global condition.
pub async fn condition[L : Location](
  self : ChoreoContext,
  loc : L,
  c : Located[Bool, L]
) -> Bool {
  self.logger.info("|CHOREO|CONDITION at \{loc.name()}")
  if self.location == loc {
    let c = c.unwrap()
    self.backend.broadcast!(loc, c)
    c
  } else {
    let value = self.backend.recv!(loc, self.location)
    value
  }
}

///| Run a sub choreography in the given location and backend.
pub async fn ChoreoContext::run_choreo[T, L : Location](
  self : ChoreoContext,
  choreography : (ChoreoContext) -> T!Async,
  role : L
) -> T {
  run_choreo!(self.backend, choreography, role, logger=self.logger)
}