///|
pub struct SceneSize {
  width : Int
  height : Int
} derive(Debug, Eq)

///|
pub struct SceneViewport {
  x : Int
  y : Int
  width : Int
  height : Int
} derive(Debug, Eq)

///|
pub struct SceneMarker {
  id : String
  label : String
  x : Int
  y : Int
  status : String
  tap_message : String
  asset_ref : String
  hit_width : Int
  hit_height : Int
} derive(Debug, Eq)

///|
pub struct SceneRegion {
  id : String
  label : String
  x : Int
  y : Int
  width : Int
  height : Int
  status : String
  tap_message : String
} derive(Debug, Eq)

///|
pub struct SceneLayer {
  id : String
  z_index : Int
  markers : Array[SceneMarker]
  regions : Array[SceneRegion]
} derive(Debug, Eq)

///|
pub struct Scene {
  id : String
  size : SceneSize
  layers : Array[SceneLayer]
  assets : Array[SceneAsset]
} derive(Debug, Eq)

///|
pub fn scene_size(width : Int, height : Int) -> SceneSize {
  { width, height }
}

///|
pub fn scene_viewport(
  x~ : Int,
  y~ : Int,
  width~ : Int,
  height~ : Int,
) -> SceneViewport {
  { x, y, width, height }
}

///|
pub fn marker(
  id~ : String,
  label~ : String,
  x~ : Int,
  y~ : Int,
  status? : String = "",
  tap_message? : String = "",
  asset_ref? : String = "",
  hit_width? : Int = 44,
  hit_height? : Int = 44,
) -> SceneMarker {
  { id, label, x, y, status, tap_message, asset_ref, hit_width, hit_height }
}

///|
pub fn region(
  id~ : String,
  label~ : String,
  x~ : Int,
  y~ : Int,
  width~ : Int,
  height~ : Int,
  status? : String = "",
  tap_message? : String = "",
) -> SceneRegion {
  { id, label, x, y, width, height, status, tap_message }
}

///|
pub fn layer(
  id : String,
  z_index : Int,
  markers : Array[SceneMarker],
  regions? : Array[SceneRegion] = [],
) -> SceneLayer {
  { id, z_index, markers, regions }
}

///|
pub fn scene(
  id : String,
  size : SceneSize,
  layers : Array[SceneLayer],
  assets? : Array[SceneAsset] = [],
) -> Scene {
  { id, size, layers, assets }
}

///|
pub fn static_scene_view(input : Scene) -> @core.Node {
  scene_view(input, [])
}

///|
pub fn static_scene_view_with_budget(
  input : Scene,
  budget : SceneRenderBudget,
) -> @core.Node {
  let plan = plan_scene_render_with_budget(input, budget)
  let rendered = if plan.marker_count > budget.max_markers ||
    plan.region_count > budget.max_regions {
    limit_scene_geometry(input, budget)
  } else {
    input
  }
  scene_view(rendered, [
    @core.data("scene-mode", scene_render_mode_id(plan.mode)),
    @core.data("scene-quality", scene_quality_id(plan.quality)),
    @core.data("markers-total", plan.marker_count.to_string()),
    @core.data("markers-visible", scene_marker_count(rendered).to_string()),
    @core.data("regions-total", plan.region_count.to_string()),
    @core.data("regions-visible", scene_region_count(rendered).to_string()),
    @core.data(
      "scene-degraded",
      (plan.marker_count > scene_marker_count(rendered) ||
      plan.region_count > scene_region_count(rendered)).to_string(),
    ),
  ])
}

///|
pub fn static_scene_view_with_viewport(
  input : Scene,
  viewport : SceneViewport,
  budget? : SceneRenderBudget = default_scene_render_budget(),
) -> @core.Node {
  let visible = viewport_scene(input, viewport)
  let plan = plan_scene_render_viewport(input, viewport, budget)
  let rendered = if scene_marker_count(visible) > budget.max_markers ||
    scene_region_count(visible) > budget.max_regions {
    limit_scene_geometry(visible, budget)
  } else {
    visible
  }
  scene_view_with_render_size(
    rendered,
    scene_size(viewport.width, viewport.height),
    [
      @core.data("scene-mode", scene_render_mode_id(plan.mode)),
      @core.data("scene-quality", scene_quality_id(plan.quality)),
      @core.data("viewport-x", viewport.x.to_string()),
      @core.data("viewport-y", viewport.y.to_string()),
      @core.data("viewport-width", viewport.width.to_string()),
      @core.data("viewport-height", viewport.height.to_string()),
      @core.data("markers-total", plan.marker_count.to_string()),
      @core.data("markers-visible", scene_marker_count(rendered).to_string()),
      @core.data("regions-total", plan.region_count.to_string()),
      @core.data("regions-visible", scene_region_count(rendered).to_string()),
      @core.data(
        "scene-degraded",
        (plan.visible_marker_count > scene_marker_count(rendered) ||
        plan.visible_region_count > scene_region_count(rendered)).to_string(),
      ),
    ],
  )
}

///|
fn scene_view(input : Scene, extra_attrs : Array[@core.Attr]) -> @core.Node {
  scene_view_with_render_size(input, input.size, extra_attrs)
}

///|
fn scene_view_with_render_size(
  input : Scene,
  render_size : SceneSize,
  extra_attrs : Array[@core.Attr],
) -> @core.Node {
  let children : Array[@core.Node] = []
  for item in input.layers {
    children.push(layer_view(item, input.assets))
  }
  let attrs = [
    @core.class_name("bunnia-scene"),
    @core.attr("style", scene_style(render_size)),
    @core.data("scene-id", input.id),
    @core.data("scene-width", input.size.width.to_string()),
    @core.data("scene-height", input.size.height.to_string()),
    @core.data("render-width", render_size.width.to_string()),
    @core.data("render-height", render_size.height.to_string()),
    @core.data("scene-assets", input.assets.length().to_string()),
  ]
  for item in extra_attrs {
    attrs.push(item)
  }
  @core.view_with_attrs(attrs, children)
}

///|
fn layer_view(input : SceneLayer, assets : Array[SceneAsset]) -> @core.Node {
  let children : Array[@core.Node] = []
  for item in input.regions {
    children.push(region_view(item))
  }
  for item in input.markers {
    children.push(marker_view(item, assets))
  }
  @core.view_with_attrs(
    [
      @core.class_name("bunnia-scene-layer"),
      @core.attr("style", "z-index: \{input.z_index};"),
      @core.data("layer-id", input.id),
      @core.data("z-index", input.z_index.to_string()),
    ],
    children,
  )
}

///|
fn region_view(input : SceneRegion) -> @core.Node {
  let events = if input.tap_message == "" {
    []
  } else {
    [@core.tap(input.tap_message)]
  }
  @core.keyed(
    input.id,
    @core.node(
      kind="view",
      attrs=[
        @core.class_name("bunnia-scene-region \{status_class(input.status)}"),
        @core.attr("style", region_style(input)),
        @core.data("region-id", input.id),
        @core.data("x", input.x.to_string()),
        @core.data("y", input.y.to_string()),
        @core.data("width", input.width.to_string()),
        @core.data("height", input.height.to_string()),
        @core.data("status", input.status),
      ],
      events~,
      children=[@core.text(input.label)],
    ),
  )
}

///|
fn marker_view(input : SceneMarker, assets : Array[SceneAsset]) -> @core.Node {
  let events = if input.tap_message == "" {
    []
  } else {
    [@core.tap(input.tap_message)]
  }
  let children : Array[@core.Node] = []
  match find_asset(assets, input.asset_ref) {
    Some(asset) => children.push(asset_image(asset, input.label))
    None => ()
  }
  children.push(@core.text(input.label))
  @core.keyed(
    input.id,
    @core.node(
      kind="view",
      attrs=[
        @core.class_name("bunnia-scene-marker \{status_class(input.status)}"),
        @core.attr("style", marker_style(input)),
        @core.data("marker-id", input.id),
        @core.data("x", input.x.to_string()),
        @core.data("y", input.y.to_string()),
        @core.data("status", input.status),
        @core.data("asset-ref", input.asset_ref),
        @core.data("hit-width", input.hit_width.to_string()),
        @core.data("hit-height", input.hit_height.to_string()),
      ],
      events~,
      children~,
    ),
  )
}

///|
pub fn set_scene_marker_status(
  scene_id : String,
  marker_id : String,
  status : String,
) -> @core.Patch {
  @core.set_string("scenes.\{scene_id}.markers.\{marker_id}.status", status)
}

///|
pub fn select_scene_marker(
  scene_id : String,
  marker_id : String,
) -> @core.Patch {
  @core.set_string("scenes.\{scene_id}.selectedMarker", marker_id)
}

///|
pub fn set_scene_region_status(
  scene_id : String,
  region_id : String,
  status : String,
) -> @core.Patch {
  @core.set_string("scenes.\{scene_id}.regions.\{region_id}.status", status)
}

///|
pub fn select_scene_region(
  scene_id : String,
  region_id : String,
) -> @core.Patch {
  @core.set_string("scenes.\{scene_id}.selectedRegion", region_id)
}

///|
fn find_asset(assets : Array[SceneAsset], asset_ref : String) -> SceneAsset? {
  if asset_ref == "" {
    None
  } else {
    for item in assets {
      if item.id == asset_ref {
        return Some(item)
      }
    }
    None
  }
}

///|
fn asset_image(asset : SceneAsset, alt : String) -> @core.Node {
  @core.node(kind="image", attrs=[
    @core.class_name("bunnia-scene-sprite"),
    @core.attr("src", asset.src),
    @core.attr("alt", alt),
    @core.data("asset-id", asset.id),
    @core.data("asset-kind", scene_asset_kind_id(asset.kind)),
    @core.data("asset-remote", asset.remote.to_string()),
    @core.data("asset-bytes", asset.estimated_bytes.to_string()),
  ])
}

///|
fn scene_style(size : SceneSize) -> String {
  "width: \{size.width}rpx; height: \{size.height}rpx;"
}

///|
fn marker_style(input : SceneMarker) -> String {
  "left: \{input.x}rpx; top: \{input.y}rpx; min-width: \{input.hit_width}rpx; min-height: \{input.hit_height}rpx;"
}

///|
fn region_style(input : SceneRegion) -> String {
  "left: \{input.x}rpx; top: \{input.y}rpx; width: \{input.width}rpx; height: \{input.height}rpx;"
}

///|
fn status_class(status : String) -> String {
  if status == "" {
    "bunnia-status-default"
  } else {
    "bunnia-status-\{status_slug(status)}"
  }
}

///|
fn status_slug(status : String) -> String {
  status
  .replace(old=" ", new="-")
  .replace(old="_", new="-")
  .replace(old="/", new="-")
  .to_string()
}

///|
fn viewport_scene(input : Scene, viewport : SceneViewport) -> Scene {
  let layers : Array[SceneLayer] = []
  for item in input.layers {
    let markers : Array[SceneMarker] = []
    let regions : Array[SceneRegion] = []
    for scene_marker in item.markers {
      if marker_in_viewport(scene_marker, viewport) {
        markers.push(translate_marker(scene_marker, viewport))
      }
    }
    for scene_region in item.regions {
      if region_in_viewport(scene_region, viewport) {
        regions.push(translate_region(scene_region, viewport))
      }
    }
    layers.push(layer(item.id, item.z_index, markers, regions~))
  }
  scene(input.id, input.size, layers, assets=input.assets)
}

///|
fn translate_marker(
  input : SceneMarker,
  viewport : SceneViewport,
) -> SceneMarker {
  {
    id: input.id,
    label: input.label,
    x: input.x - viewport.x,
    y: input.y - viewport.y,
    status: input.status,
    tap_message: input.tap_message,
    asset_ref: input.asset_ref,
    hit_width: input.hit_width,
    hit_height: input.hit_height,
  }
}

///|
fn translate_region(
  input : SceneRegion,
  viewport : SceneViewport,
) -> SceneRegion {
  {
    id: input.id,
    label: input.label,
    x: input.x - viewport.x,
    y: input.y - viewport.y,
    width: input.width,
    height: input.height,
    status: input.status,
    tap_message: input.tap_message,
  }
}

///|
fn marker_in_viewport(input : SceneMarker, viewport : SceneViewport) -> Bool {
  input.x >= viewport.x &&
  input.y >= viewport.y &&
  input.x < viewport.x + viewport.width &&
  input.y < viewport.y + viewport.height
}

///|
fn region_in_viewport(input : SceneRegion, viewport : SceneViewport) -> Bool {
  input.x + input.width > viewport.x &&
  input.y + input.height > viewport.y &&
  input.x < viewport.x + viewport.width &&
  input.y < viewport.y + viewport.height
}