;;; dag-draw-render.el --- Graph rendering for dag-draw -*- lexical-binding: t -*-

;; Copyright (C) 2024, 2025

;; Author: Generated by Claude
;; Keywords: internal

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;;; Commentary:

;; Implementation of graph rendering in multiple formats: SVG, ASCII art, and DOT.
;; This module takes positioned graphs from the layout algorithm and converts them
;; into visual representations with nodes, edges, and labels.

;;; Code:

(require 'dash)
(require 'ht)
(require 'dag-draw)
(require 'dag-draw-core)
(require 'dag-draw-pass4-splines)
(require 'dag-draw-svg)
(require 'dag-draw-dot)
(require 'dag-draw-ascii-grid)
(require 'dag-draw-pass3-positioning)

;;; Customization

(defgroup dag-draw-render nil
  "Rendering options for dag-draw graphs."
  :group 'dag-draw
  :prefix "dag-draw-render-")

(defcustom dag-draw-render-svg-node-fill "#f0f0f0"
  "Default fill color for SVG nodes."
  :type 'string
  :group 'dag-draw-render)

(defcustom dag-draw-render-svg-node-stroke "#000000"
  "Default stroke color for SVG nodes."
  :type 'string
  :group 'dag-draw-render)

(defcustom dag-draw-render-svg-edge-stroke "#666666"
  "Default stroke color for SVG edges."
  :type 'string
  :group 'dag-draw-render)

;; PHASE 1 FIX: Removed dag-draw-render-ascii-grid-scale - use dag-draw-ascii-coordinate-scale only
;; COORDINATE SYSTEM FIX: Scale factors unified in dag-draw-ascii-grid.el

;;; Helper Functions


;;; Main Rendering Functions

(defun dag-draw-render-ascii (graph &optional selected)
  "Render GRAPH as ASCII art using GKNV-compliant coordinate conversion.

GRAPH is a `dag-draw-graph' structure that must have positioned nodes.

SELECTED is an optional node ID (symbol) to render with selection highlighting.
Selected nodes are drawn with double-line box characters (╔╗╚╝═║) instead
of single-line characters (┌┐└┘─│).

This function respects the GKNV 4-pass algorithm and performs only
coordinate conversion from world coordinates to ASCII grid coordinates.
If GRAPH nodes are not yet positioned, runs the layout algorithm first.

Returns a string containing the ASCII representation of the graph with
box-drawing characters for nodes and edges."
  ;; GKNV COMPLIANCE: Ensure positioning has completed
  ;; ASCII-first mode doesn't require splines, just positioned nodes
  (unless (and (> (ht-size (dag-draw-graph-nodes graph)) 0)
               ;; Check if ALL nodes have coordinates (not just first one)
               (cl-every (lambda (node) (dag-draw-node-x-coord node))
                         (ht-values (dag-draw-graph-nodes graph))))
    ;; Run GKNV algorithm ONCE if not already done
    (dag-draw-layout-graph graph))

  ;; Handle empty graphs
  (if (= (ht-size (dag-draw-graph-nodes graph)) 0)
      "(Empty Graph)"

    ;; ASCII-first mode: coordinates are already in grid units, no conversion needed
    (dag-draw--render-ascii-native graph selected)))

(defun dag-draw--render-ascii-native (graph &optional selected)
  "Render GRAPH with ASCII-native coordinates directly from GKNV positioning.

GRAPH is a `dag-draw-graph' structure with nodes positioned in grid units.

SELECTED is an optional node ID (symbol) to render with selection highlighting.

Coordinates from the GKNV algorithm are already in grid units, so no
scale conversion is needed.  Creates an ASCII grid, draws nodes and edges,
applies junction character enhancement, and converts to string.

Returns a string containing the ASCII representation of the graph."

  (when dag-draw-debug-output
    (message "DEBUG: Starting dag-draw--render-ascii-native - SOURCE FILE VERSION"))

  ;; Calculate grid bounds directly from ASCII coordinates
  (let* ((nodes (ht-values (dag-draw-graph-nodes graph)))
         (min-x (apply #'min (mapcar (lambda (n) (or (dag-draw-node-x-coord n) 0)) nodes)))
         (max-x (apply #'max (mapcar (lambda (n) (+ (or (dag-draw-node-x-coord n) 0)
                                                     (length (dag-draw-node-label n)))) nodes)))
         (min-y (apply #'min (mapcar (lambda (n) (or (dag-draw-node-y-coord n) 0)) nodes)))
         (max-y (apply #'max (mapcar (lambda (n) (+ (or (dag-draw-node-y-coord n) 0) 3)) nodes)))

         ;; Create grid with padding (ensure integers for make-vector)
         (grid-width (round (max 20 (+ (- max-x min-x) 10))))
         (grid-height (round (max 10 (+ (- max-y min-y) 5))))
         (grid (dag-draw--create-ascii-grid grid-width grid-height)))

    (when dag-draw-debug-output
      (message "\n=== ASCII-NATIVE RENDERING ===")
      (message "Grid bounds: (%d,%d) to (%d,%d)" min-x min-y max-x max-y)
      (message "Grid size: %dx%d" grid-width grid-height))

    ;; Draw nodes directly using ASCII coordinates (ensure integers)
    ;; Collect node boundary positions to exclude from junction enhancement
    (let ((node-boundaries nil)
          (node-rects (ht-create)))
      (when dag-draw-debug-output
        (message "DEBUG: About to loop over %d nodes" (length nodes)))
      (dolist (node nodes)
        (when dag-draw-debug-output
          (message "DEBUG: Processing node: %s" (dag-draw-node-label node)))
        (let* ((node-id (dag-draw-node-id node))
               (node-selected-p (or (and selected (eq node-id selected))
                                    (ht-get (dag-draw-node-attributes node) :ascii-highlight)))
               (x (round (- (or (dag-draw-node-x-coord node) 0) min-x)))
               (y (round (- (or (dag-draw-node-y-coord node) 0) min-y)))
               (marker (ht-get (dag-draw-node-attributes node) :ascii-marker))
               (base-label (dag-draw-node-label node))
               (label (if marker (concat marker base-label) base-label))
               ;; Use node's calculated dimensions if layout has run (scales to ASCII),
               ;; otherwise fall back to calculating from label for pre-layout rendering
               (layout-run-p (dag-draw-graph-max-rank graph))
               (width (if layout-run-p
                          (round (dag-draw-node-x-size node))
                        (+ (string-width label) 4)))
               (height (if layout-run-p
                           (round (dag-draw-node-y-size node))
                         3)))
          ;; Record this node's rectangle for edge obstacle-avoidance routing
          (ht-set! node-rects node-id (list x y width height node-selected-p))
          ;; Draw node and collect its boundary positions
          (when dag-draw-debug-output
            (message "DEBUG: About to call dag-draw--draw-node-box for %s at (%d,%d) size %dx%d selected=%s"
                     label x y width height node-selected-p))
          (let ((boundaries (dag-draw--draw-node-box grid x y width height label node-selected-p)))
            (when dag-draw-debug-output
              (message "DEBUG: Got %d boundaries back, first few: %S"
                       (length boundaries)
                       (seq-take boundaries 5)))
            (setq node-boundaries (append node-boundaries boundaries)))))

      ;; Draw edges with obstacle-avoidance routing through the grid
      (let ((grid-width (if (> (length grid) 0) (length (aref grid 0)) 0)))
        (dolist (edge (dag-draw-graph-edges graph))
          (let* ((nodes (dag-draw--edge-nodes graph edge))
                 (from-id (dag-draw-node-id (car nodes)))
                 (to-id (dag-draw-node-id (cdr nodes)))
                 (from-rect (ht-get node-rects from-id))
                 (to-rect (ht-get node-rects to-id)))
            (when (and from-rect to-rect)
              (let ((obstacles (delq nil (ht-map (lambda (id r)
                                                   (unless (or (eq id from-id) (eq id to-id)) r))
                                                 node-rects))))
                (when dag-draw-debug-output
                  (message "DEBUG: Routing edge %s -> %s around %d obstacles"
                           from-id to-id (length obstacles)))
                (dag-draw--route-edge grid from-rect to-rect obstacles grid-width))))))

      ;; CLAUDE.md: Apply junction characters after routing, before final output
      ;; "walks the edge in order to determine the locally-relevant algorithm"
      ;; FIX: Exclude node boundaries from junction enhancement to prevent corruption
      (when dag-draw-debug-output
        (message "DEBUG: Total node boundaries collected: %d" (length node-boundaries))
        (when (< (length node-boundaries) 15)
          (message "DEBUG: First few boundaries: %S" (seq-take node-boundaries 10))))
      (dag-draw--apply-junction-chars-to-grid grid node-boundaries))

    ;; Convert grid to string
    (dag-draw--ascii-grid-to-string grid)))





(defun dag-draw--get-box-chars (selected-p)
  "Return box-drawing characters based on SELECTED-P.

If SELECTED-P is non-nil, returns double-line box characters.
Otherwise returns single-line box characters.

Returns a plist with keys:
  :top-left :top-right :bottom-left :bottom-right
  :horizontal :vertical"
  (if selected-p
      (list :top-left ?╔
            :top-right ?╗
            :bottom-left ?╚
            :bottom-right ?╝
            :horizontal ?═
            :vertical ?║)
    (list :top-left ?┌
          :top-right ?┐
          :bottom-left ?└
          :bottom-right ?┘
          :horizontal ?─
          :vertical ?│)))

(defun dag-draw--draw-node-box (grid x y width height label &optional selected-p)
  "Draw a node box with LABEL at specified grid position.

GRID is a 2D vector representing the ASCII character grid (modified in place).
X and Y are integers representing the top-left corner position.
WIDTH and HEIGHT are integers representing box dimensions in characters.
LABEL is a string (may contain newlines for multiline text).
SELECTED-P is an optional boolean indicating if this node is selected.

Draws box-drawing characters to create a bordered box.
If SELECTED-P is non-nil, uses double-line characters (╔ ╗ ╚ ╝ ═ ║).
Otherwise uses single-line characters (┌ ┐ └ ┘ ─ │).

Returns a list of (x . y) cons cells representing node boundary positions,
used later to exclude these positions from junction character enhancement."
  (let ((grid-height (length grid))
        (grid-width (if (> (length grid) 0) (length (aref grid 0)) 0))
        (boundaries nil)
        (chars (dag-draw--get-box-chars selected-p)))

    (when dag-draw-debug-output
      (message "DEBUG draw-node-box: x=%d y=%d width=%d height=%d grid=%dx%d selected=%s"
               x y width height grid-width grid-height selected-p))

    ;; Only draw if within grid bounds
    (when (and (>= x 0) (>= y 0)
               (< (+ x width) grid-width)
               (< (+ y height) grid-height))

      ;; Draw top border and record boundary positions
      (dotimes (i width)
        (let ((pos-x (+ x i)))
          (dag-draw--set-char grid pos-x y
                              (cond ((= i 0) (plist-get chars :top-left))
                                    ((= i (1- width)) (plist-get chars :top-right))
                                    (t (plist-get chars :horizontal))))
          (push (cons pos-x y) boundaries)))

      ;; Draw middle rows with label (supports multiline text)
      ;; Uses display width tracking for proper CJK/Unicode support
      (let ((label-lines (split-string label "\n"))
            (interior-display-width (- width 2)))  ; Interior width in display columns
        (dotimes (row (- height 2))
          (let* ((actual-row (+ y row 1))
                 (current-line (if (< row (length label-lines))
                                   (nth row label-lines)
                                 ""))
                 ;; Calculate padding based on display width for consistent visual appearance
                 (text-display-width (string-width current-line))
                 (total-padding (- interior-display-width text-display-width))
                 ;; Center text: split padding between left and right
                 (left-padding (/ total-padding 2))
                 (right-padding (- total-padding left-padding))
                 ;; Build content: left-padding + text + right-padding
                 (padded-content (concat (make-string (max 0 left-padding) ?\s)
                                         current-line
                                         (make-string (max 0 right-padding) ?\s)))
                 (char-pos (1+ x))
                 (display-col 0))
            ;; Left border
            (dag-draw--set-char grid x actual-row (plist-get chars :vertical))
            (push (cons x actual-row) boundaries)
            ;; Place content characters, stopping when interior display width is filled
            (dotimes (i (length padded-content))
              (when (< display-col interior-display-width)
                (let ((char (aref padded-content i)))
                  (dag-draw--set-char grid char-pos actual-row char)
                  (setq char-pos (1+ char-pos))
                  (setq display-col (+ display-col (char-width char))))))
            ;; Right border - place immediately after content (at char-pos, not fixed position)
            ;; This ensures visual alignment regardless of CJK character widths
            (dag-draw--set-char grid char-pos actual-row (plist-get chars :vertical))
            (push (cons char-pos actual-row) boundaries))))

      ;; Draw bottom border and record boundary positions
      (dotimes (i width)
        (let ((pos-x (+ x i))
              (pos-y (+ y height -1)))
          (dag-draw--set-char grid pos-x pos-y
                              (cond ((= i 0) (plist-get chars :bottom-left))
                                    ((= i (1- width)) (plist-get chars :bottom-right))
                                    (t (plist-get chars :horizontal))))
          (push (cons pos-x pos-y) boundaries))))

    ;; Return list of boundary positions
    (when dag-draw-debug-output
      (message "DEBUG draw-node-box returning: %d boundaries" (length boundaries)))
    boundaries))



;; GKNV-Compliant Rendering - No Hollow Routing Workarounds Needed
;; Splines are now pre-clipped to boundaries, enabling simple line drawing

(defun dag-draw--set-char (grid x y char)
  "Safely set character in GRID at position (X,Y) to CHAR.

GRID is a 2D vector representing the ASCII character grid (modified in place).
X and Y are integers representing grid coordinates.
CHAR is a character to place at the position.

Only sets the character if the position is within grid bounds.
No bounds checking error is raised for out-of-bounds positions."
  (let ((grid-height (length grid))
        (grid-width (if (> (length grid) 0) (length (aref grid 0)) 0)))
    (when (and (>= x 0) (< x grid-width) (>= y 0) (< y grid-height))
      (aset (aref grid y) x char))))


;;; SVG and DOT rendering functions are provided by their respective modules

;;; Enhanced spline segment drawing functions for Phase C improvements

(defun dag-draw--rect-blocks-column-p (rect col y1 y2)
  "Non-nil if node RECT occupies column COL in any row of [Y1,Y2].
RECT is (X Y WIDTH HEIGHT) in grid coordinates."
  (let ((rx (nth 0 rect)) (ry (nth 1 rect))
        (rw (nth 2 rect)) (rh (nth 3 rect)))
    (and (>= col rx) (< col (+ rx rw))
         (< ry (1+ y2)) (> (+ ry rh) y1))))

(defun dag-draw--column-clear-p (col y1 y2 obstacles)
  "Non-nil if no rect in OBSTACLES blocks column COL across rows [Y1,Y2]."
  (not (cl-some (lambda (r) (dag-draw--rect-blocks-column-p r col y1 y2)) obstacles)))

(defun dag-draw--find-clear-column (preferred y1 y2 obstacles grid-width)
  "Return a column near PREFERRED that is clear of OBSTACLES across [Y1,Y2].
Searches outward from PREFERRED, staying within GRID-WIDTH.  Falls back to
PREFERRED if nothing clear is found."
  (or (and (dag-draw--column-clear-p preferred y1 y2 obstacles) preferred)
      (cl-loop for d from 1 to grid-width
               for right = (+ preferred d)
               for left = (- preferred d)
               thereis (cond ((and (< right grid-width)
                                   (dag-draw--column-clear-p right y1 y2 obstacles))
                              right)
                             ((and (>= left 0)
                                   (dag-draw--column-clear-p left y1 y2 obstacles))
                              left)))
      preferred))

(defun dag-draw--draw-h-run (grid x1 x2 y)
  "Draw a horizontal run of ─ in GRID on row Y between columns X1 and X2."
  (let ((a (min x1 x2)) (b (max x1 x2)))
    (while (<= a b)
      (dag-draw--set-grid-char grid a y ?─)
      (setq a (1+ a)))))

(defun dag-draw--draw-v-run (grid x y1 y2)
  "Draw a vertical run of │ in GRID on column X between rows Y1 and Y2."
  (let ((a (min y1 y2)) (b (max y1 y2)))
    (while (<= a b)
      (dag-draw--set-grid-char grid x a ?│)
      (setq a (1+ a)))))

(defun dag-draw--route-edge (grid from-rect to-rect obstacles grid-width)
  "Draw an orthogonal edge from FROM-RECT to TO-RECT in GRID, dodging OBSTACLES.

FROM-RECT and TO-RECT are (X Y WIDTH HEIGHT &optional SELECTED-P) node
rectangles in grid coordinates.  OBSTACLES is a list of the other node
rectangles to route around.  GRID-WIDTH bounds the detour search.

The edge leaves the bottom-center port of FROM-RECT and arrives at the
top-center port of TO-RECT.  When the source column is blocked by an
intervening node box, the vertical trunk is shifted to the nearest clear
column.  A downward arrow (▼) is placed at the destination port and a
T-junction is written on the source's bottom border at the port — the
double-line ╦ when the source node is selected/highlighted, else ┬."
  (let* ((fx (nth 0 from-rect)) (fy (nth 1 from-rect))
         (fw (nth 2 from-rect)) (fh (nth 3 from-rect))
         (from-selected-p (nth 4 from-rect))
         (tx (nth 0 to-rect)) (ty (nth 1 to-rect)) (tw (nth 2 to-rect))
         (from-port-x (+ fx (/ fw 2)))
         (from-bottom (+ fy fh -1))          ; source bottom border row
         (to-port-x (+ tx (/ tw 2)))
         (start-y (1+ from-bottom))           ; first edge row below source
         (arrow-y ty)                         ; destination top border row
         (trunk-end (1- arrow-y)))            ; last vertical row before arrow
    (if (>= start-y arrow-y)
        ;; Degenerate vertical gap: just point the arrow.
        (dag-draw--set-grid-char grid to-port-x arrow-y ?▼)
      (let* ((trunk (dag-draw--find-clear-column
                     from-port-x start-y trunk-end obstacles grid-width))
             ;; The arrow must land on the INTERIOR of the destination's top
             ;; border (never on a corner).  Enter straight when the trunk is
             ;; already interior; otherwise jog across to the centre port.
             (lo (1+ tx))
             (hi (- (+ tx tw) 2))
             (arrow-x (if (and (<= lo hi) (>= trunk lo) (<= trunk hi))
                          trunk
                        (max lo (min hi to-port-x)))))
        ;; horizontal from source port across to the trunk column (if offset)
        (when (/= from-port-x trunk)
          (dag-draw--draw-h-run grid from-port-x trunk start-y))
        ;; vertical trunk down to just above the destination
        (dag-draw--draw-v-run grid trunk start-y trunk-end)
        ;; horizontal from trunk across to the destination port (if offset)
        (when (/= trunk arrow-x)
          (dag-draw--draw-h-run grid trunk arrow-x trunk-end))
        ;; arrow into the destination, T-junction out of the source
        (dag-draw--set-grid-char grid arrow-x arrow-y ?▼)
        (dag-draw--set-grid-char grid from-port-x from-bottom
                                 (if from-selected-p ?╦ ?┬))))))

(defun dag-draw--set-grid-char (grid x y char)
  "Set character at position (X,Y) in GRID to CHAR with bounds checking.

GRID is a 2D vector representing the ASCII character grid (modified in place).
X and Y are integers representing grid coordinates.
CHAR is a character to place at the position.

Only sets the character if the position is within grid bounds."
  (when (and (>= x 0) (< x (length (aref grid 0)))
             (>= y 0) (< y (length grid)))
    (aset (aref grid y) x char)))

(provide 'dag-draw-render)

;;; dag-draw-render.el ends here
