;;; dag-draw-core.el --- Core utilities 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:

;; Core utility functions and data structure helpers for the dag-draw package.
;; This includes graph traversal, validation, and manipulation functions.

;;; Code:

(require 'dash)
(require 'ht)
(require 'dag-draw)

;; Forward declarations
(declare-function dag-draw--debug-spacing-calculation "dag-draw-quality")
(declare-function dag-draw--calculate-max-required-rank-separation "dag-draw-quality")

;;; Helper Functions

(defun dag-draw--plist-to-ht (plist)
  "Convert property list PLIST to hash table.
Extracts keyword properties like :ascii-marker, :svg-fill, etc.

PLIST is a property list with alternating keyword keys and values.

Returns a hash table with the same key-value pairs."
  (let ((ht (ht-create)))
    (while plist
      (ht-set ht (pop plist) (pop plist)))
    ht))

;;; Batch Graph Creation

(defun dag-draw-create-from-spec (&rest spec)
  "Create a DAG from a declarative specification.

SPEC is a property list with the following structure:
  :nodes - List of node specifications
  :edges - List of edge specifications

Node specification format:
  (node-id :label \"Label\" &rest attributes)

  Required:
    node-id - Symbol identifying the node
    :label  - String display label

  Optional attributes (keyword properties):
    :ascii-marker      - String to prepend to label
    :ascii-highlight   - Boolean for double-line borders
    :svg-fill          - CSS color for background
    :svg-stroke        - CSS color for border
    :svg-stroke-width  - Number for border thickness
    Any other custom attributes

Edge specification format:
  (from-id to-id &rest attributes)

  Required:
    from-id - Source node ID
    to-id   - Destination node ID

  Optional attributes (keyword properties):
    :weight     - Number (default: 1)
    :label      - String edge label
    :min-length - Number of minimum ranks between nodes
    Any other custom attributes

Returns an unlaid-out `dag-draw-graph' structure.
Call `dag-draw-layout-graph' to compute positions before rendering.

Example:
  (setq graph (dag-draw-create-from-spec
                :nodes \\='((a :label \"Start\"
                            :ascii-marker \"-> \")
                          (b :label \"Middle\")
                          (c :label \"End\"
                            :ascii-marker \"[DONE] \"))
                :edges \\='((a b)
                          (b c :weight 5))))

Validation:
  - Throws error if :nodes or :edges keys are missing
  - Throws error if any node is missing :label property
  - Throws error if duplicate node IDs exist
  - Throws error if edge references non-existent node
  - Throws error if edge is malformed"
  ;; Parse spec
  (let ((nodes (plist-get spec :nodes))
        (edges (plist-get spec :edges)))

    ;; Validate spec structure
    (unless (plist-member spec :nodes)
      (error "Spec missing required :nodes key"))
    (unless (plist-member spec :edges)
      (error "Spec missing required :edges key"))

    ;; Create empty graph
    (let ((graph (dag-draw-create-graph))
          (node-id-set (make-hash-table :test 'eq)))

      ;; Validate and add nodes
      (dolist (node-spec nodes)
        (let* ((node-id (car node-spec))
               (node-plist (cdr node-spec))
               (label (plist-get node-plist :label)))

          ;; Validate node has :label
          (unless label
            (error "Node '%s' missing required :label property" node-id))

          ;; Check for duplicate node ID
          (when (gethash node-id node-id-set)
            (error "Duplicate node ID: '%s'" node-id))

          ;; Mark node as seen
          (puthash node-id t node-id-set)

          ;; Extract label and remaining attributes
          (let* ((attrs-plist (copy-sequence node-plist))
                 ;; Remove :label from attributes plist
                 (attrs-plist-without-label
                  (let ((result '()))
                    (while attrs-plist
                      (let ((key (pop attrs-plist))
                            (val (pop attrs-plist)))
                        (unless (eq key :label)
                          (push key result)
                          (push val result))))
                    (nreverse result)))
                 (attrs-ht (dag-draw--plist-to-ht attrs-plist-without-label)))

            ;; Add node to graph
            (dag-draw-add-node graph node-id label attrs-ht))))

      ;; Validate and add edges
      (dolist (edge-spec edges)
        (let ((from-id (nth 0 edge-spec))
              (to-id (nth 1 edge-spec))
              (edge-attrs-plist (nthcdr 2 edge-spec)))

          ;; Validate edge has at least two elements
          (unless (>= (length edge-spec) 2)
            (error "Malformed edge: %S - expected (from to &rest attrs)" edge-spec))

          ;; Validate from-node exists
          (unless (gethash from-id node-id-set)
            (error "Edge %S: node '%s' not found in spec" edge-spec from-id))

          ;; Validate to-node exists
          (unless (gethash to-id node-id-set)
            (error "Edge %S: node '%s' not found in spec" edge-spec to-id))

          ;; Extract edge attributes
          (let* ((attrs-ht (dag-draw--plist-to-ht edge-attrs-plist))
                 (weight (ht-get attrs-ht :weight))
                 (label (ht-get attrs-ht :label)))

            ;; Add edge to graph
            (dag-draw-add-edge graph from-id to-id weight label attrs-ht))))

      ;; Return unlaid-out graph
      graph)))

;;; Graph Traversal and Analysis

(defun dag-draw-get-node (graph node-id)
  "Get the node with NODE-ID from GRAPH.

GRAPH is a `dag-draw-graph' structure.
NODE-ID is the unique identifier for the node.

Returns the `dag-draw-node' structure, or nil if not found."
  (ht-get (dag-draw-graph-nodes graph) node-id))

(defun dag-draw-get-edges-from (graph node-id)
  "Get all edges originating from NODE-ID in GRAPH.

GRAPH is a `dag-draw-graph' structure.
NODE-ID is the unique identifier for the source node.

Returns a list of `dag-draw-edge' structures."
  (--filter (eq (dag-draw-edge-from-node it) node-id)
            (dag-draw-graph-edges graph)))

(defun dag-draw-get-edges-to (graph node-id)
  "Get all edges terminating at NODE-ID in GRAPH.

GRAPH is a `dag-draw-graph' structure.
NODE-ID is the unique identifier for the target node.

Returns a list of `dag-draw-edge' structures."
  (--filter (eq (dag-draw-edge-to-node it) node-id)
            (dag-draw-graph-edges graph)))

(defun dag-draw--edge-endpoints (edge)
  "Return cons of (from-id . to-id) node IDs from EDGE.

EDGE is a `dag-draw-edge' structure.

Returns a cons cell with source and target node IDs.
This helper reduces boilerplate when both endpoints are needed."
  (cons (dag-draw-edge-from-node edge)
        (dag-draw-edge-to-node edge)))

(defun dag-draw--edge-nodes (graph edge)
  "Return cons of (from-node . to-node) node structures from EDGE in GRAPH.

GRAPH is a `dag-draw-graph' structure.
EDGE is a `dag-draw-edge' structure.

Returns a cons cell with the source and target node structures.
This helper reduces boilerplate when both endpoint nodes are needed."
  (cons (dag-draw-get-node graph (dag-draw-edge-from-node edge))
        (dag-draw-get-node graph (dag-draw-edge-to-node edge))))

(defun dag-draw-get-successors (graph node-id)
  "Get list of successor node IDs for NODE-ID in GRAPH.

GRAPH is a `dag-draw-graph' structure.
NODE-ID is the unique identifier for the node.

Returns a list of node IDs that are direct successors."
  (mapcar #'dag-draw-edge-to-node (dag-draw-get-edges-from graph node-id)))

(defun dag-draw-get-predecessors (graph node-id)
  "Get list of predecessor node IDs for NODE-ID in GRAPH.

GRAPH is a `dag-draw-graph' structure.
NODE-ID is the unique identifier for the node.

Returns a list of node IDs that are direct predecessors."
  (mapcar #'dag-draw-edge-from-node (dag-draw-get-edges-to graph node-id)))

(defun dag-draw-get-node-ids (graph)
  "Get list of all node IDs in GRAPH.

GRAPH is a `dag-draw-graph' structure.

Returns a list of all unique node identifiers."
  (ht-keys (dag-draw-graph-nodes graph)))

;; Note: dag-draw-node-count and dag-draw-edge-count are defined in dag-draw.el
;; to avoid circular dependencies

;;; Graph Properties

(defun dag-draw-get-source-nodes (graph)
  "Get list of source nodes (nodes with no incoming edges) in GRAPH.

GRAPH is a `dag-draw-graph' structure.

Returns a list of node IDs that have no predecessors."
  (let ((all-nodes (dag-draw-get-node-ids graph))
        (target-nodes (mapcar #'dag-draw-edge-to-node (dag-draw-graph-edges graph))))
    (--filter (not (member it target-nodes)) all-nodes)))

;;; Graph Modification

(defun dag-draw-remove-node (graph node-id)
  "Remove NODE-ID and all connected edges from GRAPH.

GRAPH is a `dag-draw-graph' structure to modify.
NODE-ID is the unique identifier for the node to remove.

Returns the modified GRAPH with the node and its edges removed."
  (when (ht-get (dag-draw-graph-nodes graph) node-id)
    ;; Remove all edges connected to this node
    (setf (dag-draw-graph-edges graph)
          (--remove (or (eq (dag-draw-edge-from-node it) node-id)
                        (eq (dag-draw-edge-to-node it) node-id))
                    (dag-draw-graph-edges graph)))
    ;; Remove the node itself
    (ht-remove! (dag-draw-graph-nodes graph) node-id))
  graph)

(defun dag-draw-remove-edge (graph from-node to-node)
  "Remove the edge from FROM-NODE to TO-NODE in GRAPH.

GRAPH is a `dag-draw-graph' structure to modify.
FROM-NODE is the node ID of the edge source.
TO-NODE is the node ID of the edge target.

Returns the modified GRAPH with the specified edge removed."
  (setf (dag-draw-graph-edges graph)
        (--remove (and (eq (dag-draw-edge-from-node it) from-node)
                       (eq (dag-draw-edge-to-node it) to-node))
                  (dag-draw-graph-edges graph)))
  graph)

;;; Graph Copying and Cloning

(defun dag-draw-copy-graph (graph)
  "Create a deep copy of GRAPH.

GRAPH is a `dag-draw-graph' structure to copy.

Returns a new `dag-draw-graph' with all nodes, edges, and attributes
copied.  Modifications to the copy will not affect the original."
  (let ((new-graph (dag-draw-graph-create
                    :node-separation (dag-draw-graph-node-separation graph)
                    :rank-separation (dag-draw-graph-rank-separation graph)
                    :attributes (ht-copy (dag-draw-graph-attributes graph)))))

    ;; Copy nodes
    (ht-each (lambda (node-id node)
               (let ((new-node (dag-draw-node-create
                               :id (dag-draw-node-id node)
                               :label (dag-draw-node-label node)
                               :x-size (dag-draw-node-x-size node)
                               :y-size (dag-draw-node-y-size node)
                               :x-coord (dag-draw-node-x-coord node)
                               :y-coord (dag-draw-node-y-coord node)
                               :rank (dag-draw-node-rank node)
                               :order (dag-draw-node-order node)
                               :attributes (if (dag-draw-node-attributes node)
                                             (ht-copy (dag-draw-node-attributes node))
                                             (ht-create)))))
                 (ht-set! (dag-draw-graph-nodes new-graph) node-id new-node)))
             (dag-draw-graph-nodes graph))

    ;; Copy edges
    (dolist (edge (dag-draw-graph-edges graph))
      (let ((new-edge (dag-draw-edge-create
                      :from-node (dag-draw-edge-from-node edge)
                      :to-node (dag-draw-edge-to-node edge)
                      :weight (dag-draw-edge-weight edge)
                      :min-length (dag-draw-edge-δ edge)  ; GKNV δ(e) notation
                      :label (dag-draw-edge-label edge)
                      :spline-points (copy-sequence (dag-draw-edge-spline-points edge))
                      :attributes (if (dag-draw-edge-attributes edge)
                                    (ht-copy (dag-draw-edge-attributes edge))
                                    (ht-create)))))
        (push new-edge (dag-draw-graph-edges new-graph))))

    ;; Copy other graph properties
    (setf (dag-draw-graph-max-rank new-graph) (dag-draw-graph-max-rank graph))
    (setf (dag-draw-graph-rank-sets new-graph) (copy-tree (dag-draw-graph-rank-sets graph)))

    new-graph))

;;; Debugging and Inspection

(defun dag-draw-graph-summary (graph)
  "Return a human-readable summary string of GRAPH.

GRAPH is a `dag-draw-graph' structure.

Returns a string describing the graph's node count, edge count,
and maximum rank."
  (format "Graph: %d nodes, %d edges, max-rank: %s"
          (dag-draw-node-count graph)
          (dag-draw-edge-count graph)
          (or (dag-draw-graph-max-rank graph) "unset")))

(provide 'dag-draw-core)

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