;;; 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-ports)
(require 'dag-draw-ascii-nodes)
(require 'dag-draw-ascii-edges)
(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))
      (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)))
          ;; 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 directly using ASCII coordinates (ensure integers)
      (dolist (edge (dag-draw-graph-edges graph))
        (let* ((nodes (dag-draw--edge-nodes graph edge))
               (from-node (car nodes))
               (to-node (cdr nodes))
               (from-x (round (- (or (dag-draw-node-x-coord from-node) 0) min-x)))
               (from-y (round (- (or (dag-draw-node-y-coord from-node) 0) min-y)))
               (to-x (round (- (or (dag-draw-node-x-coord to-node) 0) min-x)))
               (to-y (round (- (or (dag-draw-node-y-coord to-node) 0) min-y))))
          (when dag-draw-debug-output
            (message "DEBUG: Drawing edge from (%d,%d) to (%d,%d)" from-x from-y to-x to-y))
          (dag-draw--draw-simple-edge grid from-x from-y to-x to-y from-node to-node)))

      ;; 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)))

;; DELETED: dag-draw--convert-gknv-to-ascii-grid - obsolete in ASCII-first architecture

;; DELETED: dag-draw--draw-nodes-gknv-compliant - obsolete in ASCII-first architecture

(defun dag-draw--avoid-ascii-collision (x y width height drawn-nodes)
  "Adjust node position to avoid collision with already drawn nodes.

X and Y are integers representing the proposed grid position.
WIDTH and HEIGHT are integers representing node dimensions in grid units.
DRAWN-NODES is a list of rectangles, each (x1 y1 x2 y2) representing
already-placed nodes.

Returns a list (adjusted-x adjusted-y) where adjusted-x and adjusted-y
are integers representing a non-overlapping grid position."

  (let ((current-rect (list x y (+ x width -1) (+ y height -1)))
        (min-separation 3))  ; Minimum 3-character separation between nodes

    ;; Check for collisions with already drawn nodes
    (dolist (drawn-rect drawn-nodes)
      (when (dag-draw--ascii-rectangles-overlap current-rect drawn-rect)
        ;; Collision detected - move to the right with separation
        (let ((collision-right (nth 2 drawn-rect)))
          (setq x (+ collision-right min-separation))
          (setq current-rect (list x y (+ x width -1) (+ y height -1))))))

    (list x y)))

(defun dag-draw--ascii-rectangles-overlap (rect1 rect2)
  "Check if two rectangles overlap in ASCII grid space.

RECT1 and RECT2 are lists of the form (left top right bottom) where
all coordinates are integers in grid space.

Returns t if the rectangles overlap, nil otherwise."
  (let ((x1-left (nth 0 rect1)) (y1-top (nth 1 rect1))
        (x1-right (nth 2 rect1)) (y1-bottom (nth 3 rect1))
        (x2-left (nth 0 rect2)) (y2-top (nth 1 rect2))
        (x2-right (nth 2 rect2)) (y2-bottom (nth 3 rect2)))

    ;; Rectangles overlap if they overlap in both X and Y dimensions
    (and (<= x1-left x2-right) (<= x2-left x1-right)
         (<= y1-top y2-bottom) (<= y2-top y1-bottom))))


;; DELETED: dag-draw--draw-edges-gknv-compliant - obsolete in ASCII-first architecture

;; DELETED: dag-draw--draw-edge-with-proper-ports - obsolete in ASCII-first architecture

(defun dag-draw--calculate-boundary-port (center-x center-y width height side)
  "Calculate port position on node boundary for given SIDE.

CENTER-X and CENTER-Y are numbers representing node center in grid units.
WIDTH and HEIGHT are numbers representing node dimensions in grid units.
SIDE is a symbol: one of `top', `bottom', `left', or `right'.

Returns a list (x y) where x and y are integers representing the port
position on the specified side of the node boundary."
  (let ((left (round (- center-x (/ width 2))))
        (right (round (+ center-x (/ width 2))))
        (top (round (- center-y (/ height 2))))
        (bottom (round (+ center-y (/ height 2)))))

    (cond
     ((eq side 'top) (list (round center-x) top))
     ((eq side 'bottom) (list (round center-x) bottom))
     ((eq side 'left) (list left (round center-y)))
     ((eq side 'right) (list right (round center-y)))
     (t (list (round center-x) (round center-y))))))

(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))


(defun dag-draw--draw-simple-line (grid x1 y1 x2 y2)
  "Draw a simple line from (X1,Y1) to (X2,Y2) with arrow.

GRID is a 2D vector representing the ASCII character grid (modified in place).
X1, Y1, X2, Y2 are integers representing start
and end positions in grid coordinates.

Draws an L-shaped path (vertical first, then horizontal) using line characters
\(─ │) and adds a directional arrow (▼ ▲ ▶ ◀) at the endpoint.

GKNV-compliant: splines are pre-clipped to boundaries, enabling simple drawing."
  (let ((grid-height (length grid))
        (grid-width (if (> (length grid) 0) (length (aref grid 0)) 0)))

    ;; Draw vertical line first
    (when (not (= y1 y2))
      (let ((start-y (min y1 y2))
            (end-y (max y1 y2)))
        (dotimes (i (1+ (- end-y start-y)))
          (let ((y (+ start-y i)))
            (when (and (>= x1 0) (< x1 grid-width) (>= y 0) (< y grid-height))
              (dag-draw--set-char grid x1 y ?│))))))

    ;; Draw horizontal line
    (when (not (= x1 x2))
      (let ((start-x (min x1 x2))
            (end-x (max x1 x2)))
        (dotimes (i (1+ (- end-x start-x)))
          (let ((x (+ start-x i)))
            (when (and (>= x 0) (< x grid-width) (>= y2 0) (< y2 grid-height))
              (dag-draw--set-char grid x y2 ?─))))))

    ;; Add arrow at end point
    (when (and (>= x2 0) (< x2 grid-width) (>= y2 0) (< y2 grid-height))
      (let ((arrow-char (cond ((> y2 y1) ?▼)  ; Down arrow
                              ((< y2 y1) ?▲)  ; Up arrow
                              ((> x2 x1) ?▶)  ; Right arrow
                              ((< x2 x1) ?◀)  ; Left arrow
                              (t ?●))))       ; Point
        (dag-draw--set-char grid x2 y2 arrow-char)))))

;; 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--draw-simple-edge (grid from-x from-y to-x to-y from-node to-node)
  "Draw a simple edge from (FROM-X,FROM-Y) to (TO-X,TO-Y) in GRID.

GRID is a 2D vector representing the ASCII character grid (modified in place).
FROM-X and FROM-Y are integers representing source node top-left corner.
TO-X and TO-Y are integers representing destination node top-left corner.
FROM-NODE is a `dag-draw-node' structure for the source node.
TO-NODE is a `dag-draw-node' structure for the destination node.

Calculates proper port positions on node boundaries per GKNV Section 4.2,
then draws an orthogonal path with vertical and horizontal segments using
line characters (─ │) and a downward arrow (▼) at the destination."

  ;; Calculate port positions per GKNV Section 4.2: Node Port as X-direction offset from node center
  (let* ((from-node-width (+ (string-width (dag-draw-node-label from-node)) 4))  ; Actual from-node width (CJK-aware)
         (to-node-width (+ (string-width (dag-draw-node-label to-node)) 4))       ; Actual to-node width (CJK-aware)
         (from-port-x (+ from-x (/ from-node-width 2))) ; Center X of from-node
         (from-port-y (+ from-y 3))                     ; Bottom Y of from-node
         (to-port-x (+ to-x (/ to-node-width 2)))       ; Center X of to-node
         (to-port-y to-y)                               ; Top Y of to-node
         ;; Calculate inter-rank routing position (midway between ranks)
         (routing-y (- to-port-y 1))) ; One row above destination node

    ;; Draw vertical line down from from-node to routing level
    (when (< from-port-y routing-y)
      (let ((y from-port-y))
        (while (< y routing-y)
          (dag-draw--set-grid-char grid from-port-x y ?│)
          (setq y (1+ y)))))

    ;; Draw horizontal line at inter-rank routing level
    (when (/= from-port-x to-port-x)
      (let ((start-x (min from-port-x to-port-x))
            (end-x (max from-port-x to-port-x)))
        (while (<= start-x end-x)
          (dag-draw--set-grid-char grid start-x routing-y ?─)
          (setq start-x (1+ start-x)))))

    ;; Draw final vertical segment down to destination node
    (when (< routing-y to-port-y)
      (dag-draw--set-grid-char grid to-port-x routing-y ?│))

    ;; Draw arrow at destination
    (dag-draw--set-grid-char grid to-port-x to-port-y ?▼)))

(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
