// The world of the CLI (upstream `typst-cli/src/world.rs`).

///|
/// A world that provides access to the operating system.
priv struct SystemWorld {
  /// The working directory.
  workdir : String?
  /// Typst's standard library.
  library : @library.Library
  /// Metadata about discovered fonts and lazily loaded fonts.
  fonts : @kit.FontStore
  /// Maps file ids to source files and buffers.
  files : @kit.FileStore[SystemFiles]
  /// The current datetime if requested. This is stored here to ensure it is
  /// always the same within one compilation.
  /// Reset between compilations if not [`Time::Fixed`].
  now : @kit.Time
}

///|
/// An error that occurs during world construction.
priv suberror WorldCreationError {
  /// The input file does not appear to exist.
  InputNotFound(String)
  /// The input file path was malformed.
  InputMalformed(@syntax.VirtualizeError)
  /// The root directory does not appear to exist.
  RootNotFound(String)
  /// The requested creation timestamp was invalid.
  InvalidTimestamp
  /// Another type of I/O error.
  Io(@platform.IoError)
}

///|
impl Show for WorldCreationError with fn output(self, logger) {
  match self {
    InputMalformed(err) =>
      match err {
        Path(Escapes) =>
          logger.write_string("source file must be contained in project root")
        Path(Backslash) =>
          logger.write_string("source path must not contain a backslash")
        Invalid(s) =>
          logger.write_string(
            "source path contains invalid sequence `\{@library.repr_str(s)}`",
          )
        Utf8 => logger.write_string("source path must be valid UTF-8")
      }
    InputNotFound(path) =>
      logger.write_string("input file not found (searched at \{path})")
    RootNotFound(path) =>
      logger.write_string("root directory not found (searched at \{path})")
    InvalidTimestamp => logger.write_string("creation timestamp out of range")
    Io(err) => logger.write_string(err.to_string())
  }
}

///|
/// Creates a new system world.
async fn SystemWorld::new(
  input : Input?,
  world_args : WorldArgs,
  process_args : ProcessArgs,
) -> SystemWorld raise WorldCreationError {
  let library = {
    // Convert the input pairs to a dictionary.
    let inputs = @library.Dict::new()
    for pair in world_args.inputs {
      inputs.insert(pair.0, Str(pair.1))
    }
    let features = @library.Features(process_args.features)
    @typst.library_builder([
      @library.html_format(),
      @library.pdf_format(),
      @library.svg_format(),
      @library.png_format(),
      @library.bundle_format(),
    ])
    .with_inputs(inputs)
    .with_features(features)
    .build()
  }
  let now = match world_args.creation_timestamp {
    Some(time) =>
      @kit.Time::fixed_timestamp(time) catch {
        _ => raise InvalidTimestamp
      }
    None => @kit.Time::system()
  }
  let files = @kit.FileStore::new(SystemFiles::new(input, world_args))
  let fonts = discover_fonts(world_args.font) catch {
    err => raise Io(@platform.IoError(Other, err.to_string()))
  }
  { workdir: @platform.current_dir(), library, fonts, files, now, }
}

///|
/// The project root relative to which absolute paths are resolved.
fn SystemWorld::root(self : SystemWorld) -> String {
  self.files.loader().project.path()
}

///|
/// The current working directory.
fn SystemWorld::workdir(self : SystemWorld) -> String {
  self.workdir.unwrap_or(".")
}

///|
/// Return all paths the last compilation depended on.
fn SystemWorld::dependencies(self : SystemWorld) -> Array[String] {
  let loader = self.files.loader()
  let out = []
  for id in self.files.dependencies() {
    try loader.resolve(id) catch {
      _ => ()
    } noraise {
      path => out.push(path)
    }
  }
  out
}

///|
/// Reset the compilation state in preparation of a new compilation.
fn SystemWorld::reset(self : SystemWorld) -> Unit {
  self.files.reset()
  self.now.reset()
}

///|
impl @library.World for SystemWorld with fn library(self) {
  self.library
}

///|
impl @library.World for SystemWorld with fn book(self) {
  self.fonts.book()
}

///|
impl @library.World for SystemWorld with fn main(self) {
  self.files.loader().main
}

///|
impl @library.World for SystemWorld with fn source(self, id) {
  self.files.source(id)
}

///|
impl @library.World for SystemWorld with fn file(self, id) {
  self.files.file(id)
}

///|
impl @library.World for SystemWorld with fn font(self, index) {
  self.fonts.font(index)
}

///|
impl @library.World for SystemWorld with fn today(self, offset) {
  self.now.today(offset)
}

///|
impl @kit.DiagnosticWorld for SystemWorld with fn name(self, id) {
  let vpath = id.vpath()
  match id.root() {
    Project =>
      // Try to express the path relative to the working directory.
      try vpath.realize(self.root()) catch {
        _ => vpath.get_without_slash()
      } noraise {
        rooted =>
          match diff_paths(rooted, self.workdir()) {
            Some(path) => path
            None => vpath.get_without_slash()
          }
      }
    Package(spec) => "\{spec}\{vpath.get_with_slash()}"
  }
}

///|
/// Static `FileId` allocated for stdin. This is to ensure that stdin can
/// live in the project root without colliding with any real on-disk file.
let stdin_id : @syntax.FileId = @syntax.FileId::unique(
  @syntax.RootedPath::new(
    Project,
    @syntax.VirtualPath::new("") catch {
      _ => panic()
    },
  ),
)

///|
/// Static `FileId` allocated for empty/no input at all. This is to ensure
/// that we can create a [`SystemWorld`] based on no main file or stdin at
/// all.
let empty_id : @syntax.FileId = @syntax.FileId::unique(
  @syntax.RootedPath::new(
    Project,
    @syntax.VirtualPath::new("") catch {
      _ => panic()
    },
  ),
)

///|
/// Provides project files from a configured directory and package files
/// from standard locations.
priv struct SystemFiles {
  main : @syntax.FileId
  project : @kit.FsRoot
  packages : @kit.SystemPackages
}

///|
/// Creates a new loader given the configuration.
fn SystemFiles::new(
  input : Input?,
  world_args : WorldArgs,
) -> SystemFiles raise WorldCreationError {
  // Resolve the system-global input path.
  let input_path = match input {
    Some(Path(path)) =>
      Some(
        @platform.canonicalize(path) catch {
          err =>
            raise if err.kind() == NotFound {
              InputNotFound(path)
            } else {
              Io(err)
            }
        },
      )
    _ => None
  }

  // Resolve the system-global root directory.
  let root = {
    let path = match world_args.root {
      Some(root) => root
      None =>
        match input_path.bind(@platform.parent) {
          Some(parent) => parent
          None => "."
        }
    }
    @platform.canonicalize(path) catch {
      err =>
        raise if err.kind() == NotFound { RootNotFound(path) } else { Io(err) }
    }
  }
  let main = match input_path {
    Some(path) => {
      // Resolve the virtual path of the main file within the project root.
      let vpath = @syntax.VirtualPath::virtualize(root, path) catch {
        err => raise InputMalformed(err)
      }
      @syntax.RootedPath::new(Project, vpath).intern()
    }
    None =>
      if input is Some(Stdin) {
        // Return the special id of STDIN.
        stdin_id
      } else {
        // Return the special id of EMPTY/no input at all otherwise.
        empty_id
      }
  }
  {
    main,
    project: @kit.FsRoot::new(root),
    packages: system_packages(world_args.package_args),
  }
}

///|
/// Resolves the file system path for the given `id`.
fn SystemFiles::resolve(
  self : SystemFiles,
  id : @syntax.FileId,
) -> String raise @library.FileError {
  self.root(id).resolve(id.vpath())
}

///|
/// Resolves the root in which the given file ID resides.
fn SystemFiles::root(
  self : SystemFiles,
  id : @syntax.FileId,
) -> @kit.FsRoot raise @library.FileError {
  match id.root() {
    Project => self.project
    Package(spec) =>
      self.packages.obtain(spec) catch {
        err => raise @library.FileError::Package(err)
      }
  }
}

///|
impl @kit.FileLoader for SystemFiles with fn load(self, id) {
  if id == empty_id {
    b""
  } else if id == stdin_id {
    read_from_stdin()
  } else {
    self.root(id).load(id.vpath())
  }
}

///|
/// Read from stdin.
fn read_from_stdin() -> Bytes raise @library.FileError {
  @platform.read_stdin() catch {
    err => raise @kit.file_error_from_io(err, "")
  }
}

///|
/// Returns a new package storage for the given args
/// (`typst-cli/src/packages.rs`).
fn system_packages(args : PackageArgs) -> @kit.SystemPackages {
  @kit.SystemPackages::from_parts(
    match args.package_path {
      Some(path) => Some(@kit.FsPackages::new(path))
      None => @kit.FsPackages::system_data()
    },
    match args.package_cache_path {
      Some(path) => Some(@kit.FsPackages::new(path))
      None => @kit.FsPackages::system_cache()
    },
  )
}

///|
/// Discovers the fonts as specified by the CLI flags
/// (`typst-cli/src/fonts.rs`).
async fn discover_fonts(args : FontArgs) -> @kit.FontStore {
  let fonts = @kit.FontStore::new()
  if !args.ignore_system_fonts {
    fonts.extend_all(@kit.system())
  }
  if !args.ignore_embedded_fonts {
    fonts.extend_all(@kit.embedded())
  }
  for path in args.font_paths {
    if path is Some(path) {
      fonts.extend_all(@kit.scan(path))
    }
  }
  fonts
}

///|
/// Constructs a relative path from `base` to `path` (`pathdiff::diff_paths`).
fn diff_paths(path : String, base : String) -> String? {
  if @platform.is_absolute(path) != @platform.is_absolute(base) {
    return if @platform.is_absolute(path) { Some(path) } else { None }
  }
  let ita = components(path)
  let itb = components(base)
  let comps : Array[String] = []
  let mut a = 0
  let mut b = 0
  while true {
    let ca = if a < ita.length() { Some(ita[a]) } else { None }
    let cb = if b < itb.length() { Some(itb[b]) } else { None }
    a += 1
    b += 1
    match (ca, cb) {
      (None, None) => break
      (Some(x), None) => {
        comps.push(x)
        for i in a.. comps.push("..")
      (Some(x), Some(y)) if comps.is_empty() && x == y => ()
      (Some(x), Some(".")) => comps.push(x)
      (Some(_), Some("..")) => return None
      (Some(x), Some(_)) => {
        comps.push("..")
        for _ in b.. Array[String] {
  let out = []
  if path.has_prefix("/") {
    out.push("/")
  }
  for i, part in path.split("/").to_array() {
    let part = part.to_owned()
    if part == "" || (part == "." && i > 0) {
      continue
    }
    out.push(part)
  }
  out
}