/// 项目持久化:MoonViz Studio 文档格式(.mvz.json)。
///
/// 格式 = 多画板文档集合,每画板内嵌 Document::to_json 树。
/// 与 .mbt.md(声明文本)互为投影:save 存结构真相,export-decl 存声明视图。

///|
/// 整个项目序列化为 JSON(save 用)
pub fn Project::to_project_json(self : Project) -> String {
  let sb = StringBuilder::new()
  sb.write_string("{\"moonviz\":\"studio\",\"version\":2,\"artboards\":[")
  let mut first = true
  for id in self.artboards.keys() {
    match self.artboards.get(id) {
      Some(ab) => {
        if !first {
          sb.write_string(",")
        }
        first = false
        sb.write_string(
          "{\"id\":\"\{json_escape(ab.id)}\",\"name\":\"\{json_escape(ab.name)}\",\"width\":\{ab.width},\"height\":\{ab.height},\"doc\":\{ab.doc.to_json()}}",
        )
      }
      None => ()
    }
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
/// 从项目 JSON 重建 Project(替换当前内容;load 用)
///
/// 事务模式(对齐 replace_with):先全部解析进暂存表,全部成功才原子替换
/// 现有画板——单板失败不再被静默吞掉,也不会出现「项目已被清空才报错」
/// 的失败半态;任何 LoadErr 都汇总进 Err,现状保持不变。
pub fn Project::from_project_json(
  self : Project,
  text : String,
) -> Result[Unit, String] {
  // 逐画板定位 "doc":{...} 子对象并解析
  let art_marker = "\"artboards\":["
  let arr_start = match text.find(art_marker) {
    Some(i) => i + art_marker.length()
    None => return Err("missing_artboards")
  }
  let staged : Map[String, ArtboardMeta] = Map::new()
  let errors : Array[String] = []
  let body = text.substring(start=arr_start)
  let mut pos = 0
  let n = body.length()
  while pos < n {
    let obj_start = find_obj_start(body, pos)
    match obj_start {
      -1 => break
      os => {
        let end = skip_object2(body, os)
        if end <= os {
          break
        }
        let obj = body.substring(start=os, end~)
        match load_one_artboard(obj) {
          LoadOk(id, meta) => staged.set(id, meta)
          LoadErr(e) => errors.push(e)
        }
        pos = end
      }
    }
  }
  if errors.length() > 0 {
    Err("artboard_load_failed: " + jstr8(errors, "; "))
  } else if staged.size() == 0 {
    Err("no_artboards_loaded")
  } else {
    // 全部成功:原子替换
    let ids : Array[String] = []
    for k in self.artboards.keys() {
      ids.push(k)
    }
    for k in ids.iter() {
      self.artboards.remove(k)
    }
    for id in staged.keys() {
      match staged.get(id) {
        Some(meta) => self.artboards.set(id, meta)
        None => ()
      }
    }
    self.revision = self.revision + 1
    Ok(())
  }
}

///|
enum LoadOutcome {
  LoadOk(String, ArtboardMeta)
  LoadErr(String)
}

///|
fn load_one_artboard(obj : String) -> LoadOutcome {
  let id = jstr_field(obj, "id")
  let name = jstr_field(obj, "name")
  let w = jnum_field(obj, "width")
  let h = jnum_field(obj, "height")
  if id == "" {
    return LoadErr("missing_id")
  }
  // 提取 "doc":{...}
  let dm = "\"doc\":"
  let dpos = match obj.find(dm) {
    Some(i) => i + dm.length()
    None => return LoadErr("missing_doc")
  }
  let dstart = match find_obj_from(obj, dpos) {
    Some(i) => i
    None => return LoadErr("missing_doc_obj")
  }
  let dend = skip_object2(obj, dstart)
  let doc_json = obj.substring(start=dstart, end=dend)
  match Document::from_json(doc_json) {
    Ok(doc) => LoadOk(id, { id, name, width: w, height: h, doc, })
    Err(e) => LoadErr(e)
  }
}

///|
/// 提取 "key":"value" 的 value(已反转义)
fn jstr_field(obj : String, key : String) -> String {
  let pattern = "\"" + key + "\":\""
  match obj.find(pattern) {
    Some(i) => {
      let rest = obj.substring(start=i + pattern.length())
      match rest.find("\"") {
        Some(e) => unescape_json(rest.substring(start=0, end=e))
        None => ""
      }
    }
    None => ""
  }
}

///|
/// 提取 "key":N 数值
fn jnum_field(obj : String, key : String) -> Double {
  let pattern = "\"" + key + "\":"
  match obj.find(pattern) {
    Some(i) => {
      let rest = obj.substring(start=i + pattern.length())
      let mut num = 0.0
      let mut frac = 0.1
      let mut dot = false
      for c in rest.iter() {
        let code = c.to_int()
        if code >= 48 && code <= 57 {
          let d = (code - 48).to_double()
          if dot {
            num = num + d * frac
            frac = frac * 0.1
          } else {
            num = num * 10.0 + d
          }
        } else if code == 46 && !dot {
          dot = true
        } else {
          break
        }
      }
      num
    }
    None => 0.0
  }
}

///|
/// 从 from 起(含)找第一个 '{'
fn find_obj_from(s : String, from : Int) -> Int? {
  let n = s.length()
  let mut i = from
  while i < n {
    if char_at2(s, i) == "{" {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn find_obj_start(s : String, from : Int) -> Int {
  match find_obj_from(s, from) {
    Some(i) => i
    None => -1
  }
}

///|
/// 跳过完整对象文本(含字符串内转义),返回结束下标(不含)
fn skip_object2(s : String, start : Int) -> Int {
  let mut depth = 0
  let mut pos = start
  let mut in_str = false
  let n = s.length()
  while pos < n {
    let c = char_at2(s, pos)
    if in_str {
      if c == "\\" {
        pos = pos + 2
        continue
      }
      if c == "\"" {
        in_str = false
      }
    } else if c == "\"" {
      in_str = true
    } else if c == "{" {
      depth = depth + 1
    } else if c == "}" {
      depth = depth - 1
      if depth == 0 {
        return pos + 1
      }
    }
    pos = pos + 1
  }
  n
}

///|
/// 画板复制(Studio 的 ⌘D):深拷贝文档并重命名
pub fn Project::duplicate_artboard(
  self : Project,
  src~ : String,
  new_name~ : String,
) -> Result[String, String] {
  match self.artboards.get(src) {
    None => Err("unknown_artboard: \{src}")
    Some(ab) => {
      if self.artboards.contains(new_name) {
        return Err("artboard_exists: \{new_name}")
      }
      let copied = ab.doc.clone()
      let copy : ArtboardMeta = {
        id: new_name,
        name: new_name,
        width: ab.width,
        height: ab.height,
        doc: copied,
      }
      self.artboards.set(new_name, copy)
      self.revision = self.revision + 1
      Ok("{\"ok\":true,\"artboard\":\"\{new_name}\",\"from\":\"\{src}\"}")
    }
  }
}