;;; dag-draw-svg.el --- SVG 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:

;; SVG rendering implementation for dag-draw graphs.
;; Converts positioned graphs into scalable vector graphics with
;; smooth splines, proper arrow markers, and clean styling.

;;; Code:

(require 'dag-draw-core)
(require 'dag-draw-pass4-splines)

;;; Customization

(defcustom dag-draw-svg-coordinate-scale 10.0
  "Scale factor for converting ASCII coordinates to SVG pixels.
When graphs use ASCII coordinate mode, multiply coordinates by this
factor to get appropriate SVG pixel coordinates.  Default is 10,
meaning 1 ASCII grid unit = 10 SVG pixels."
  :type 'float
  :group '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)

;;; SVG Rendering Helpers

(defun dag-draw--svg-scale-factor (graph)
  "Get the coordinate scale factor for GRAPH.
If GRAPH uses ASCII coordinates, return `dag-draw-svg-coordinate-scale'.
Otherwise return 1.0 (no scaling needed for high-res coordinates)."
  (if (eq (dag-draw-graph-coordinate-mode graph) 'ascii)
      dag-draw-svg-coordinate-scale
    1.0))

;;; SVG Rendering

(defun dag-draw-render-svg (graph &optional selected)
  "Render GRAPH as SVG string with positioned nodes and smooth spline edges.

GRAPH is a `dag-draw-graph' structure with positioned nodes and spline points.

SELECTED is an optional node ID (symbol) to render with selection highlighting.
Selected nodes are rendered with a glow filter effect for visual emphasis.

Calculates graph bounds, creates an SVG header with appropriate viewBox,
renders edges as SVG paths, and renders nodes as SVG rectangles with labels.

Returns a string containing the complete SVG XML representation of the graph."
  (let* ((scale (dag-draw--svg-scale-factor graph))
         (bounds (dag-draw-get-graph-bounds graph))
         (min-x (* scale (nth 0 bounds)))
         (min-y (* scale (nth 1 bounds)))
         (max-x (* scale (nth 2 bounds)))
         (max-y (* scale (nth 3 bounds)))
         (width (- max-x min-x))
         (height (- max-y min-y))
         (margin 20)
         (svg-width (+ width (* 2 margin)))
         (svg-height (+ height (* 2 margin)))
         ;; Only use selection if the node actually exists in the graph
         (valid-selected (and selected (dag-draw-get-node graph selected) selected)))

    (concat
     (dag-draw--svg-header svg-width svg-height (- min-x margin) (- min-y margin)
                           (+ width (* 2 margin)) (+ height (* 2 margin)))
     (dag-draw--svg-defs valid-selected)
     (dag-draw--svg-render-edges graph scale)
     (dag-draw--svg-render-nodes graph scale valid-selected)
     (dag-draw--svg-footer))))

(defun dag-draw--svg-header (svg-width svg-height view-x view-y view-width view-height)
  "Generate SVG header with dimensions and viewBox.

SVG-WIDTH and SVG-HEIGHT are numbers representing the SVG canvas
size.  VIEW-X and VIEW-Y are numbers representing the viewBox
origin.  VIEW-WIDTH and VIEW-HEIGHT are numbers representing the
viewBox dimensions.

Returns a string containing the opening SVG tag with xmlns and
viewBox attributes."
  (format "<svg width=\"%.1f\" height=\"%.1f\" viewBox=\"%.1f %.1f %.1f %.1f\" xmlns=\"http://www.w3.org/2000/svg\">\n"
          svg-width svg-height view-x view-y view-width view-height))

(defun dag-draw--svg-defs (&optional selected)
  "Generate SVG definitions for arrow markers, filters, and styles.

SELECTED is an optional node ID indicating if selection filter should
be included.

Returns a string containing SVG <defs> section with arrowhead marker
definition, optional selection glow filter, and CSS styles."
  (concat
   "  <defs>\n"
   "    <marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
   "      <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" dag-draw-render-svg-edge-stroke "\" />\n"
   "    </marker>\n"
   ;; Add selection glow filter if a node is selected
   (if selected
       (concat
        "    <filter id=\"selection-glow\">\n"
        "      <feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"3\" result=\"blur\"/>\n"
        "      <feFlood flood-color=\"#4a90e2\" flood-opacity=\"0.5\" result=\"color\"/>\n"
        "      <feComposite in=\"color\" in2=\"blur\" operator=\"in\" result=\"coloredBlur\"/>\n"
        "      <feMerge>\n"
        "        <feMergeNode in=\"coloredBlur\"/>\n"
        "        <feMergeNode in=\"SourceGraphic\"/>\n"
        "      </feMerge>\n"
        "    </filter>\n")
     "")
   "    <style><![CDATA[\n"
   "      .node { fill: " dag-draw-render-svg-node-fill "; stroke: " dag-draw-render-svg-node-stroke "; stroke-width: 1; }\n"
   "      .node-label { font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; dominant-baseline: central; }\n"
   "      .edge { fill: none; stroke: " dag-draw-render-svg-edge-stroke "; stroke-width: 2; marker-end: url(#arrowhead); }\n"
   "      .edge-label { font-family: Arial, sans-serif; font-size: 10px; text-anchor: middle; }\n"
   "    ]]></style>\n"
   "  </defs>\n"))

(defun dag-draw--svg-render-nodes (graph scale &optional selected)
  "Render all nodes in GRAPH as SVG rectangles with labels.

GRAPH is a `dag-draw-graph' structure containing positioned nodes.
SCALE is the coordinate scale factor to apply.
SELECTED is an optional node ID (symbol) to render with selection highlighting.

For each node, creates an SVG <rect> element centered at the node's
coordinates and a <text> element for the label.  If the node ID matches
SELECTED, applies the selection-glow filter.

Returns a string containing SVG <g> group with all node elements."
  (let ((node-svg "  <g class=\"nodes\">\n"))
    (ht-each (lambda (node-id node)
               (let* ((x (* scale (or (dag-draw-node-x-coord node) 0)))
                      (y (* scale (or (dag-draw-node-y-coord node) 0)))
                      (width (* scale (dag-draw-node-x-size node)))
                      (height (* scale (dag-draw-node-y-size node)))
                      (label (dag-draw-node-label node))
                      (rect-x (- x (/ width 2.0)))
                      (rect-y (- y (/ height 2.0)))
                      (is-selected (and selected (eq node-id selected)))
                      (filter-attr (if is-selected " filter=\"url(#selection-glow)\"" ""))
                      ;; Get custom SVG style properties
                      (style-props (dag-draw--get-svg-node-style node))
                      (inline-style (dag-draw--svg-build-inline-style style-props))
                      ;; Get text-specific attributes
                      (attrs (dag-draw-node-attributes node))
                      (text-color (ht-get attrs :svg-text-color))
                      (text-fill-attr (if text-color (format " fill=\"%s\"" text-color) ""))
                      (tooltip (ht-get attrs :svg-tooltip)))

                 (setq node-svg
                       (concat node-svg
                               (format "    <rect class=\"node\" x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"3\"%s%s />\n"
                                       rect-x rect-y width height filter-attr inline-style)
                               ;; Add tooltip if present
                               (if tooltip
                                   (format "    <title>%s</title>\n" (dag-draw--escape-xml tooltip))
                                 "")
                               (format "    <text class=\"node-label\" x=\"%.1f\" y=\"%.1f\"%s>%s</text>\n"
                                       x y text-fill-attr (dag-draw--escape-xml label))))))
             (dag-draw-graph-nodes graph))

    (concat node-svg "  </g>\n")))

(defun dag-draw--svg-render-edges (graph scale)
  "Render all edges in GRAPH as SVG paths with smooth splines.

GRAPH is a `dag-draw-graph' structure containing edges with spline points.
SCALE is the coordinate scale factor to apply.

For each edge, creates an SVG <path> element from the spline points.
If the edge has a label, also creates a <text> element at the label position.

Returns a string containing SVG <g> group with all edge elements."
  (let ((edge-svg "  <g class=\"edges\">\n"))
    (dolist (edge (dag-draw-graph-edges graph))
      ;; Render edge path if spline points exist
      (when (dag-draw-edge-spline-points edge)
        (let ((path-data (dag-draw--svg-path-from-spline edge scale)))
          (setq edge-svg
                (concat edge-svg
                        (format "    <path class=\"edge\" d=\"%s\" />\n" path-data)))))

      ;; Render edge label if present (independent of spline points)
      (when (dag-draw-edge-label edge)
        (let ((label-pos (dag-draw-edge-label-position edge)))
          (when label-pos
            (setq edge-svg
                  (concat edge-svg
                          (format "    <text class=\"edge-label\" x=\"%.1f\" y=\"%.1f\">%s</text>\n"
                                  (* scale (dag-draw-point-x label-pos))
                                  (* scale (dag-draw-point-y label-pos))
                                  (dag-draw--escape-xml (dag-draw-edge-label edge)))))))))

    (concat edge-svg "  </g>\n")))

(defun dag-draw--svg-path-from-spline (edge scale)
  "Convert EDGE spline points to SVG path data.

EDGE is a `dag-draw-edge' structure with spline-points attribute.
SCALE is the coordinate scale factor to apply.

Converts the list of `dag-draw-point' structures into SVG path data
string using M (moveto) and L (lineto) commands.

Returns a string containing the SVG path data, or nil if no spline points."
  (let ((points (dag-draw-edge-spline-points edge)))
    (when points
      (let ((path-data (format "M %.1f,%.1f"
                               (* scale (dag-draw-point-x (car points)))
                               (* scale (dag-draw-point-y (car points))))))

        ;; Add line segments for all remaining points
        (dolist (point (cdr points))
          (setq path-data
                (concat path-data
                        (format " L %.1f,%.1f"
                                (* scale (dag-draw-point-x point))
                                (* scale (dag-draw-point-y point))))))

        path-data))))

(defun dag-draw--svg-footer ()
  "Generate SVG footer.

Returns a string containing the closing SVG tag."
  "</svg>\n")

(defun dag-draw--escape-xml (text)
  "Escape XML special characters in TEXT.

TEXT is a string that may contain XML special characters.

Replaces & < > \" ' with their XML entity equivalents to ensure
valid XML output.  Escapes & first to avoid double-escaping.

Returns the escaped string safe for use in XML attributes and content."
  ;; Escape & first, then other characters (avoiding double-escaping)
  (let ((escaped-ampersand (replace-regexp-in-string "&" "&amp;" text)))
    (replace-regexp-in-string
     "'" "&apos;"
     (replace-regexp-in-string
      "\"" "&quot;"
      (replace-regexp-in-string
       ">" "&gt;"
       (replace-regexp-in-string "<" "&lt;" escaped-ampersand))))))

;;; SVG Visual Properties Support

(defun dag-draw--get-svg-node-style (node)
  "Extract SVG style properties from NODE attributes.

NODE is a `dag-draw-node' structure.

Checks for :svg-fill, :svg-stroke, :svg-stroke-width, and :svg-fill-opacity
attributes in the node's attributes hash table.  Returns an alist of
(property . value) pairs for attributes that are set, with defaults for
unset attributes.

Returns alist like ((fill . \"#ff0000\") (stroke . \"#0000ff\") (stroke-width . 2))."
  (let ((attrs (dag-draw-node-attributes node))
        (styles nil))
    ;; Extract custom fill color
    (when-let ((fill (ht-get attrs :svg-fill)))
      (push (cons 'fill fill) styles))
    ;; Extract custom stroke color
    (when-let ((stroke (ht-get attrs :svg-stroke)))
      (push (cons 'stroke stroke) styles))
    ;; Extract custom stroke width
    (when-let ((stroke-width (ht-get attrs :svg-stroke-width)))
      (push (cons 'stroke-width stroke-width) styles))
    ;; Extract custom fill opacity
    (when-let ((fill-opacity (ht-get attrs :svg-fill-opacity)))
      (push (cons 'fill-opacity fill-opacity) styles))
    styles))

(defun dag-draw--svg-build-inline-style (style-alist)
  "Convert STYLE-ALIST to SVG inline style attribute string.

STYLE-ALIST is an alist of (property . value) pairs where property
is a symbol and value is a string or number.

Returns a string like ' style=\"fill: #ff0000; stroke: #0000ff; stroke-width: 2;\"'
or empty string if STYLE-ALIST is nil.  The returned string includes a
leading space if non-empty, suitable for direct concatenation into a tag."
  (if (null style-alist)
      ""
    (concat " style=\""
            (mapconcat
             (lambda (pair)
               (let ((prop (car pair))
                     (val (cdr pair)))
                 (format "%s: %s"
                         (symbol-name prop)
                         (if (numberp val) (number-to-string val) val))))
             style-alist
             "; ")
            ";\"")))

(provide 'dag-draw-svg)

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