// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|

///|
pub struct ContextMenuItem {
  id : String
  label : String
  shortcut : String
  disabled : Bool
  is_separator : Bool
  children : Array[ContextMenuItem]
} derive(Eq, Debug)

///|
pub fn ContextMenuItem::new(
  id : String,
  label : String,
  shortcut? : String = "",
  disabled? : Bool = false,
  children? : Array[ContextMenuItem] = [],
) -> ContextMenuItem {
  { id, label, shortcut, disabled, is_separator: false, children, }
}

///|
pub fn ContextMenuItem::submenu(
  id : String,
  label : String,
  children : Array[ContextMenuItem],
  disabled? : Bool = false,
) -> ContextMenuItem {
  { id, label, shortcut: "", disabled, is_separator: false, children, }
}

///|
pub fn ContextMenuItem::separator() -> ContextMenuItem {
  {
    id: "",
    label: "",
    shortcut: "",
    disabled: true,
    is_separator: true,
    children: [],
  }
}

///|
pub struct ContextMenuResponse {
  open : Bool
  selected_id : String?
  pos : @math.Vec2
  size : @math.Vec2
} derive(Eq, Debug)

///|
/// Helper to compute collision-aware context menu placement with automatic
/// horizontal and vertical screen boundary flipping.
fn calculate_context_menu_pos(
  anchor : @math.Vec2,
  size : @math.Vec2,
  viewport : @math.Vec2,
) -> @math.Vec2 {
  let mut x = anchor.x + 2.0
  let mut y = anchor.y + 2.0

  // Horizontal collision: if menu exceeds viewport width, flip to the left of anchor
  if x + size.x > viewport.x {
    let flipped_x = anchor.x - size.x - 2.0
    x = if flipped_x >= 4.0 { flipped_x } else { viewport.x - size.x - 4.0 }
  }

  // Ensure left boundary safety
  if x < 4.0 {
    x = 4.0
  }

  // Vertical collision: if menu exceeds viewport height, flip upwards
  if y + size.y > viewport.y {
    let flipped_y = anchor.y - size.y - 2.0
    y = if flipped_y >= 4.0 { flipped_y } else { viewport.y - size.y - 4.0 }
  }

  // Ensure top boundary safety
  if y < 4.0 {
    y = 4.0
  }

  @math.Vec2::new(x, y)
}

///|
fn compute_menu_content_height(
  items : Array[ContextMenuItem],
  item_h : Double,
  sep_h : Double,
  pad_y : Double,
) -> Double {
  let mut h = 0.0
  for item in items {
    if item.is_separator {
      h = h + sep_h
    } else {
      h = h + item_h
    }
  }
  if h > 0.0 {
    h + pad_y * 2.0
  } else {
    32.0
  }
}

///|
/// Render a right-click context menu modal in Studio Light porcelain style.
/// Anchors to the specified pointer position with boundary collision detection,
/// outside-click dismissal, cascading submenus, and keyboard Escape cancellation.
pub fn context_menu_items(
  ctx : UIContext,
  id_salt : String,
  open : Bool,
  pos : @math.Vec2,
  items : Array[ContextMenuItem],
  menu_width? : Double = 180.0,
  viewport_size? : @math.Vec2,
) -> ContextMenuResponse {
  let _ = id_salt
  if !open {
    ctx.set_active_submenu_id("")
    return { open: false, selected_id: None, pos, size: @math.Vec2::zero(), }
  }

  let scale = ctx.style.scale
  let item_h = 24.0 * scale
  let sep_h = 7.0 * scale
  let pad_y = 4.0 * scale

  let total_w = menu_width * scale
  let total_h = compute_menu_content_height(items, item_h, sep_h, pad_y)
  let menu_size = @math.Vec2::new(total_w, total_h)

  // Viewport dimensions and collision flip calculation
  let viewport = match viewport_size {
    Some(vp) => vp
    None => {
      let vw = if ctx.available_width() > 0.0 {
        ctx.available_width()
      } else {
        800.0
      }
      @math.Vec2::new(vw, 600.0)
    }
  }

  let placed_pos = calculate_context_menu_pos(pos, menu_size, viewport)
  let menu_rect = @math.Rect::new(placed_pos.x, placed_pos.y, total_w, total_h)

  // Occlusion and hover blocking
  ctx.block_hover(menu_rect)

  // Escape dismissal
  if ctx.input.key_pressed(Key::Escape) {
    ctx.set_active_submenu_id("")
    return { open: false, selected_id: None, pos: placed_pos, size: menu_size, }
  }

  // Identify hovered item and active submenu. Like every popup that blocks
  // hover over its own area, the interaction phase runs in the foreground
  // scope: is_hovered ignores blockers there, so the menu's own one-frame-old
  // blocker cannot swallow its rows.
  ctx.begin_foreground()
  let mut cur_y = placed_pos.y + pad_y
  let mut active_sub_item : ContextMenuItem? = None
  let mut active_sub_row_y = 0.0
  let mut selected_id : String? = None

  for item in items {
    if item.is_separator {
      cur_y = cur_y + sep_h
    } else {
      let row_rect = @math.Rect::new(
        placed_pos.x + 4.0 * scale,
        cur_y,
        total_w - 8.0 * scale,
        item_h,
      )
      let hovered = ctx.is_hovered(row_rect) && !item.disabled
      if hovered {
        ctx.set_cursor_icon("pointer")
        if item.children.length() > 0 {
          ctx.set_active_submenu_id(item.id)
        } else if ctx.active_submenu_id() != "" &&
          !item.children.is_empty() == false {
          ctx.set_active_submenu_id("")
        }
      }

      if ctx.active_submenu_id() == item.id && item.children.length() > 0 {
        active_sub_item = Some(item)
        active_sub_row_y = cur_y
      }

      if hovered && ctx.input.mouse_pressed && item.children.is_empty() {
        selected_id = Some(item.id)
      }
      cur_y = cur_y + item_h
    }
  }

  // Submenu placement, hover blocking, and hit testing
  let mut sub_rect_opt : @math.Rect? = None
  if active_sub_item is Some(sub_item) {
    let sub_w = total_w
    let sub_h = compute_menu_content_height(
      sub_item.children,
      item_h,
      sep_h,
      pad_y,
    )
    let mut sub_x = placed_pos.x + total_w - 2.0 * scale
    if sub_x + sub_w > viewport.x {
      sub_x = placed_pos.x - sub_w + 2.0 * scale
    }
    if sub_x < 4.0 {
      sub_x = 4.0
    }
    let mut sub_y = active_sub_row_y - pad_y
    if sub_y + sub_h > viewport.y {
      sub_y = viewport.y - sub_h - 4.0
    }
    if sub_y < 4.0 {
      sub_y = 4.0
    }
    let sub_rect = @math.Rect::new(sub_x, sub_y, sub_w, sub_h)
    sub_rect_opt = Some(sub_rect)
    ctx.block_hover(sub_rect)

    // Submenu item hit-testing
    let mut sub_cur_y = sub_y + pad_y
    for child in sub_item.children {
      if child.is_separator {
        sub_cur_y = sub_cur_y + sep_h
      } else {
        let child_row = @math.Rect::new(
          sub_x + 4.0 * scale,
          sub_cur_y,
          sub_w - 8.0 * scale,
          item_h,
        )
        let child_hovered = ctx.is_hovered(child_row) && !child.disabled
        if child_hovered {
          ctx.set_cursor_icon("pointer")
          if ctx.input.mouse_pressed {
            selected_id = Some(child.id)
          }
        }
        sub_cur_y = sub_cur_y + item_h
      }
    }
  }

  // Keyboard navigation while the menu is open: arrows walk the actionable
  // rows (persisted in scoped memory), Enter activates a row or opens a
  // submenu. The keyboard selection highlights like hover in the render pass.
  let menu_kb_id = ctx.id_stack.derive_id("ctx_menu_kb::" + id_salt)
  let actionable : Array[Int] = []
  for i, it in items {
    if !it.is_separator && !it.disabled {
      actionable.push(i)
    }
  }
  if actionable.length() > 0 {
    let mut cur = ctx
      .memory()
      .get_int_or_default(menu_kb_id, actionable[0], tag="kb")
    let mut in_list = false
    for i in actionable {
      if i == cur {
        in_list = true
        break
      }
    }
    if !in_list {
      cur = actionable[0]
    }
    if ctx.input.key_pressed(Key::ArrowDown) {
      ctx.input.consume_key(Key::ArrowDown)
      let pos_in = (fn() {
        for p, i in actionable {
          if i == cur {
            return p
          }
        }
        0
      })()
      cur = actionable[if pos_in + 1 < actionable.length() {
          pos_in + 1
        } else {
          pos_in
        }]
    }
    if ctx.input.key_pressed(Key::ArrowUp) {
      ctx.input.consume_key(Key::ArrowUp)
      let pos_in = (fn() {
        for p, i in actionable {
          if i == cur {
            return p
          }
        }
        0
      })()
      cur = actionable[if pos_in > 0 { pos_in - 1 } else { 0 }]
    }
    ctx.memory().set_int(menu_kb_id, cur, tag="kb")
    let kb_item = items[cur]
    if ctx.input.key_pressed(Key::Enter) {
      ctx.input.consume_key(Key::Enter)
      if kb_item.children.length() > 0 {
        ctx.set_active_submenu_id(kb_item.id)
      } else {
        selected_id = Some(kb_item.id)
      }
    }
  }

  // Outside click dismissal
  if ctx.input.mouse_pressed {
    let in_main = menu_rect.contains(ctx.input.mouse_pos)
    let in_sub = match sub_rect_opt {
      Some(sr) => sr.contains(ctx.input.mouse_pos)
      None => false
    }
    if !in_main && !in_sub {
      ctx.set_active_submenu_id("")
      ctx.end_foreground()
      return {
        open: false,
        selected_id: None,
        pos: placed_pos,
        size: menu_size,
      }
    }
  }

  if selected_id is Some(id) {
    ctx.set_active_submenu_id("")
    ctx.end_foreground()
    return {
      open: false,
      selected_id: Some(id),
      pos: placed_pos,
      size: menu_size,
    }
  }

  ctx.end_foreground()

  // Render into foreground layer
  ctx.begin_foreground()

  let menu_radius = ctx.style.radius_md * scale
  let row_radius = ctx.style.radius_sm * scale
  let font_nm = ctx.style.font_normal * scale
  let font_sm = ctx.style.font_small * scale

  // 1. Dual-layer Elevation Shadow for main menu
  ctx
  .painter()
  .add_rect(
    @math.Rect::new(placed_pos.x, placed_pos.y + 4.0 * scale, total_w, total_h),
    @color.Color::shadow_ambient(),
    menu_radius,
  )
  ctx
  .painter()
  .add_rect(
    @math.Rect::new(placed_pos.x, placed_pos.y + 1.0 * scale, total_w, total_h),
    @color.Color::shadow_key(),
    menu_radius,
  )

  // Porcelain Surface and Crisp Border
  ctx.painter().add_rect(menu_rect, @color.Color::bg_window(), menu_radius)
  ctx
  .painter()
  .add_rect_stroke(menu_rect, @color.Color::border_default(), 1.0, menu_radius)

  // Render main menu items
  let kb_idx = ctx.memory().get_int_or_default(menu_kb_id, -1, tag="kb")
  let mut cur_draw_y = placed_pos.y + pad_y
  for i, item in items {
    if item.is_separator {
      let line_y = cur_draw_y + 3.0 * scale
      ctx
      .painter()
      .add_line(
        @math.Vec2::new(placed_pos.x + 8.0 * scale, line_y),
        @math.Vec2::new(placed_pos.x + total_w - 8.0 * scale, line_y),
        @color.Color::border_muted(),
        1.0,
      )
      cur_draw_y = cur_draw_y + sep_h
    } else {
      let row_rect = @math.Rect::new(
        placed_pos.x + 4.0 * scale,
        cur_draw_y,
        total_w - 8.0 * scale,
        item_h,
      )
      let kb_selected = !item.is_separator && !item.disabled && kb_idx == i
      let hovered = (
          ctx.is_hovered(row_rect) ||
          ctx.active_submenu_id() == item.id ||
          kb_selected
        ) &&
        !item.disabled

      if hovered {
        ctx.painter().add_rect(row_rect, @color.Color::bg_surface(), row_radius)
      }

      let text_col = if item.disabled {
        @color.Color::text_disabled()
      } else if hovered {
        @color.Color::accent_primary()
      } else {
        @color.Color::text_primary()
      }

      let text_y = cur_draw_y + (item_h - font_nm) * 0.5
      ctx
      .painter()
      .add_text(
        @math.Vec2::new(placed_pos.x + 12.0 * scale, text_y),
        item.label,
        font_nm,
        text_col,
      )

      if item.children.length() > 0 {
        // Render cascading chevron indicator
        let chev_col = if hovered {
          @color.Color::accent_primary()
        } else {
          @color.Color::text_muted()
        }
        ctx
        .painter()
        .add_text(
          @math.Vec2::new(placed_pos.x + total_w - 18.0 * scale, text_y),
          ">",
          font_sm,
          chev_col,
        )
      } else if !item.shortcut.is_empty() {
        let sc_size = ctx.measure_text(item.shortcut, font_sm)
        let sc_x = placed_pos.x + total_w - sc_size.x - 12.0 * scale
        let sc_col = if item.disabled {
          @color.Color::text_disabled()
        } else if hovered {
          @color.Color::accent_primary()
        } else {
          @color.Color::text_muted()
        }
        ctx
        .painter()
        .add_text(
          @math.Vec2::new(sc_x, text_y + 1.0 * scale),
          item.shortcut,
          font_sm,
          sc_col,
        )
      }

      cur_draw_y = cur_draw_y + item_h
    }
  }

  // 2. Render cascading Submenu if active
  if active_sub_item is Some(sub_item) && sub_rect_opt is Some(sub_rect) {
    ctx
    .painter()
    .add_rect(
      @math.Rect::new(
        sub_rect.x,
        sub_rect.y + 4.0 * scale,
        sub_rect.w,
        sub_rect.h,
      ),
      @color.Color::shadow_ambient(),
      menu_radius,
    )
    ctx
    .painter()
    .add_rect(
      @math.Rect::new(
        sub_rect.x,
        sub_rect.y + 1.0 * scale,
        sub_rect.w,
        sub_rect.h,
      ),
      @color.Color::shadow_key(),
      menu_radius,
    )
    ctx.painter().add_rect(sub_rect, @color.Color::bg_window(), menu_radius)
    ctx
    .painter()
    .add_rect_stroke(sub_rect, @color.Color::border_default(), 1.0, menu_radius)

    let mut sub_draw_y = sub_rect.y + pad_y
    for child in sub_item.children {
      if child.is_separator {
        let line_y = sub_draw_y + 3.0 * scale
        ctx
        .painter()
        .add_line(
          @math.Vec2::new(sub_rect.x + 8.0 * scale, line_y),
          @math.Vec2::new(sub_rect.x + sub_rect.w - 8.0 * scale, line_y),
          @color.Color::border_muted(),
          1.0,
        )
        sub_draw_y = sub_draw_y + sep_h
      } else {
        let child_row = @math.Rect::new(
          sub_rect.x + 4.0 * scale,
          sub_draw_y,
          sub_rect.w - 8.0 * scale,
          item_h,
        )
        let child_hovered = ctx.is_hovered(child_row) && !child.disabled
        if child_hovered {
          ctx
          .painter()
          .add_rect(child_row, @color.Color::bg_surface(), row_radius)
        }

        let text_col = if child.disabled {
          @color.Color::text_disabled()
        } else if child_hovered {
          @color.Color::accent_primary()
        } else {
          @color.Color::text_primary()
        }

        let text_y = sub_draw_y + (item_h - font_nm) * 0.5
        ctx
        .painter()
        .add_text(
          @math.Vec2::new(sub_rect.x + 12.0 * scale, text_y),
          child.label,
          font_nm,
          text_col,
        )

        if !child.shortcut.is_empty() {
          let sc_size = ctx.measure_text(child.shortcut, font_sm)
          let sc_x = sub_rect.x + sub_rect.w - sc_size.x - 12.0 * scale
          let sc_col = if child.disabled {
            @color.Color::text_disabled()
          } else if child_hovered {
            @color.Color::accent_primary()
          } else {
            @color.Color::text_muted()
          }
          ctx
          .painter()
          .add_text(
            @math.Vec2::new(sc_x, text_y + 1.0 * scale),
            child.shortcut,
            font_sm,
            sc_col,
          )
        }

        sub_draw_y = sub_draw_y + item_h
      }
    }
  }

  ctx.end_foreground()

  { open: true, selected_id: None, pos: placed_pos, size: menu_size, }
}

///|
/// Closure-based ContextMenu for fully custom layout and widget composition.
pub fn context_menu(
  ctx : UIContext,
  id_salt : String,
  open : Bool,
  pos : @math.Vec2,
  content : (UIContext) -> Unit,
  menu_width? : Double = 180.0,
  viewport_size? : @math.Vec2,
) -> ContextMenuResponse {
  let menu_id = ctx.id_stack.derive_id("context_menu::" + id_salt)
  if !open {
    if ctx.open_menu_id() == menu_id {
      ctx.set_open_menu_id(Id::zero())
    }
    return { open: false, selected_id: None, pos, size: @math.Vec2::zero(), }
  }

  ctx.set_open_menu_id(menu_id)

  let pad = 4.0
  let total_w = menu_width

  // Measure content by evaluating in a temporary draw list
  let prev_cursor = ctx.cursor()
  let prev_avail_w = ctx.available_width()
  let prev_spacing = ctx.item_spacing()

  ctx.set_cursor(@math.Vec2::new(pad, pad))
  ctx.set_available_width(total_w - pad * 2.0)
  ctx.set_item_spacing(@math.Vec2::new(0.0, 2.0))

  let temp_draw_list = @draw.DrawList::new()
  let saved_dl = ctx.layers.draw_list()
  ctx.layers.set_draw_list(temp_draw_list)

  ctx.vertical(content)

  ctx.layers.set_draw_list(saved_dl)

  let content_h = ctx.cursor().y - pad
  let total_h = if content_h > 0.0 { content_h + pad * 2.0 } else { 32.0 }
  let menu_size = @math.Vec2::new(total_w, total_h)

  // Viewport and placement
  let viewport = match viewport_size {
    Some(vp) => vp
    None => {
      let vw = if ctx.available_width() > 0.0 {
        ctx.available_width()
      } else {
        800.0
      }
      @math.Vec2::new(vw, 600.0)
    }
  }

  let placed_pos = calculate_context_menu_pos(pos, menu_size, viewport)
  let menu_rect = @math.Rect::new(placed_pos.x, placed_pos.y, total_w, total_h)

  ctx.block_hover(menu_rect)

  // Escape key dismissal
  if ctx.input.key_pressed(Key::Escape) {
    ctx.set_open_menu_id(Id::zero())
    ctx.set_cursor(prev_cursor)
    ctx.set_available_width(prev_avail_w)
    ctx.set_item_spacing(prev_spacing)
    return { open: false, selected_id: None, pos: placed_pos, size: menu_size, }
  }

  // Outside click dismissal
  if ctx.input.mouse_pressed && !menu_rect.contains(ctx.input.mouse_pos) {
    ctx.set_open_menu_id(Id::zero())
    ctx.set_cursor(prev_cursor)
    ctx.set_available_width(prev_avail_w)
    ctx.set_item_spacing(prev_spacing)
    return { open: false, selected_id: None, pos: placed_pos, size: menu_size, }
  }

  // Re-run closure with exact placed coordinates in foreground
  ctx.begin_foreground()

  // Shadows
  let custom_menu_radius = ctx.style.radius_md
  ctx
  .painter()
  .add_rect(
    @math.Rect::new(placed_pos.x, placed_pos.y + 4.0, total_w, total_h),
    @color.Color::shadow_ambient(),
    custom_menu_radius,
  )
  ctx
  .painter()
  .add_rect(
    @math.Rect::new(placed_pos.x, placed_pos.y + 1.0, total_w, total_h),
    @color.Color::shadow_key(),
    custom_menu_radius,
  )

  // Card
  ctx
  .painter()
  .add_rect(menu_rect, @color.Color::bg_window(), custom_menu_radius)
  ctx
  .painter()
  .add_rect_stroke(
    menu_rect,
    @color.Color::border_default(),
    1.0,
    custom_menu_radius,
  )

  ctx.set_cursor(@math.Vec2::new(placed_pos.x + pad, placed_pos.y + pad))
  ctx.set_available_width(total_w - pad * 2.0)
  ctx.set_item_spacing(@math.Vec2::new(0.0, 2.0))

  ctx.vertical(content)

  ctx.end_foreground()

  ctx.set_cursor(prev_cursor)
  ctx.set_available_width(prev_avail_w)
  ctx.set_item_spacing(prev_spacing)

  let is_still_open = ctx.open_menu_id() == menu_id
  { open: is_still_open, selected_id: None, pos: placed_pos, size: menu_size, }
}