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

// HTML rendering: a GitHub-style split view and a unified view, both with
// intraline highlights taken from the alignment DP's own traceback.

///|
fn esc(s : String) -> String {
  let buf = StringBuilder()
  for c in s {
    match c {
      '&' => buf <+ "&"
      '<' => buf <+ "<"
      '>' => buf <+ ">"
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Render one aligned pair's ops as (left row, right row) HTML, merging
/// adjacent highlighted tokens into single runs.
fn pair_row_html(ops : Array[Op]) -> (String, String) {
  let l = StringBuilder()
  let r = StringBuilder()
  let lrun = StringBuilder()
  let rrun = StringBuilder()
  fn flushes() {
    if !lrun.is_empty() {
      l <+ "\{lrun.to_string()}"
      lrun.reset()
    }
    if !rrun.is_empty() {
      r <+ "\{rrun.to_string()}"
      rrun.reset()
    }
  }

  for op in ops {
    match op {
      OEq(t) => {
        flushes()
        l <+ "\{esc(t.text)}"
        r <+ "\{esc(t.text)}"
      }
      OSub(a, b) => {
        lrun <+ "\{esc(a.text)}"
        rrun <+ "\{esc(b.text)}"
      }
      ODel(a) => lrun <+ "\{esc(a.text)}"
      OIns(b) => rrun <+ "\{esc(b.text)}"
    }
  }
  flushes()
  (l.to_string(), r.to_string())
}

///|
/// Render a replacement block (paired rows and gaps) into the split table.
/// Budget discipline: dimension guards first, then a per-line token cap
/// (which also bounds every budget term, so the counter cannot overflow),
/// then an early-exit cell budget; over budget the whole block renders as
/// PLAIN unpaired rows. Selected pairs additionally get a per-pair traceback
/// guard: an oversized pair stays paired but is rendered without highlights.
fn split_replacement(
  buf : StringBuilder,
  old_lines : ArrayView[String],
  new_lines : ArrayView[String],
) -> Unit {
  fn row(l : String, lc : String, r : String, rc : String) {
    buf <+
      "\{l}\{r}\n"
  }

  let mut fallback = old_lines.length() > 64 || new_lines.length() > 64
  let olds : Array[Array[Tok]] = []
  let news : Array[Array[Tok]] = []
  if !fallback {
    cap~: for l in old_lines {
      let t = tokenize_line(l)
      if t.length() > 1024 {
        fallback = true
        break cap~
      }
      olds.push(t)
    }
  }
  if !fallback {
    cap~: for l in new_lines {
      let t = tokenize_line(l)
      if t.length() > 1024 {
        fallback = true
        break cap~
      }
      news.push(t)
    }
  }
  if !fallback {
    let mut work = 0
    budget~: for ta in olds {
      for tb in news {
        work += (ta.length() + 1) * (tb.length() + 1)
        if work > 2_000_000 {
          fallback = true
          break budget~
        }
      }
    }
  }
  if fallback {
    for l in old_lines {
      row(esc(l), "del", "", "empty")
    }
    for l in new_lines {
      row("", "empty", esc(l), "add")
    }
  } else {
    for pair in align(olds, news) {
      match pair {
        (Some(ii), Some(jj)) =>
          if (olds[ii].length() + 1) * (news[jj].length() + 1) > 262144 {
            row(esc(old_lines[ii]), "del", esc(new_lines[jj]), "add")
          } else {
            let (lh, rh) = pair_row_html(pair_ops(olds[ii], news[jj]))
            row(lh, "del", rh, "add")
          }
        (Some(ii), None) => row(esc(old_lines[ii]), "del", "", "empty")
        (None, Some(jj)) => row("", "empty", esc(new_lines[jj]), "add")
        (None, None) => ()
      }
    }
  }
}

///|
/// GitHub-style split (side-by-side) view of a diff, as an HTML ``.
/// Equal lines appear on both sides; replacement blocks are aligned by
/// weighted similarity and their paired rows carry word-level highlights;
/// unpaired lines leave the other cell empty. Style it with `html_page` or
/// your own CSS (classes: split, hunk-header, ctx, del, add, empty, wd, wa).
pub fn side_by_side_html(
  old~ : ArrayView[String],
  new~ : ArrayView[String],
  context? : Int = 3,
) -> String {
  let buf = StringBuilder()
  buf <+ "
\n" for h in @diff.Diff(old~, new~).group(context~) { buf <+ "\n" let edits = h.edits() let o = h.old_view() let n = h.new_view() let mut i = 0 while i < edits.length() { match (edits[i], edits.get(i + 1)) { ( Delete(old_index~, old_len~, ..), Some(Insert(new_index~, new_len~, ..)), ) => { split_replacement( buf, o.view(start=old_index, end=old_index + old_len), n.view(start=new_index, end=new_index + new_len), ) i += 2 } (Equal(old_index~, len~, ..), _) => { for l in o.view(start=old_index, end=old_index + len) { buf <+ "\n" } i += 1 } (Delete(old_index~, old_len~, ..), _) => { for l in o.view(start=old_index, end=old_index + old_len) { buf <+ "\n" } i += 1 } (Insert(new_index~, new_len~, ..), _) => { for l in n.view(start=new_index, end=new_index + new_len) { buf <+ "\n" } i += 1 } } } } buf <+ "
\{esc(h.header())}
\{esc(l)}\{esc(l)}
\{esc(l)}
\{esc(l)}
\n" buf.to_string() } ///| /// Wrap rendered diff HTML in a complete standalone page with default /// styling (light red/green rows, deeper word-level highlights). pub fn html_page(title~ : String, body : String) -> String { let buf = StringBuilder() buf <+ "\n" buf <+ "\{esc(title)}\n" buf <+ "\{body}" buf <+ "\n" buf.to_string() } ///| /// Unified (single-column) view of a diff: hunk headers followed by /// ` `/`-`/`+`-prefixed lines, with the same weighted alignment and /// word-level highlights as the split view — deletions of a replacement /// block first, then its insertions. Wrap the result in `
` or use
/// `html_page`.
pub fn unified_html(
  old~ : ArrayView[String],
  new~ : ArrayView[String],
  context? : Int = 3,
) -> String {
  let buf = StringBuilder()
  fn line(cls : String, prefix : String, body : String) {
    buf <+ "\{prefix}\{body}\n"
  }

  buf <+ "
\n"
  for h in @diff.Diff(old~, new~).group(context~) {
    buf <+ "\{esc(h.header())}\n"
    let edits = h.edits()
    let o = h.old_view()
    let n = h.new_view()
    let mut i = 0
    while i < edits.length() {
      match (edits[i], edits.get(i + 1)) {
        (
          Delete(old_index~, old_len~, ..),
          Some(Insert(new_index~, new_len~, ..)),
        ) => {
          let old_lines = o.view(start=old_index, end=old_index + old_len)
          let new_lines = n.view(start=new_index, end=new_index + new_len)
          let mut fallback = old_lines.length() > 64 || new_lines.length() > 64
          let olds : Array[Array[Tok]] = []
          let news : Array[Array[Tok]] = []
          if !fallback {
            cap~: for l in old_lines {
              let t = tokenize_line(l)
              if t.length() > 1024 {
                fallback = true
                break cap~
              }
              olds.push(t)
            }
          }
          if !fallback {
            cap~: for l in new_lines {
              let t = tokenize_line(l)
              if t.length() > 1024 {
                fallback = true
                break cap~
              }
              news.push(t)
            }
          }
          if !fallback {
            let mut work = 0
            budget~: for ta in olds {
              for tb in news {
                work += (ta.length() + 1) * (tb.length() + 1)
                if work > 2_000_000 {
                  fallback = true
                  break budget~
                }
              }
            }
          }
          if fallback {
            for l in old_lines {
              line("del", "-", esc(l))
            }
            for l in new_lines {
              line("add", "+", esc(l))
            }
          } else {
            let pairs = align(olds, news)
            let rights : Array[String] = []
            for pair in pairs {
              match pair {
                (Some(ii), Some(jj)) =>
                  if (olds[ii].length() + 1) * (news[jj].length() + 1) > 262144 {
                    line("del", "-", esc(old_lines[ii]))
                    rights.push(esc(new_lines[jj]))
                  } else {
                    let (lh, rh) = pair_row_html(pair_ops(olds[ii], news[jj]))
                    line("del", "-", lh)
                    rights.push(rh)
                  }
                (Some(ii), None) => line("del", "-", esc(old_lines[ii]))
                (None, Some(jj)) => rights.push(esc(new_lines[jj]))
                (None, None) => ()
              }
            }
            for r in rights {
              line("add", "+", r)
            }
          }
          i += 2
        }
        (Equal(old_index~, len~, ..), _) => {
          for l in o.view(start=old_index, end=old_index + len) {
            line("ctx", " ", esc(l))
          }
          i += 1
        }
        (Delete(old_index~, old_len~, ..), _) => {
          for l in o.view(start=old_index, end=old_index + old_len) {
            line("del", "-", esc(l))
          }
          i += 1
        }
        (Insert(new_index~, new_len~, ..), _) => {
          for l in n.view(start=new_index, end=new_index + new_len) {
            line("add", "+", esc(l))
          }
          i += 1
        }
      }
    }
  }
  buf <+ "
\n" buf.to_string() }