///|
pub struct WechatBuildBudget {
  max_pages : Int
  max_files : Int
  max_total_bytes : Int
  max_wxml_bytes : Int
  max_wxss_bytes : Int
  max_js_bytes : Int
  max_initial_data_bytes : Int
  max_event_patch_bytes : Int
  max_event_patch_operations : Int
  max_page_first_screen_bytes : Int
  max_page_update_payload_bytes : Int
  max_page_update_operations : Int
} derive(Debug, Eq)

///|
pub struct WechatBuildReport {
  page_count : Int
  file_count : Int
  total_bytes : Int
  wxml_bytes : Int
  wxss_bytes : Int
  js_bytes : Int
  initial_data_bytes : Int
  event_patch_bytes : Int
  event_patch_operation_count : Int
  missing_event_patch_count : Int
  orphan_event_patch_count : Int
  max_page_first_screen_bytes : Int
  max_page_update_payload_bytes : Int
  max_page_update_operation_count : Int
  recommended_main_package_bytes : Int
  package_size_advisory : String
  diagnostics : Array[String]
  summary : String
} derive(Debug, Eq)

///|
pub fn default_build_budget() -> WechatBuildBudget {
  {
    max_pages: 32,
    max_files: 16,
    max_total_bytes: 2 * 1024 * 1024,
    max_wxml_bytes: 65536,
    max_wxss_bytes: 65536,
    max_js_bytes: 65536,
    max_initial_data_bytes: 65536,
    max_event_patch_bytes: 8192,
    max_event_patch_operations: 64,
    max_page_first_screen_bytes: 98304,
    max_page_update_payload_bytes: 8192,
    max_page_update_operations: 16,
  }
}

///|
pub fn build_budget(
  max_pages~ : Int,
  max_files~ : Int,
  max_total_bytes~ : Int,
  max_wxml_bytes~ : Int,
  max_wxss_bytes~ : Int,
  max_js_bytes~ : Int,
  max_initial_data_bytes~ : Int,
  max_event_patch_bytes~ : Int,
  max_event_patch_operations~ : Int,
  max_page_first_screen_bytes~ : Int,
  max_page_update_payload_bytes~ : Int,
  max_page_update_operations~ : Int,
) -> WechatBuildBudget {
  {
    max_pages,
    max_files,
    max_total_bytes,
    max_wxml_bytes,
    max_wxss_bytes,
    max_js_bytes,
    max_initial_data_bytes,
    max_event_patch_bytes,
    max_event_patch_operations,
    max_page_first_screen_bytes,
    max_page_update_payload_bytes,
    max_page_update_operations,
  }
}

///|
pub fn report_project(
  project : WechatProject,
  _runtime : WechatRuntime,
) -> WechatBuildReport {
  report_project_pages(project.files, project.pages, default_build_budget())
}

///|
pub fn report_project_with_budget(
  project : WechatProject,
  _runtime : WechatRuntime,
  budget : WechatBuildBudget,
) -> WechatBuildReport {
  report_project_pages(project.files, project.pages, budget)
}

///|
pub fn report_project_pages(
  files : Array[WechatFile],
  pages : Array[WechatProjectPage],
  budget : WechatBuildBudget,
) -> WechatBuildReport {
  report_files(files, pages, budget)
}

///|
fn report_files(
  files : Array[WechatFile],
  pages : Array[WechatProjectPage],
  budget : WechatBuildBudget,
) -> WechatBuildReport {
  let diagnostics : Array[String] = []
  let mut total = 0
  let mut wxml = 0
  let mut wxss = 0
  let mut js = 0
  for file in files {
    let bytes = materialized_file_bytes(file, pages)
    total += bytes
    if file.path.has_suffix(".wxml") {
      wxml += bytes
    } else if file.path.has_suffix(".wxss") {
      wxss += bytes
    } else if file.path.has_suffix(".js") {
      js += bytes
    }
  }
  let initial = initial_data_bytes(pages)
  let event_patch = event_patch_bytes(pages)
  let event_patch_operations = event_patch_operation_count(pages)
  let recommended_main_package_bytes = recommended_main_package_bytes(budget)
  let package_size_advisory = package_size_advisory(
    total,
    budget.max_total_bytes,
    recommended_main_package_bytes,
  )
  let (max_first_screen, max_update_payload, max_update_operations) = push_page_performance_diagnostics(
    diagnostics, files, pages, budget,
  )
  push_page_manifest_diagnostics(diagnostics, pages)
  let (missing_event_patch_count, orphan_event_patch_count) = count_runtime_event_diagnostics(
    pages,
  )
  push_over_budget(
    diagnostics,
    "wechat-page-count-over-budget",
    pages.length(),
    budget.max_pages,
  )
  push_over_budget(
    diagnostics,
    "wechat-file-count-over-budget",
    files.length(),
    budget.max_files,
  )
  push_over_budget(
    diagnostics,
    "wechat-total-bytes-over-budget",
    total,
    budget.max_total_bytes,
  )
  push_over_budget(
    diagnostics,
    "wechat-wxml-bytes-over-budget",
    wxml,
    budget.max_wxml_bytes,
  )
  push_over_budget(
    diagnostics,
    "wechat-wxss-bytes-over-budget",
    wxss,
    budget.max_wxss_bytes,
  )
  push_over_budget(
    diagnostics,
    "wechat-js-bytes-over-budget",
    js,
    budget.max_js_bytes,
  )
  push_over_budget(
    diagnostics,
    "wechat-initial-data-bytes-over-budget",
    initial,
    budget.max_initial_data_bytes,
  )
  push_over_budget(
    diagnostics,
    "wechat-event-patch-bytes-over-budget",
    event_patch,
    budget.max_event_patch_bytes,
  )
  push_over_budget(
    diagnostics,
    "wechat-event-patch-operations-over-budget",
    event_patch_operations,
    budget.max_event_patch_operations,
  )
  {
    page_count: pages.length(),
    file_count: files.length(),
    total_bytes: total,
    wxml_bytes: wxml,
    wxss_bytes: wxss,
    js_bytes: js,
    initial_data_bytes: initial,
    event_patch_bytes: event_patch,
    event_patch_operation_count: event_patch_operations,
    missing_event_patch_count,
    orphan_event_patch_count,
    max_page_first_screen_bytes: max_first_screen,
    max_page_update_payload_bytes: max_update_payload,
    max_page_update_operation_count: max_update_operations,
    recommended_main_package_bytes,
    package_size_advisory,
    diagnostics,
    summary: "Bunnia WeChat build report: pages=\{pages.length()} files=\{files.length()} total=\{total} hard_package_limit=\{budget.max_total_bytes} recommended_package_limit=\{recommended_main_package_bytes} package_size=\{package_size_advisory} wxml=\{wxml} wxss=\{wxss} js=\{js} initial_data=\{initial} event_patches=\{event_patch} event_patch_ops=\{event_patch_operations} missing_event_patches=\{missing_event_patch_count} orphan_event_patches=\{orphan_event_patch_count} max_first_screen=\{max_first_screen} max_update_payload=\{max_update_payload} max_update_ops=\{max_update_operations} diagnostics=\{diagnostics.length()}",
  }
}

///|
fn recommended_main_package_bytes(budget : WechatBuildBudget) -> Int {
  let wechat_recommended = 1536 * 1024
  if budget.max_total_bytes < wechat_recommended {
    budget.max_total_bytes
  } else {
    wechat_recommended
  }
}

///|
fn package_size_advisory(
  total : Int,
  hard_limit : Int,
  recommended_limit : Int,
) -> String {
  if total > hard_limit {
    "over-hard-limit"
  } else if total > recommended_limit {
    "split-recommended"
  } else {
    "within-headroom"
  }
}

///|
fn push_page_manifest_diagnostics(
  diagnostics : Array[String],
  pages : Array[WechatProjectPage],
) -> Unit {
  for page in pages {
    for item in page_manifest_diagnostics(page) {
      diagnostics.push(item)
    }
  }
}

///|
fn count_runtime_event_diagnostics(
  pages : Array[WechatProjectPage],
) -> (Int, Int) {
  let mut missing_count = 0
  let mut orphan_count = 0
  for page in pages {
    for message in page.page.plan.event_messages {
      if !runtime_has_patch(page.runtime, message) {
        missing_count += 1
      }
    }
    for patch in page.runtime.event_patches {
      if !plan_has_event(page.page.plan, patch.message) {
        orphan_count += 1
      }
    }
  }
  (missing_count, orphan_count)
}

///|
fn event_patch_operation_count(pages : Array[WechatProjectPage]) -> Int {
  let mut total = 0
  for page in pages {
    total += report_page_update_operation_count(page)
  }
  total
}

///|
fn runtime_has_patch(runtime : WechatRuntime, message : String) -> Bool {
  for item in runtime.event_patches {
    if item.message == message {
      return true
    }
  }
  false
}

///|
fn plan_has_event(plan : @core.RenderPlan, message : String) -> Bool {
  for item in plan.event_messages {
    if item == message {
      return true
    }
  }
  false
}

///|
fn push_page_performance_diagnostics(
  diagnostics : Array[String],
  files : Array[WechatFile],
  pages : Array[WechatProjectPage],
  budget : WechatBuildBudget,
) -> (Int, Int, Int) {
  let mut max_first_screen = 0
  let mut max_update_payload = 0
  let mut max_update_operations = 0
  for page in pages {
    let first_screen = page_first_screen_bytes(page, files)
    let update_payload = page_update_payload_bytes(page)
    let update_operations = report_page_update_operation_count(page)
    if first_screen > max_first_screen {
      max_first_screen = first_screen
    }
    if update_payload > max_update_payload {
      max_update_payload = update_payload
    }
    if update_operations > max_update_operations {
      max_update_operations = update_operations
    }
    push_page_over_budget(
      diagnostics,
      "wechat-page-first-screen-bytes-over-budget",
      page.route,
      first_screen,
      budget.max_page_first_screen_bytes,
    )
    push_page_over_budget(
      diagnostics,
      "wechat-page-update-payload-bytes-over-budget",
      page.route,
      update_payload,
      budget.max_page_update_payload_bytes,
    )
    push_page_over_budget(
      diagnostics,
      "wechat-page-update-operations-over-budget",
      page.route,
      update_operations,
      budget.max_page_update_operations,
    )
  }
  (max_first_screen, max_update_payload, max_update_operations)
}

///|
fn page_first_screen_bytes(
  page : WechatProjectPage,
  files : Array[WechatFile],
) -> Int {
  route_file_bytes(page.route, files) +
  shared_first_screen_bytes(page.route, files) +
  page.runtime.initial_data_json.length()
}

///|
fn shared_first_screen_bytes(route : String, files : Array[WechatFile]) -> Int {
  let mut total = 0
  for file in files {
    if file.path == "app.wxss" || file.path == "bunnia.runtime.js" {
      total += file.content.length()
    } else if file.path == "bunnia.backend.js" &&
      route_requires_backend(route, files) {
      total += file.content.length()
    }
  }
  total
}

///|
fn route_requires_backend(route : String, files : Array[WechatFile]) -> Bool {
  for file in files {
    if file.path == "\{route}.js" && file.content.contains("bunnia.backend.js") {
      return true
    }
  }
  false
}

///|
fn page_update_payload_bytes(page : WechatProjectPage) -> Int {
  page_event_patch_bytes(page)
}

///|
fn report_page_update_operation_count(page : WechatProjectPage) -> Int {
  let mut total = 0
  for item in page.runtime.event_patches {
    if page.page.plan.event_messages.contains(item.message) {
      total += item.patches.length()
    }
  }
  total
}

///|
fn initial_data_bytes(items : Array[WechatProjectPage]) -> Int {
  let mut total = 0
  for item in items {
    total += item.runtime.initial_data_json.length()
  }
  total
}

///|
fn event_patch_bytes(pages : Array[WechatProjectPage]) -> Int {
  let mut total = 0
  for page in pages {
    total += page_event_patch_bytes(page)
  }
  total
}

///|
fn push_over_budget(
  diagnostics : Array[String],
  label : String,
  actual : Int,
  limit : Int,
) -> Unit {
  if actual > limit {
    diagnostics.push("\{label}:\{actual}/\{limit}")
  }
}

///|
fn push_page_over_budget(
  diagnostics : Array[String],
  label : String,
  route : String,
  actual : Int,
  limit : Int,
) -> Unit {
  if actual > limit {
    diagnostics.push("\{label}:\{route}:\{actual}/\{limit}")
  }
}