// 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.

///|
pub(all) suberror LayoutYamlError {
  EmptyYamlDocument
  MultipleYamlDocuments(Int)
} derive(Debug)

///|
priv struct CompactLayoutDocument {
  document : LayoutDocument
}

///|
fn[T] compact_yaml_error(
  path : @json.JsonPath,
  message : String,
) -> T raise JsonDecodeError {
  raise JsonDecodeError((path, message))
}

///|
fn compact_dimension(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    Number(_) => Json::object({ "Length": value })
    String("auto") | String("Auto") => Json::string("Auto")
    String("min-content") | String("MinContent") => Json::string("MinContent")
    String("max-content") | String("MaxContent") => Json::string("MaxContent")
    String(text) =>
      match text.strip_suffix("%") {
        Some(number) =>
          Json::object({
            "Percent": Json::number(@string.parse_double(number) / 100.0),
          }) catch {
            _ => compact_yaml_error(path, "Invalid percentage: \{text}")
          }
        None =>
          match text.strip_suffix("fr") {
            Some(number) =>
              Json::object({ "Fr": Json::number(@string.parse_double(number)) }) catch {
                _ => compact_yaml_error(path, "Invalid fraction: \{text}")
              }
            None => compact_yaml_error(path, "Unsupported dimension: \{text}")
          }
      }
    { .. } => value
    _ => compact_yaml_error(path, "Dimension must be a number or unit string")
  }
}

///|
fn compact_available_space(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    Number(_) => value
    String("min-content") | String("MinContent") => Json::string("MinContent")
    String("max-content") | String("MaxContent") => Json::string("MaxContent")
    _ =>
      compact_yaml_error(
        path, "Available space must be a number, min-content, or max-content",
      )
  }
}

///|
fn compact_size(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    Array([width, height]) =>
      Json::object({
        "width": compact_dimension(width, path.add_index(0)),
        "height": compact_dimension(height, path.add_index(1)),
      })
    Object(values) => {
      let result : Map[String, Json] = Map([])
      for key, item in values {
        if key != "width" && key != "height" {
          compact_yaml_error(path.add_key(key), "Unknown size field: \{key}")
        }
        result[key] = compact_dimension(item, path.add_key(key))
      }
      Json::object(result)
    }
    _ => compact_yaml_error(path, "Size must be [width, height] or an object")
  }
}

///|
fn compact_rect(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    Number(_) | String(_) => {
      let dimension = compact_dimension(value, path)
      Json::object({
        "left": dimension,
        "right": dimension,
        "top": dimension,
        "bottom": dimension,
      })
    }
    Array([vertical_value, horizontal_value]) => {
      let vertical = compact_dimension(vertical_value, path.add_index(0))
      let horizontal = compact_dimension(horizontal_value, path.add_index(1))
      Json::object({
        "left": horizontal,
        "right": horizontal,
        "top": vertical,
        "bottom": vertical,
      })
    }
    Array([top, right, bottom, left]) =>
      Json::object({
        "top": compact_dimension(top, path.add_index(0)),
        "right": compact_dimension(right, path.add_index(1)),
        "bottom": compact_dimension(bottom, path.add_index(2)),
        "left": compact_dimension(left, path.add_index(3)),
      })
    Object(values) => {
      let result : Map[String, Json] = Map([])
      for key, item in values {
        if key != "left" && key != "right" && key != "top" && key != "bottom" {
          compact_yaml_error(path.add_key(key), "Unknown edge: \{key}")
        }
        result[key] = compact_dimension(item, path.add_key(key))
      }
      Json::object(result)
    }
    _ =>
      compact_yaml_error(
        path, "Edges must use one, two, or four values, or an object",
      )
  }
}

///|
fn compact_gap(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    Number(_) | String(_) => {
      let dimension = compact_dimension(value, path)
      Json::object({ "width": dimension, "height": dimension })
    }
    Array([row, column]) =>
      Json::object({
        "height": compact_dimension(row, path.add_index(0)),
        "width": compact_dimension(column, path.add_index(1)),
      })
    Object(values) => {
      let result : Map[String, Json] = Map([])
      for key, item in values {
        let canonical_key = match key {
          "x" | "column" | "width" => "width"
          "y" | "row" | "height" => "height"
          _ => compact_yaml_error(path.add_key(key), "Unknown gap axis: \{key}")
        }
        if result.contains(canonical_key) {
          compact_yaml_error(path.add_key(key), "Duplicate gap axis: \{key}")
        }
        result[canonical_key] = compact_dimension(item, path.add_key(key))
      }
      Json::object(result)
    }
    _ => compact_yaml_error(path, "Gap must be a value, pair, or object")
  }
}

///|
fn compact_flow(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  guard value is String(flow) else {
    return compact_yaml_error(path, "Flow must be a string")
  }
  Json::string(
    match flow {
      "row" | "Row" => "Row"
      "column" | "Column" => "Column"
      "row-reverse" | "RowReverse" => "RowReverse"
      "column-reverse" | "ColumnReverse" => "ColumnReverse"
      _ => compact_yaml_error(path, "Unsupported flow: \{flow}")
    },
  )
}

///|
fn compact_align_items(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  guard value is String(align) else {
    return compact_yaml_error(path, "Align must be a string")
  }
  Json::string(
    match align {
      "start" | "Start" => "Start"
      "end" | "End" => "End"
      "flex-start" | "FlexStart" => "FlexStart"
      "flex-end" | "FlexEnd" => "FlexEnd"
      "center" | "Center" => "Center"
      "baseline" | "Baseline" => "Baseline"
      "stretch" | "Stretch" => "Stretch"
      _ => compact_yaml_error(path, "Unsupported alignment: \{align}")
    },
  )
}

///|
fn compact_track_list(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  guard value is Array(values) else {
    return compact_yaml_error(path, "Grid tracks must be an array")
  }
  Json::array(
    values.mapi((index, item) => compact_dimension(item, path.add_index(index))),
  )
}

///|
fn compact_grid_placement(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    Number(number, ..) =>
      Json::object({ "Line": Json::number(number.to_int().to_double()) })
    String("auto") | String("Auto") => Json::string("Auto")
    String(text) =>
      match text.strip_prefix("span ") {
        Some(count) =>
          Json::object({
            "Span": Json::number(@string.parse_int(count).to_double()),
          }) catch {
            _ => compact_yaml_error(path, "Invalid grid span: \{text}")
          }
        None => compact_yaml_error(path, "Unsupported grid placement: \{text}")
      }
    { .. } => value
    _ => compact_yaml_error(path, "Invalid grid placement")
  }
}

///|
fn compact_grid_line(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  guard value is Array([start, end]) else {
    return compact_yaml_error(path, "Grid placement must be [start, end]")
  }
  Json::object({
    "start": compact_grid_placement(start, path.add_index(0)),
    "end": compact_grid_placement(end, path.add_index(1)),
  })
}

///|
fn compact_node_field_allowed(field : String) -> Bool {
  match field {
    "label"
    | "children"
    | "style"
    | "size"
    | "width"
    | "height"
    | "padding"
    | "gap"
    | "flow"
    | "grow"
    | "align"
    | "grid"
    | "place"
    | "absolute" => true
    _ => false
  }
}

///|
fn compact_style_from_node(
  node : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  guard node is Object(fields) else {
    return compact_yaml_error(path, "Node definition must be an object")
  }
  for field, _ in fields {
    if !compact_node_field_allowed(field) {
      compact_yaml_error(path.add_key(field), "Unknown node field: \{field}")
    }
  }
  match node {
    { "style": style, .. } => {
      for field, _ in fields {
        if field != "label" && field != "children" && field != "style" {
          compact_yaml_error(
            path.add_key(field),
            "Compact style fields cannot be mixed with style",
          )
        }
      }
      guard style is { .. } else {
        return compact_yaml_error(
          path.add_key("style"),
          "Style must be an object",
        )
      }
      return style
    }
    _ => ()
  }
  let style : Map[String, Json] = Map([])
  match node {
    { "size": value, "width"? : None, "height"? : None, .. } =>
      style["size"] = compact_size(value, path.add_key("size"))
    { "size": _, .. } =>
      compact_yaml_error(
        path.add_key("size"),
        "Size cannot be combined with width or height",
      )
    { "width"? : width, "height"? : height, .. } => {
      let size : Map[String, Json] = Map([])
      match width {
        Some(value) =>
          size["width"] = compact_dimension(value, path.add_key("width"))
        None => ()
      }
      match height {
        Some(value) =>
          size["height"] = compact_dimension(value, path.add_key("height"))
        None => ()
      }
      if !size.is_empty() {
        style["size"] = Json::object(size)
      }
    }
    _ => abort("unreachable")
  }
  if node is { "padding": value, .. } {
    style["padding"] = compact_rect(value, path.add_key("padding"))
  }
  if node is { "gap": value, .. } {
    style["gap"] = compact_gap(value, path.add_key("gap"))
  }
  if node is { "flow": value, .. } {
    if node is { "grid": _, .. } {
      compact_yaml_error(
        path.add_key("flow"),
        "Flow cannot be combined with grid",
      )
    }
    style["display"] = Json::string("Flex")
    style["flex_direction"] = compact_flow(value, path.add_key("flow"))
  }
  if node is { "grow": value, .. } {
    style["flex_grow"] = value
  }
  if node is { "align": value, .. } {
    style["align_items"] = compact_align_items(value, path.add_key("align"))
  }
  match node {
    { "grid": Object(grid), .. } => {
      style["display"] = Json::string("Grid")
      for field, item in grid {
        match field {
          "columns" =>
            style["grid_template_columns"] = compact_track_list(
              item,
              path.add_key("grid").add_key(field),
            )
          "rows" =>
            style["grid_template_rows"] = compact_track_list(
              item,
              path.add_key("grid").add_key(field),
            )
          _ =>
            compact_yaml_error(
              path.add_key("grid").add_key(field),
              "Unknown grid field: \{field}",
            )
        }
      }
    }
    { "grid": _, .. } =>
      compact_yaml_error(path.add_key("grid"), "Grid must be an object")
    _ => ()
  }
  match node {
    { "place": Object(place), .. } =>
      for field, item in place {
        match field {
          "column" =>
            style["grid_column"] = compact_grid_line(
              item,
              path.add_key("place").add_key(field),
            )
          "row" =>
            style["grid_row"] = compact_grid_line(
              item,
              path.add_key("place").add_key(field),
            )
          _ =>
            compact_yaml_error(
              path.add_key("place").add_key(field),
              "Unknown placement field: \{field}",
            )
        }
      }
    { "place": _, .. } =>
      compact_yaml_error(path.add_key("place"), "Place must be an object")
    _ => ()
  }
  match node {
    { "absolute": true, .. } => style["position"] = Json::string("Absolute")
    { "absolute": { .. } as inset, .. } => {
      style["position"] = Json::string("Absolute")
      style["inset"] = compact_rect(inset, path.add_key("absolute"))
    }
    { "absolute": _, .. } =>
      compact_yaml_error(
        path.add_key("absolute"),
        "Absolute must be true or an inset object",
      )
    _ => ()
  }
  Json::object(style)
}

///|
fn compact_node(
  value : Json,
  path : @json.JsonPath,
) -> Json raise JsonDecodeError {
  match value {
    String(id) =>
      Json::object({
        "id": Json::string(id),
        "label": Json::string(id),
        "style": Json::empty_object(),
        "children": Json::array([]),
      })
    Object(entries) if entries.length() == 1 => {
      let (id, specification) = entries.iter().next().unwrap()
      let definition = match specification {
        Null => Json::empty_object()
        { .. } => specification
        _ =>
          return compact_yaml_error(
            path.add_key(id),
            "Node definition must be an object",
          )
      }
      guard definition
        is { "label"? : label_json, "children"? : children_json, .. } else {
        return compact_yaml_error(
          path.add_key(id),
          "Node definition must be an object",
        )
      }
      let label : String = match label_json {
        Some(value) =>
          @json.from_json(value, path=path.add_key(id).add_key("label"))
        None => id
      }
      let children : Array[Json] = match children_json {
        Some(Array(children)) =>
          children.mapi((index, child) => {
            compact_node(
              child,
              path.add_key(id).add_key("children").add_index(index),
            )
          })
        Some(_) =>
          return compact_yaml_error(
            path.add_key(id).add_key("children"),
            "Children must be an array",
          )
        None => []
      }
      Json::object({
        "id": Json::string(id),
        "label": Json::string(label),
        "style": compact_style_from_node(definition, path.add_key(id)),
        "children": Json::array(children),
      })
    }
    { .. } => compact_yaml_error(path, "A node must contain exactly one id key")
    _ => compact_yaml_error(path, "A node must be a string or object")
  }
}

///|
fn compact_document(
  json : Json,
  path : @json.JsonPath,
) -> LayoutDocument raise JsonDecodeError {
  guard json is { "space"? : space, "layout": layout, .. } else {
    return compact_yaml_error(path, "Compact layout document must be an object")
  }
  guard json is Object(fields) else {
    return compact_yaml_error(path, "Compact layout document must be an object")
  }
  for field, _ in fields {
    if field != "space" && field != "layout" {
      compact_yaml_error(
        path.add_key(field),
        "Unknown document field: \{field}",
      )
    }
  }
  let available_space = match space {
    Some(Array([width, height])) =>
      Json::object({
        "width": compact_available_space(
          width,
          path.add_key("space").add_index(0),
        ),
        "height": compact_available_space(
          height,
          path.add_key("space").add_index(1),
        ),
      })
    Some(_) =>
      return compact_yaml_error(
        path.add_key("space"),
        "Space must be [width, height]",
      )
    None =>
      Json::object({
        "width": Json::string("MaxContent"),
        "height": Json::string("MaxContent"),
      })
  }
  let root = compact_node(layout, path.add_key("layout"))
  LayoutDocument::from_json(
    Json::object({
      "version": Json::number(1.0),
      "available_space": available_space,
      "root": root,
    }),
  )
}

///|
impl @json.FromJson for CompactLayoutDocument with fn from_json(json, path) {
  { document: compact_document(json, path) }
}

///|
pub fn LayoutDocument::from_yaml(source : String) -> LayoutDocument raise {
  let documents = @yaml.Yaml::load_from_string(source)
  if documents.is_empty() || documents[0] is BadValue {
    raise EmptyYamlDocument
  }
  if documents.length() != 1 {
    raise MultipleYamlDocuments(documents.length())
  }
  let json = documents[0].to_json()
  let compact : CompactLayoutDocument = @json.from_json(json)
  compact.document
}

///|
pub fn compute_layout_yaml(input : String) -> String {
  try {
    let document = LayoutDocument::from_yaml(input)
    let result = compute_layout_document(document)
    Json::object({ "ok": Json::boolean(true), "result": result.to_json() }).stringify()
  } catch {
    @yaml.YamlError(mark~, info~) =>
      response_error(
        "parse",
        "line \{mark.line + 1}, column \{mark.col + 1}: \{info}",
      )
    EmptyYamlDocument => response_error("parse", "YAML document is empty")
    MultipleYamlDocuments(count) =>
      response_error("parse", "Expected one YAML document, found \{count}")
    @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")
  }
}