///|
async fn package_spec(
  spec : ProtonBundleSpec,
  runtime : String,
) -> @packager.PackageSpec {
  let payloads = spec.package_spec.payloads.copy()
  payloads.append(package_payloads(spec, runtime, current_bundle_host()))
  @packager.PackageSpec::new(
    spec.package_spec.executable,
    spec.package_spec.product_name,
    spec.package_spec.identifier,
    spec.package_spec.version,
    spec.package_spec.formats,
    spec.package_spec.output,
    payloads~,
    icons=spec.package_spec.icons,
    url_schemes=spec.package_spec.url_schemes,
    document_types=spec.package_spec.document_types,
    sign=spec.package_spec.sign,
    notarize=spec.package_spec.notarize,
  )
}

///|
/// Packages an already-built Proton application using the active immutable
/// runtime installation.
pub async fn build(spec : ProtonBundleSpec) -> BundleResult {
  guard path_is_file(spec.package_spec.executable) else {
    raise BundleRuntimeError::ExecutableMissing(
      path=spec.package_spec.executable,
    )
  }
  guard path_is_file(spec.cef_process) else {
    raise BundleRuntimeError::HelperMissing(path=spec.cef_process)
  }
  let installation = @cef_store.resolve_declared(spec.runtime_requirements)
  let artifacts = @packager.build_with(
    package_spec(spec, installation.runtime_root()),
    package_customization(spec),
    async fn(layout) { prepare_proton_package(spec, layout) },
  )
  BundleResult::{ artifacts, platform: installation.platform() }
}

///|
async fn package_payloads(
  spec : ProtonBundleSpec,
  runtime : String,
  host : BundleHost,
) -> Array[@packager.Payload] {
  let payloads : Array[@packager.Payload] = []
  match host {
    Macos | Linux => {
      payloads.push(
        @packager.Payload::new(
          runtime,
          "proton",
          location=@packager.PayloadLocation::Resources,
        ),
      )
      if host == Linux {
        payloads.push(
          @packager.Payload::new(
            spec.cef_process,
            "cef_process",
            location=@packager.PayloadLocation::Executables,
          ),
        )
      }
    }
    Windows => {
      require_windows_runtime_layout(runtime)
      for name in windows_runtime_binary_files() {
        payloads.push(
          @packager.Payload::new(
            resolve_path(runtime, "bin/" + name),
            name,
            location=@packager.PayloadLocation::Executables,
          ),
        )
      }
      for name in windows_runtime_root_data_files() {
        payloads.push(
          @packager.Payload::new(
            resolve_path(runtime, "bin/" + name),
            name,
            location=@packager.PayloadLocation::Executables,
          ),
        )
      }
      payloads.push(
        @packager.Payload::new(
          resolve_path(runtime, "Resources"),
          ".",
          location=@packager.PayloadLocation::Resources,
        ),
      )
      payloads.push(
        @packager.Payload::new(
          spec.cef_process,
          "cef_process.exe",
          location=@packager.PayloadLocation::Executables,
        ),
      )
    }
  }
  let location = @packager.PayloadLocation::Resources
  for payload in project_payloads(spec, location) {
    payloads.push(payload)
  }
  payloads
}

///|
async fn project_payloads(
  spec : ProtonBundleSpec,
  location : @packager.PayloadLocation,
) -> Array[@packager.Payload] {
  let inputs : Array[String] = []
  match spec.frontend_dist {
    Some(path) => inputs.push(path)
    None => ()
  }
  inputs.append(spec.resources)
  let copied : Map[String, Bool] = Map([])
  let payloads : Array[@packager.Payload] = []
  for input in inputs {
    guard copied.get(input) is None else { continue }
    copied[input] = true
    let source = match glob_tree_root(input) {
      Some(root) => root
      None if input.contains("*") =>
        raise BundlePlanError::UnsupportedResourceGlob(path=input)
      None => input
    }
    guard path_exists(source) else {
      raise BundlePlanError::MissingInput(path=input)
    }
    let relative = match relative_to_base(spec.base_dir, source) {
      Some(relative) => relative
      None => raise BundlePlanError::InputOutsideBase(path=input)
    }
    payloads.push(@packager.Payload::new(source, relative, location~))
  }
  payloads
}

///|
fn package_customization(
  spec : ProtonBundleSpec,
) -> @packager.PackageCustomization raise BundlePlanError {
  @packager.PackageCustomization::new(
    macos_plist_strings=[
      @packager.MacosPlistString::new(
        "ProtonUpdateRevision",
        spec.update_revision.to_string(),
      ),
    ],
    macos_sign_targets=macos_package_sign_targets(spec),
    windows_sign_targets=windows_package_sign_targets(spec),
    windows_verify_targets=["d3dcompiler_47.dll", "dxil.dll"],
    linux_resources_path="usr/Resources",
  )
}

///|
fn macos_package_sign_targets(
  spec : ProtonBundleSpec,
) -> Array[@packager.MacosSignTarget] raise BundlePlanError {
  let prefix = "Contents/Resources/proton"
  let framework = prefix + "/Frameworks/Chromium Embedded Framework.framework"
  let targets = [
    @packager.MacosSignTarget::new(framework + "/Libraries/libEGL.dylib"),
    @packager.MacosSignTarget::new(framework + "/Libraries/libGLESv2.dylib"),
    @packager.MacosSignTarget::new(
      framework + "/Libraries/libcef_sandbox.dylib",
    ),
    @packager.MacosSignTarget::new(
      framework + "/Libraries/libvk_swiftshader.dylib",
    ),
    @packager.MacosSignTarget::new(framework),
    @packager.MacosSignTarget::new(prefix + "/bin/libEGL.dylib"),
    @packager.MacosSignTarget::new(prefix + "/bin/libGLESv2.dylib"),
    @packager.MacosSignTarget::new(prefix + "/bin/libvk_swiftshader.dylib"),
  ]
  for binary in spec.sign_binaries {
    match relative_to_base(spec.base_dir, binary) {
      Some(relative) =>
        targets.push(
          @packager.MacosSignTarget::new(
            "Contents/Resources/" + relative,
            entitlements=true,
          ),
        )
      None => raise BundlePlanError::InputOutsideBase(path=binary)
    }
  }
  for
    helper in macos_helper_specs(
      spec.package_spec.product_name,
      spec.package_spec.identifier,
    ) {
    targets.push(
      @packager.MacosSignTarget::new(
        "Contents/Frameworks/" + helper.bundle_name + ".app",
        entitlements=true,
      ),
    )
  }
  targets
}

///|
fn windows_package_sign_targets(
  spec : ProtonBundleSpec,
) -> Array[String] raise BundlePlanError {
  let targets = ["cef_process.exe"]
  for binary in spec.sign_binaries {
    match relative_to_base(spec.base_dir, binary) {
      Some(relative) => {
        let target = "Resources/" + relative
        if !targets.contains(target) {
          targets.push(target)
        }
      }
      None => raise BundlePlanError::InputOutsideBase(path=binary)
    }
  }
  targets
}

///|
async fn prepare_proton_package(
  spec : ProtonBundleSpec,
  layout : @packager.PackageLayout,
) -> Unit {
  let host = current_bundle_host()
  let metadata_root = match host {
    Windows => layout.root()
    Macos | Linux => layout.resources()
  }
  stage_update_revision(spec, metadata_root)
  let kind = match host {
    Macos => "macos-app"
    Windows => "windows-portable"
    Linux => "linux-appimage"
  }
  write_package_manifest(spec, metadata_root, kind)
  match host {
    Macos => prepare_macos_proton_package(spec, layout)
    Windows =>
      validate_windows_portable_layout(layout.root(), layout.executable())
    Linux => {
      let staged_helper = resolve_path(layout.executables(), "cef_process")
      guard path_is_file(staged_helper) else {
        raise BundleRuntimeError::HelperMissing(path=staged_helper)
      }
      set_linux_rpath(layout.executable(), linux_package_rpath)
      set_linux_rpath(staged_helper, linux_package_rpath)
      let executable_name = host_path(layout.executable()).basename().to_owned()
      let apprun = resolve_path(layout.root(), "AppRun")
      write_text(apprun, proton_linux_apprun(executable_name))
      @async_fs.chmod(apprun, 0o755)
    }
  }
}

///|
async fn prepare_macos_proton_package(
  spec : ProtonBundleSpec,
  layout : @packager.PackageLayout,
) -> Unit {
  require_launchable_macho(layout.executable())
  for
    helper in macos_helper_specs(
      spec.package_spec.product_name,
      spec.package_spec.identifier,
    ) {
    let helper_app = resolve_path(
      layout.libraries(),
      helper.bundle_name + ".app",
    )
    let helper_macos = resolve_path(helper_app, "Contents/MacOS")
    let helper_executable = resolve_path(helper_macos, helper.executable_name)
    ensure_dir(helper_macos)
    copy_file(spec.cef_process, helper_executable)
    @async_fs.chmod(helper_executable, 0o755)
    require_launchable_macho(helper_executable)
    write_text(
      resolve_path(helper_app, "Contents/Info.plist"),
      helper_info_plist(spec, helper),
    )
  }
}