// The `eval` command (upstream `typst-cli/src/eval.rs`).

///|
/// Evaluate an input expression, potentially as a query over an existing
/// document.
async fn eval(command : EvalCommand) -> Unit raise HintedFailure {
  let world = SystemWorld::new(command.in_, command.world, command.process) catch {
    err => raise bail("\{err}")
  }

  // Reset everything and ensure that the main file is present.
  world.reset()
  (@library.World::source(world, @library.World::main(world)) |> ignore) catch {
    err => raise bail("\{err}")
  }

  // Compile the main file and get the introspector.
  let warned = compile_target(world, command.target)
  let warnings = warned.warnings.copy()
  match warned.output {
    // The target compiled successfully, continue with evaluating the input
    // expression.
    Ok(output) => {
      let expr_world : ExpressionWorld = {
        world,
        expression: @utf8.encode(command.expression),
      }
      let sink = @library.Sink::new()
      let eval_result = try
        evaluate_expression(
          command.expression,
          sink,
          expr_world,
          output.introspector,
        )
      catch {
        @library.SourceError(errors) => Err(errors)
      } noraise {
        value => Ok(value)
      }
      let errors = match eval_result {
        Err(errors) => {
          set_failed()
          errors
        }
        Ok(value) => {
          print_eval_result(value, command.format, command.pretty)
          []
        }
      }
      // Collect additional warnings from evaluating the expression.
      warnings.append(sink.warnings)
      print_diagnostics(
        expr_world,
        errors,
        warnings,
        command.process.diagnostic_format,
      ) catch {
        err => raise bail("failed to print diagnostics (\{err})")
      }
    }

    // The target failed, print its diagnostics.
    Err(errors) => {
      set_failed()
      print_diagnostics(
        world,
        errors,
        warnings,
        command.process.diagnostic_format,
      ) catch {
        err => raise bail("failed to print diagnostics (\{err})")
      }
    }
  }
}

///|
/// Evaluate the input expression in code mode and with no scope.
fn evaluate_expression(
  expression : String,
  sink : @library.Sink,
  world : ExpressionWorld,
  introspector : @library.Introspector,
) -> @library.Value raise @library.SourceError {
  // Map spans to ranges in the string.
  let library = @library.World::library(world)
  let engine : @library.Engine = {
    world: @library.TrackedWorld::new(world),
    library,
    introspector,
    traced: @library.Traced::new(),
    sink,
    route: @library.Route::root(),
  }
  @eval.eval_string_mapped(
    engine,
    @library.Context::new(styles=@library.StyleChain::new(library.styles)),
    expression,
    expression_id,
    Code,
    @library.Scope::new(),
  )
}

///|
/// Prints the output value of `typst eval`.
fn print_eval_result(
  value : @library.Value,
  format : EvalSerializationFormat,
  pretty : Bool,
) -> Unit raise HintedFailure {
  match format {
    Json => print_out(serialize(value, Json, pretty) + "\n")
    Yaml => print_out(serialize(value, Yaml, pretty) + "\n")
    Raw => {
      let bytes = match value {
        Str(string) => @utf8.encode(string)
        Bytes(bytes) => bytes.as_slice()
        _ =>
          raise HintedFailure(
            @library.HintedString::new(
              "cannot print \{value.ty()} in raw format",
            ).with_hint("`--format=raw` only supports strings and bytes"),
          )
      }
      write_raw_stdout(bytes) catch {
        err =>
          panic_exit(
            "failed to write eval output: \{@platform.rust_debug(err)}",
          )
      }
    }
  }
}

///|
/// Static [`FileId`] for an input expression. This allows giving accurate
/// ranges to diagnostics when evaluating the expression.
let expression_id : @syntax.FileId = @syntax.FileId::unique(
  @syntax.RootedPath::new(
    Project,
    @syntax.VirtualPath::new("") catch {
      _ => panic()
    },
  ),
)

///|
/// A world wrapper that allows us to print accurate diagnostics for an
/// input expression.
priv struct ExpressionWorld {
  world : SystemWorld
  expression : Bytes
}

///|
impl @kit.DiagnosticWorld for ExpressionWorld with fn name(self, id) {
  if id == expression_id {
    ""
  } else {
    @kit.DiagnosticWorld::name(self.world, id)
  }
}

///|
impl @library.World for ExpressionWorld with fn library(self) {
  @library.World::library(self.world)
}

///|
impl @library.World for ExpressionWorld with fn book(self) {
  @library.World::book(self.world)
}

///|
impl @library.World for ExpressionWorld with fn main(self) {
  @library.World::main(self.world)
}

///|
impl @library.World for ExpressionWorld with fn source(self, id) {
  @library.World::source(self.world, id)
}

///|
impl @library.World for ExpressionWorld with fn file(self, id) {
  if id == expression_id {
    self.expression
  } else {
    @library.World::file(self.world, id)
  }
}

///|
impl @library.World for ExpressionWorld with fn font(self, index) {
  @library.World::font(self.world, index)
}

///|
impl @library.World for ExpressionWorld with fn today(self, offset) {
  @library.World::today(self.world, offset)
}

///|
/// Writes bytes like `stdout().lock().write_all(..)` on Rust's line-buffered
/// stdout (`LineWriter` with a 1024-byte buffer): everything up to the last
/// newline is written and flushed, a shorter remainder stays buffered (it is
/// flushed at exit, ignoring errors), a remainder that does not fit into the
/// buffer is written directly.
fn write_raw_stdout(bytes : Bytes) -> Unit raise @platform.IoError {
  let mut last_newline = -1
  for i in 0.. 0 {
    @platform.write_stdout(head)
  }
  if tail.length() >= 1024 {
    @platform.write_stdout(tail)
  } else if tail.length() > 0 {
    @platform.write_stdout_buffered(tail)
  }
}