///|
pub enum TitleBarStyle {
  Default = 0
  Hidden = 1
} derive(Debug, Eq, ToJson, FromJson, Hash)

///|
pub struct MemoryFile {
  path : String
  content : Bytes
} derive(Debug, Eq, ToJson, FromJson, Hash)

///|
pub struct MemorySource {
  entry : String
  files : Array[MemoryFile]
} derive(Debug, Eq, ToJson, FromJson, Hash)

///|
pub(all) enum Source {
  Url(String)
  Local(String)
  Memory(MemorySource)
} derive(Debug, Eq, ToJson, FromJson, Hash)

///|
pub struct WindowConfig {
  label : String
  title : String
  width : Int
  height : Int
  debug : Int
  devtools : Bool
  frameless : Bool
  resizable : Bool
  always_on_top : Bool
  transparent : Bool
  title_bar_style : TitleBarStyle
  title_bar_overlay : Bool
  traffic_light_position : (Int, Int)
  position : (Int, Int)
  hidden : Bool
  focused : Bool
  source : Source?
} derive(Debug, Eq, ToJson, FromJson, Hash)

///|
priv struct ResolvedSource {
  url : String
  custom_protocols : Array[(String, String)]
}

///|
pub fn MemoryFile::from_bytes(path : String, content : Bytes) -> MemoryFile {
  { path, content, }
}

///|
pub fn MemoryFile::from_string(path : String, content : String) -> MemoryFile {
  let content = @utf8.encode(content)
  { path, content, }
}

///|
#alias(new, deprecated="Use `MemorySource()` instead")
pub fn MemorySource::MemorySource(
  files : Array[MemoryFile],
  entry? : String = "index.html",
) -> MemorySource {
  { entry, files, }
}

///|
pub fn Source::html(html : String) -> Source {
  Memory(MemorySource([MemoryFile::from_string("index.html", html)]))
}

///|
pub fn Source::zip(zip : Bytes) -> Source raise {
  let archive = @zipc.Archive::from_bytes(zip)
  let files = []
  archive.members.each((name, m) => {
    if m.kind is File(file) {
      files.push(MemoryFile::from_bytes(name, file.to_bytes()))
    }
  })
  Memory(MemorySource(files))
}

///|
pub fn WindowConfig::new(
  label? : String = "",
  title? : String = "Lepus App",
  width? : Int = 800,
  height? : Int = 600,
  debug? : Int = 0,
  devtools? : Bool = false,
  frameless? : Bool = false,
  resizable? : Bool = true,
  always_on_top? : Bool = false,
  transparent? : Bool = false,
  title_bar_style? : TitleBarStyle = Default,
  title_bar_overlay? : Bool = false,
  traffic_light_position? : (Int, Int) = (-1, -1),
  position? : (Int, Int) = (-1, -1),
  hidden? : Bool = false,
  focused? : Bool = true,
  source? : Source,
) -> WindowConfig {
  {
    label,
    title,
    width,
    height,
    debug,
    devtools,
    frameless,
    resizable,
    always_on_top,
    transparent,
    title_bar_style,
    title_bar_overlay,
    traffic_light_position,
    position,
    hidden,
    focused,
    source,
  }
}

///|
/// Returns the window label used for stable cross-window addressing.
/// Falls back to `title` when no explicit label was given.
pub fn WindowConfig::label(self : WindowConfig) -> String {
  if self.label.is_empty() {
    self.title
  } else {
    self.label
  }
}

///|
pub fn WindowConfig::set_source(
  self : WindowConfig,
  source : Source,
) -> WindowConfig {
  { ..self, source: Some(source), }
}

///|
pub fn WindowConfig::navigate(
  self : WindowConfig,
  url : String,
) -> WindowConfig {
  self.set_source(Url(url))
}

///|
pub struct App {
  windows : Array[WindowConfig]
  plugins : Array[@webview.Plugin]
}

///|
pub struct Window {
  config : WindowConfig
  plugins : Array[@webview.Plugin]
}

///|
#alias(new, deprecated="Use `Window()` instead")
pub fn Window::Window(
  label? : String = "",
  title? : String = "Lepus App",
  width? : Int = 800,
  height? : Int = 600,
  debug? : Int = 0,
  devtools? : Bool = false,
  frameless? : Bool = false,
  resizable? : Bool = true,
  always_on_top? : Bool = false,
  transparent? : Bool = false,
  title_bar_style? : TitleBarStyle = Default,
  title_bar_overlay? : Bool = false,
  traffic_light_position? : (Int, Int) = (-1, -1),
  position? : (Int, Int) = (-1, -1),
  hidden? : Bool = false,
  focused? : Bool = true,
  source? : Source,
  plugins? : Array[@webview.Plugin] = [],
) -> Window {
  {
    config: {
      label,
      title,
      width,
      height,
      debug,
      devtools,
      frameless,
      resizable,
      always_on_top,
      transparent,
      title_bar_style,
      title_bar_overlay,
      traffic_light_position,
      position,
      hidden,
      focused,
      source,
    },
    plugins,
  }
}

///|
pub fn Window::set_source(self : Window, source : Source) -> Window {
  { ..self, config: self.config.set_source(source), }
}

///|
pub fn Window::navigate(self : Window, url : String) -> Window {
  self.set_source(Url(url))
}

///|
pub async fn Window::run(self : Window) -> Unit {
  App::new([self.config], plugins=self.plugins).run()
}

///|
pub fn App::new(
  windows : Array[WindowConfig],
  plugins? : Array[@webview.Plugin] = [],
) -> App {
  { windows, plugins, }
}

///|
pub fn App::with_windows(
  windows : Array[WindowConfig],
  plugins? : Array[@webview.Plugin] = [],
) -> App {
  guard windows.length() > 0 else {
    abort("App::with_windows requires at least one window")
  }
  { windows, plugins, }
}

///|
pub fn App::add_window(self : App, config : WindowConfig) -> Unit {
  self.windows.push(config)
}

///|
pub fn App::window_count(self : App) -> Int {
  self.windows.length()
}

///|
pub async fn App::run(self : App, window_index? : Int) -> Unit {
  let selected = match window_index {
    Some(i) => {
      guard i >= 0 && i < self.windows.length() else {
        abort("App::run window_index out of range")
      }
      [self.windows[i]]
    }
    None => self.windows
  }
  guard selected.length() > 0 else {
    abort("App::run requires at least one window")
  }
  assert_unique_labels(selected)
  match @webview.detect_child_window() {
    Some((base, label)) => {
      // 子进程:事件插件的 parent handler 不会被使用(此进程只安装
      // child-side proxy),故传空映射与子进程 WM 即可。
      let bus = EventBus::new(Map([]))
      let wm = @webview.WindowManager::init(is_main=false)
      let all_plugins = self.plugins + [bus.plugin(wm)]
      let config = find_window_config(self.windows, label)
      let window = build_runtime_window(config, all_plugins)
      window.run_child(base)
    }
    None => {
      // 主进程:建立 label → window_id 映射(window_id == spawn 时的 base),
      // 并为主进程的命令路由装配事件插件。
      let ids = assign_window_ids(selected)
      let bus = EventBus::new(ids)
      let wm = @webview.WindowManager::init(is_main=true)
      let all_plugins = self.plugins + [bus.plugin(wm)]
      let labels : Array[String] = []
      for config in selected {
        labels.push(config.label())
      }
      let router = @webview.build_router(all_plugins)
      @webview.Window::run_many(labels, router)
    }
  }
}

///|
/// Runs all windows in a **single process**.
///
/// Unlike `App::run` (which spawns one child process per window), this mode
/// creates every window inside the current process. All windows share one
/// `NSApplication` / Dock icon (macOS), one menu bar, and one event loop —
/// matching the Tauri/Electron model. Cross-window events are delivered by
/// direct `eval` (no IPC), so this is the recommended mode for most apps.
///
/// The event loop runs once (a single `webview_run` services all windows);
/// it returns when every window has closed.
pub async fn App::run_single_process(self : App) -> Unit {
  guard self.windows.length() > 0 else {
    abort("App::run_single_process requires at least one window")
  }
  assert_unique_labels(self.windows)
  let wm = @webview.WindowManager::init(is_main=true)
  // macOS:单进程 = 单个 NSApplication = 单个 Dock 图标,窗口天然可聚焦。
  // Regular 策略由 vendor 默认设置;安装标准主菜单启用快捷键。
  ignore(wm.install_default_app_menu())
  let bus = LocalEventBus::new()
  @async.with_task_group(task_group => {
    let first_id = build_single_process_windows(
      task_group,
      self.windows,
      self.plugins,
      bus,
    )
    guard first_id > 0 else {
      abort("App::run_single_process: no window created")
    }
    // 单次 webview_run 服务全部窗口(共享 NSApp run loop)。
    // window_ref_count 归零时 vendor 自动 terminate。
    ignore(wm.run_window(first_id))
  })
}

///|
/// 在单进程内为每个 WindowConfig 创建 WebView、安装插件(直连,无 IPC)、
/// 注册到本地事件总线,并返回首个窗口的 id(供单次 run loop 使用)。
async fn build_single_process_windows(
  task_group : @async.TaskGroup[Unit],
  configs : Array[WindowConfig],
  extra_plugins : Array[@webview.Plugin],
  bus : LocalEventBus,
) -> Int {
  let mut first_id = 0
  for config in configs {
    let webview = @webview.WebView::new_managed(
      task_group,
      title=config.title,
      url="",
      width=config.width,
      height=config.height,
      debug=config.debug,
      devtools=config.devtools,
      frameless=config.frameless,
      resizable=config.resizable,
      closeable=true,
      always_on_top=config.always_on_top,
      transparent=config.transparent,
      title_bar_style=match config.title_bar_style {
        Default => Default
        Hidden => Hidden
      },
      title_bar_overlay=config.title_bar_overlay,
      traffic_light_position=config.traffic_light_position,
      position=config.position,
      hidden=config.hidden,
      focused=config.focused,
    )
    let wid = webview.window_id()
    if first_id == 0 {
      first_id = wid
    }
    // 自定义协议
    guard config.source is Some(source) else {
      abort("WindowConfig::source is required")
    }
    let resolved = resolve_source(source)
    resolved.custom_protocols.each(mapping => {
      let (scheme, root_dir) = mapping
      webview.set_custom_protocol(scheme, root_dir)
    })
    // 平台标题栏支持
    if config.frameless ||
      config.title_bar_style == Hidden ||
      config.title_bar_overlay {
      webview.enable_custom_titlebar_support()
    }
    if config.transparent {
      webview.enable_transparent_background_support()
    }
    if config.traffic_light_position.0 >= 0 &&
      config.traffic_light_position.1 >= 0 {
      webview.set_traffic_light_position(
        config.traffic_light_position.0,
        config.traffic_light_position.1,
      )
    }
    // 插件:window_controls + 用户插件 + 事件总线(全部直连,proxy=None)
    let host = @webview.PluginHost::PluginHost(webview)
    host.install(@webview.window_controls_plugin(wid))
    for plugin in extra_plugins {
      host.install(plugin)
    }
    bus.register(config.label(), webview)
    host.install(bus.plugin())
    // 内容
    if resolved.url != "" {
      webview.navigate(resolved.url)
    }
  }
  first_id
}

///|
/// Assigns a globally-unique window id to each label, matching the `base`
/// values the child processes use when creating their windows.
fn assign_window_ids(windows : Array[WindowConfig]) -> Map[String, Int] {
  let ids = Map([])
  let mut base = 1
  for config in windows {
    ids.set(config.label(), base)
    base = base + 1
  }
  ids
}

///|
/// Returns the window whose `label()` matches `label`, aborting when absent.
fn find_window_config(
  windows : Array[WindowConfig],
  label : String,
) -> WindowConfig {
  for config in windows {
    if config.label() == label {
      return config
    }
  }
  abort("App: no window with label " + label)
}

///|
/// Asserts that every window label is unique so cross-window addressing and
/// child-process dispatch are unambiguous.
fn assert_unique_labels(windows : Array[WindowConfig]) -> Unit {
  let seen : Array[String] = []
  for config in windows {
    let label = config.label()
    guard !seen.contains(label) else {
      abort("App: duplicate window label " + label)
    }
    seen.push(label)
  }
}

///|
/// Builds a runtime `@webview.Window` from a `WindowConfig`, resolving its
/// source, registering custom protocols, and installing the App-level plugins.
async fn build_runtime_window(
  config : WindowConfig,
  plugins : Array[@webview.Plugin],
) -> @webview.Window {
  let window = @webview.Window(
    label=config.label(),
    title=config.title,
    width=config.width,
    height=config.height,
    debug=config.debug,
    devtools=config.devtools,
    frameless=config.frameless,
    resizable=config.resizable,
    always_on_top=config.always_on_top,
    transparent=config.transparent,
    title_bar_style=match config.title_bar_style {
      Default => Default
      Hidden => Hidden
    },
    title_bar_overlay=config.title_bar_overlay,
    traffic_light_position=config.traffic_light_position,
    position=config.position,
    hidden=config.hidden,
    focused=config.focused,
  )
  guard config.source is Some(source) else {
    abort("WindowConfig::source is required")
  }
  let resolved = resolve_source(source)
  resolved.custom_protocols.each(mapping => {
    let (scheme, root_dir) = mapping
    window.set_custom_protocol(scheme, root_dir)
  })
  window.navigate(resolved.url)
  plugins.each(plugin => window.install(plugin))
  window
}

///|
async fn resolve_source(source : Source) -> ResolvedSource {
  match source {
    Url(url) => { url, custom_protocols: [], }
    Local(path) => resolve_local_fs(path)
    Memory(source) => resolve_memory_source(source)
  }
}

///|
async fn resolve_local_fs(path : String) -> ResolvedSource {
  let fs_path = if path.has_prefix("file://") {
    file_url_to_path(path)
  } else {
    path
  }
  let normalized = fs_path.replace_all(old="\\", new="/")
  let entry_path = if is_html_path(normalized) {
    normalized
  } else {
    join_path(normalized, "index.html")
  }
  guard @fs.exists(entry_path) else {
    abort("LocalFs source does not exist: " + entry_path)
  }
  guard @fs.kind(entry_path) == Regular else {
    abort("LocalFs source is not a regular file: " + entry_path)
  }
  guard @fs.can_read(entry_path) else {
    abort("LocalFs source is not readable: " + entry_path)
  }
  { url: to_file_url(@fs.realpath(entry_path)), custom_protocols: [], }
}

///|
async fn resolve_memory_source(source : MemorySource) -> ResolvedSource {
  guard source.files.length() > 0 else {
    abort("MemoryFs source requires at least one file")
  }
  let entry = sanitize_memory_relative_path(source.entry)
  let root = @fs.realpath(@fs.tmpdir(prefix="lepus-source"))
  let mut has_entry = false
  for file in source.files {
    let relative_path = sanitize_memory_relative_path(file.path)
    if relative_path == entry {
      has_entry = true
    }
    let file_path = join_path(root, relative_path)
    match parent_dir(file_path) {
      Some(parent) =>
        if !@fs.exists(parent) {
          @fs.mkdir(parent, permission=0o700, recursive=true)
        }
      None => ()
    }
    @fs.write_file(
      file_path,
      file.content,
      create_mode=CreateOrTruncate,
      permission=0o644,
    )
  }
  guard has_entry else { abort("MemoryFs entry file not found: " + entry) }
  {
    url: memory_source_entry_url(entry),
    custom_protocols: [(memory_source_mapping_name(), root)],
  }
}

///|
#cfg(platform="windows")
fn memory_source_mapping_name() -> String {
  "lepus"
}

///|
#cfg(not(platform="windows"))
fn memory_source_mapping_name() -> String {
  "lepus"
}

///|
#cfg(platform="windows")
fn memory_source_entry_url(entry : String) -> String {
  "http://lepus/" + entry
}

///|
#cfg(not(platform="windows"))
fn memory_source_entry_url(entry : String) -> String {
  "lepus://localhost/" + entry
}

///|
fn to_file_url(path : String) -> String {
  let normalized = path.replace_all(old="\\", new="/")
  if normalized.has_prefix("/") {
    "file://" + normalized
  } else {
    "file:///" + normalized
  }
}

///|
fn join_path(base : String, leaf : String) -> String {
  let base = base.trim_end(chars="/\\").to_owned()
  let leaf = leaf.trim_start(chars="/\\").to_owned()
  if base.is_empty() {
    leaf
  } else if leaf.is_empty() {
    base
  } else {
    base + "/" + leaf
  }
}

///|
fn is_html_path(path : String) -> Bool {
  path.has_suffix(".html") ||
  path.has_suffix(".htm") ||
  path.has_suffix(".xhtml")
}

///|
fn file_url_to_path(url : String) -> String {
  guard url.has_prefix("file://") else { url }
  let path = url[7:].to_owned()
  if path.is_empty() {
    "/"
  } else {
    path
  }
}

///|
fn sanitize_memory_relative_path(path : String) -> String {
  let normalized = path.replace_all(old="\\", new="/")
  let trimmed = normalized.trim_start(chars="/").trim_end(chars="/").to_owned()
  guard !trimmed.is_empty() else { abort("MemoryFs path cannot be empty") }
  guard trimmed != ".." &&
    !trimmed.has_prefix("../") &&
    !trimmed.has_suffix("/..") &&
    !trimmed.contains("/../") &&
    trimmed != "." &&
    !trimmed.has_prefix("./") &&
    !trimmed.has_suffix("/.") &&
    !trimmed.contains("/./") else {
    abort("MemoryFs path cannot use '.' or '..': " + path)
  }
  trimmed
}

///|
fn parent_dir(path : String) -> String? {
  let path = path.trim_end(chars="/\\").to_owned()
  guard !path.is_empty() else { None }
  match path.rev_find("/") {
    Some(index) => {
      let parent = path[:index].trim_end(chars="/\\").to_owned()
      if parent.is_empty() {
        None
      } else {
        Some(parent)
      }
    }
    None =>
      match path.rev_find("\\") {
        Some(index) => {
          let parent = path[:index].trim_end(chars="/\\").to_owned()
          if parent.is_empty() {
            None
          } else {
            Some(parent)
          }
        }
        None => None
      }
  }
}