// Copyright 2025 International Digital Economy Academy
//
// 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.

///|
type JsonDecodeError = @json.JsonDecodeError

///|
pub(all) suberror LayoutDocumentError {
  EmptyNodeId
  DuplicateNodeId(String)
} derive(Debug)

///|
pub(all) struct LayoutNode {
  id : String
  label : String
  style : @style.Style
  children : Array[LayoutNode]
}

///|
pub(all) struct LayoutDocument {
  version : Int
  available_space : @geometry.Size[@geometry.AvailableSpace]
  root : LayoutNode
}

///|
pub(all) struct LayoutResultNode {
  id : String
  label : String
  parent_id : String?
  depth : Int
  layout : @tree.Layout
}

///|
pub(all) struct LayoutResult {
  root_id : String
  nodes : Array[LayoutResultNode]
}

///|
priv struct BuiltNode {
  id : String
  label : String
  parent_id : String?
  depth : Int
  tree_id : @tree.NodeId
}

///|
fn available_space_from_json(
  json : Json,
  path : @json.JsonPath,
) -> @geometry.AvailableSpace raise JsonDecodeError {
  match json {
    String("MinContent") => AvailMinContent
    String("MaxContent") => AvailMaxContent
    String(_) =>
      raise JsonDecodeError((path, "AvailableSpace: unsupported string"))
    _ => {
      let value : Double = @json.from_json(json, path~)
      AvailDefinite(value)
    }
  }
}

///|
fn available_space_to_json(space : @geometry.AvailableSpace) -> Json {
  match space {
    AvailDefinite(value) => Json::number(value)
    AvailMinContent => Json::string("MinContent")
    AvailMaxContent => Json::string("MaxContent")
  }
}

///|
fn available_size_from_json(
  json : Json,
  path : @json.JsonPath,
) -> @geometry.Size[@geometry.AvailableSpace] raise JsonDecodeError {
  guard json is { "width"? : width, "height"? : height, .. } else {
    raise JsonDecodeError((path, "AvailableSpace size: expected object"))
  }
  let parsed_width = match width {
    Some(value) => available_space_from_json(value, path.add_key("width"))
    None => AvailMaxContent
  }
  let parsed_height = match height {
    Some(value) => available_space_from_json(value, path.add_key("height"))
    None => AvailMaxContent
  }
  Size(width=parsed_width, height=parsed_height)
}

///|
fn available_size_to_json(
  size : @geometry.Size[@geometry.AvailableSpace],
) -> Json {
  Json::object({
    "width": available_space_to_json(size.width),
    "height": available_space_to_json(size.height),
  })
}

///|
pub impl @json.FromJson for LayoutNode with fn from_json(json, path) {
  guard json
    is {
      "id": id_json,
      "label"? : label_json,
      "style"? : style_json,
      "children"? : children_json,
      ..
    } else {
    raise JsonDecodeError((path, "LayoutNode: expected object with id"))
  }
  let id : String = @json.from_json(id_json, path=path.add_key("id"))
  let label : String = match label_json {
    Some(value) => @json.from_json(value, path=path.add_key("label"))
    None => id
  }
  let style : @style.Style = match style_json {
    Some(value) => @json.from_json(value, path=path.add_key("style"))
    None => @style.Style::default()
  }
  let children : Array[LayoutNode] = match children_json {
    Some(value) => @json.from_json(value, path=path.add_key("children"))
    None => []
  }
  { id, label, style, children }
}

///|
pub impl ToJson for LayoutNode with fn to_json(self) {
  Json::object({
    "id": Json::string(self.id),
    "label": Json::string(self.label),
    "style": self.style.to_json(),
    "children": Json::array(self.children.map(fn(child) { child.to_json() })),
  })
}

///|
pub impl @json.FromJson for LayoutDocument with fn from_json(json, path) {
  guard json
    is {
      "version"? : version_json,
      "available_space"? : available_space_json,
      "root": root_json,
      ..
    } else {
    raise JsonDecodeError((path, "LayoutDocument: expected object with root"))
  }
  let version : Int = match version_json {
    Some(value) => @json.from_json(value, path=path.add_key("version"))
    None => 1
  }
  if version != 1 {
    raise JsonDecodeError(
      (path.add_key("version"), "Only version 1 is supported"),
    )
  }
  let available_space = match available_space_json {
    Some(value) =>
      available_size_from_json(value, path.add_key("available_space"))
    None => @geometry.Size::max_content()
  }
  let root : LayoutNode = @json.from_json(root_json, path=path.add_key("root"))
  { version, available_space, root }
}

///|
pub impl ToJson for LayoutDocument with fn to_json(self) {
  Json::object({
    "version": Json::number(self.version.to_double()),
    "available_space": available_size_to_json(self.available_space),
    "root": self.root.to_json(),
  })
}

///|
pub fn LayoutDocument::from_json(
  json : Json,
) -> LayoutDocument raise JsonDecodeError {
  @json.from_json(json)
}

///|
fn point_to_json(point : @geometry.Point[Double]) -> Json {
  Json::object({ "x": Json::number(point.x), "y": Json::number(point.y) })
}

///|
fn size_to_json(size : @geometry.Size[Double]) -> Json {
  Json::object({
    "width": Json::number(size.width),
    "height": Json::number(size.height),
  })
}

///|
fn rect_to_json(rect : @geometry.Rect[Double]) -> Json {
  Json::object({
    "left": Json::number(rect.left),
    "right": Json::number(rect.right),
    "top": Json::number(rect.top),
    "bottom": Json::number(rect.bottom),
  })
}

///|
fn layout_to_json(layout : @tree.Layout) -> Json {
  Json::object({
    "order": Json::number(layout.order.to_double()),
    "location": point_to_json(layout.location),
    "size": size_to_json(layout.size),
    "content_size": size_to_json(layout.content_size),
    "scrollbar_size": size_to_json(layout.scrollbar_size),
    "border": rect_to_json(layout.border),
    "padding": rect_to_json(layout.padding),
  })
}

///|
fn optional_string_to_json(value : String?) -> Json {
  match value {
    Some(value) => Json::string(value)
    None => Json::null()
  }
}

///|
pub impl ToJson for LayoutResultNode with fn to_json(self) {
  Json::object({
    "id": Json::string(self.id),
    "label": Json::string(self.label),
    "parent_id": optional_string_to_json(self.parent_id),
    "depth": Json::number(self.depth.to_double()),
    "layout": layout_to_json(self.layout),
  })
}

///|
pub impl ToJson for LayoutResult with fn to_json(self) {
  Json::object({
    "root_id": Json::string(self.root_id),
    "nodes": Json::array(self.nodes.map(fn(node) { node.to_json() })),
  })
}

///|
pub fn LayoutResult::to_json(self : LayoutResult) -> Json {
  (self : &ToJson).to_json()
}

///|
fn build_document_node(
  tree : @tree.ChicleTree[Unit],
  node : LayoutNode,
  parent_id : String?,
  depth : Int,
  seen : Map[String, Bool],
  built : Array[BuiltNode],
) -> @tree.NodeId raise {
  if node.id == "" {
    raise EmptyNodeId
  }
  if seen.contains(node.id) {
    raise DuplicateNodeId(node.id)
  }
  seen[node.id] = true
  let tree_id = tree.new_leaf(node.style)
  built.push({ id: node.id, label: node.label, parent_id, depth, tree_id })
  let child_ids : Array[@tree.NodeId] = []
  for child in node.children {
    child_ids.push(
      build_document_node(tree, child, Some(node.id), depth + 1, seen, built),
    )
  }
  tree.set_children(tree_id, child_ids)
  tree_id
}

///|
pub fn compute_layout_document(document : LayoutDocument) -> LayoutResult raise {
  let tree : @tree.ChicleTree[Unit] = ChicleTree()
  let seen : Map[String, Bool] = Map([])
  let built : Array[BuiltNode] = []
  let root_tree_id = build_document_node(
    tree,
    document.root,
    None,
    0,
    seen,
    built,
  )
  tree.compute_layout(root_tree_id, document.available_space)
  let nodes : Array[LayoutResultNode] = []
  for node in built {
    nodes.push({
      id: node.id,
      label: node.label,
      parent_id: node.parent_id,
      depth: node.depth,
      layout: tree.layout(node.tree_id),
    })
  }
  { root_id: document.root.id, nodes }
}

///|
fn response_error(kind : String, message : String) -> String {
  Json::object({
    "ok": Json::boolean(false),
    "error": Json::object({
      "kind": Json::string(kind),
      "message": Json::string(message),
    }),
  }).stringify()
}

///|
pub fn compute_layout_json(input : String) -> String {
  if !@json.valid(input) {
    return response_error("parse", "Invalid JSON")
  }
  try {
    let document = LayoutDocument::from_json(@json.parse(input))
    let result = compute_layout_document(document)
    Json::object({ "ok": Json::boolean(true), "result": result.to_json() }).stringify()
  } catch {
    @json.JsonDecodeError((path, message)) =>
      response_error("input", "\{path.to_string()}: \{message}")
    EmptyNodeId => response_error("input", "Node ids must not be empty")
    DuplicateNodeId(id) => response_error("input", "Duplicate node id: \{id}")
    _ => response_error("layout", "Layout computation failed")
  }
}