! Copyright (c) 2022-2026 Jason Christopherson
! SPDX-License-Identifier: MIT
!
! Permission is hereby granted, free of charge, to any person obtaining a copy
! of this software and associated documentation files (the "Software"), to deal
! in the Software without restriction, including without limitation the rights
! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
! copies of the Software, and to permit persons to whom the Software is
! furnished to do so, subject to the following conditions:
!
! The Software is provided "as is", without warranty of any kind, express or
! implied, including but not limited to the warranties of merchantability,
! fitness for a particular purpose and noninfringement.
module dynamics_graph
    !! Provides a lightweight, undirected multigraph container along with the
    !! traversal utilities required to analyze the topology of closed-loop
    !! mechanisms.  Vertices are represented by integer indices, and edges are
    !! stored in a simple allocatable array.
    use iso_fortran_env
    use dynamics_error_handling
    implicit none
    private
    public :: graph_edge
    public :: graph_path
    public :: graph_loop
    public :: spanning_tree
    public :: graph

    type graph_edge
        !! Defines an undirected edge connecting two vertices.
        integer(int32), public :: vertex_1 = 0
            !! The index of the first vertex.
        integer(int32), public :: vertex_2 = 0
            !! The index of the second vertex.
    end type

    type graph_path
        !! Describes a path through a graph.
        integer(int32), allocatable, public, dimension(:) :: vertices
            !! An N+1 element array containing the vertices visited along the
            !! path, in order.
        integer(int32), allocatable, public, dimension(:) :: edges
            !! An N-element array containing the edges traversed along the path,
            !! in order.
        logical, allocatable, public, dimension(:) :: forward
            !! An N-element array that is true if the corresponding edge was
            !! traversed from vertex_1 to vertex_2, and false if the edge was
            !! traversed from vertex_2 to vertex_1.
    end type

    type graph_loop
        !! Describes an independent loop within the graph.  The loop is defined
        !! by an edge that is not part of the spanning tree; the loop itself is
        !! closed by the tree paths leading to each of the edge's vertices.
        integer(int32), public :: cut_edge = 0
            !! The index of the edge closing the loop.
        integer(int32), public :: vertex_1 = 0
            !! The first vertex of the cut edge.
        integer(int32), public :: vertex_2 = 0
            !! The second vertex of the cut edge.
    end type

    type spanning_tree
        !! Describes a spanning tree of a graph as generated by a breadth-first
        !! traversal.
        integer(int32), public :: root = 0
            !! The vertex from which the traversal was started.
        integer(int32), allocatable, public, dimension(:) :: parent_vertex
            !! An array, one entry per vertex, containing the index of the
            !! parent vertex.  The root vertex and any unreachable vertices are
            !! assigned a value of zero.
        integer(int32), allocatable, public, dimension(:) :: parent_edge
            !! An array, one entry per vertex, containing the index of the edge
            !! connecting the vertex to its parent.  The root vertex and any
            !! unreachable vertices are assigned a value of zero.
        logical, allocatable, public, dimension(:) :: parent_edge_forward
            !! An array, one entry per vertex, that is true if the edge
            !! connecting the vertex to its parent is traversed from vertex_1 to
            !! vertex_2 when moving from the parent to the vertex.
        integer(int32), allocatable, public, dimension(:) :: depth
            !! An array, one entry per vertex, containing the number of edges
            !! between the vertex and the root.  Unreachable vertices are
            !! assigned a value of -1.
        logical, allocatable, public, dimension(:) :: edge_in_tree
            !! An array, one entry per edge, that is true if the edge is part of
            !! the spanning tree.
        integer(int32), allocatable, public, dimension(:) :: visit_order
            !! An array containing the reachable vertices in the order in which
            !! they were visited.
    contains
        procedure, public :: get_path => st_get_path
        procedure, public :: get_cut_edges => st_get_cut_edges
        procedure, public :: is_connected => st_is_connected
    end type

    type graph
        !! Defines an undirected multigraph.  Multiple edges between the same
        !! pair of vertices, as well as self-loops, are permitted.
        integer(int32), private :: m_vertices = 0
        integer(int32), private :: m_count = 0
        type(graph_edge), allocatable, private, dimension(:) :: m_edges
    contains
        procedure, public :: initialize => gr_initialize
        procedure, public :: get_vertex_count => gr_vertex_count
        procedure, public :: get_edge_count => gr_edge_count
        procedure, public :: get_edge => gr_get_edge
        procedure, public :: add_edge => gr_add_edge
        procedure, public :: get_adjacent_edges => gr_adjacent_edges
        procedure, public :: build_spanning_tree => gr_spanning_tree
        procedure, public :: find_independent_loops => gr_find_loops
        procedure, public :: get_independent_loop_count => gr_loop_count
    end type

contains
! ******************************************************************************
! GRAPH MEMBERS
! ------------------------------------------------------------------------------
    subroutine gr_initialize(this, nvertices, ncapacity)
        !! Initializes the graph with the requested number of vertices and no
        !! edges.
        class(graph), intent(inout) :: this
            !! The graph object.
        integer(int32), intent(in) :: nvertices
            !! The number of vertices in the graph.  This value must be at least
            !! one.
        integer(int32), intent(in), optional :: ncapacity
            !! An optional estimate of the number of edges the graph will
            !! contain.  This value is used only to size the initial storage.

        ! Local Variables
        integer(int32) :: n, flag

        ! Input Checking
        if (nvertices < 1) error stop DYN_INVALID_INPUT_ERROR

        ! Process
        n = 10
        if (present(ncapacity)) n = max(ncapacity, 1)
        this%m_vertices = nvertices
        this%m_count = 0
        if (allocated(this%m_edges)) deallocate(this%m_edges)
        allocate(this%m_edges(n), stat = flag)
        if (flag /= 0) error stop DYN_MEMORY_ERROR
    end subroutine

! ------------------------------------------------------------------------------
    pure function gr_vertex_count(this) result(rst)
        !! Gets the number of vertices in the graph.
        class(graph), intent(in) :: this
            !! The graph object.
        integer(int32) :: rst
            !! The vertex count.

        rst = this%m_vertices
    end function

! ------------------------------------------------------------------------------
    pure function gr_edge_count(this) result(rst)
        !! Gets the number of edges in the graph.
        class(graph), intent(in) :: this
            !! The graph object.
        integer(int32) :: rst
            !! The edge count.

        rst = this%m_count
    end function

! ------------------------------------------------------------------------------
    function gr_get_edge(this, i) result(rst)
        !! Gets the requested edge.
        class(graph), intent(in) :: this
            !! The graph object.
        integer(int32), intent(in) :: i
            !! The index of the edge to retrieve (1 = first edge).
        type(graph_edge) :: rst
            !! The requested edge.

        if (i < 1 .or. i > this%m_count) error stop DYN_INDEX_OUT_OF_RANGE
        rst = this%m_edges(i)
    end function

! ------------------------------------------------------------------------------
    subroutine gr_add_edge(this, v1, v2)
        !! Adds an edge to the graph.
        class(graph), intent(inout) :: this
            !! The graph object.
        integer(int32), intent(in) :: v1
            !! The index of the first vertex.
        integer(int32), intent(in) :: v2
            !! The index of the second vertex.

        ! Local Variables
        integer(int32) :: flag
        type(graph_edge), allocatable, dimension(:) :: temp

        ! Input Checking
        if (this%m_vertices < 1) error stop DYN_INVALID_INPUT_ERROR
        if (v1 < 1 .or. v1 > this%m_vertices) error stop DYN_INDEX_OUT_OF_RANGE
        if (v2 < 1 .or. v2 > this%m_vertices) error stop DYN_INDEX_OUT_OF_RANGE

        ! Ensure sufficient capacity exists
        if (this%m_count >= size(this%m_edges)) then
            allocate(temp(2 * size(this%m_edges)), stat = flag)
            if (flag /= 0) error stop DYN_MEMORY_ERROR
            temp(1:this%m_count) = this%m_edges(1:this%m_count)
            call move_alloc(temp, this%m_edges)
        end if

        ! Store the edge
        this%m_count = this%m_count + 1
        this%m_edges(this%m_count)%vertex_1 = v1
        this%m_edges(this%m_count)%vertex_2 = v2
    end subroutine

! ------------------------------------------------------------------------------
    function gr_adjacent_edges(this, v) result(rst)
        !! Gets the indices of all edges connected to the requested vertex.
        class(graph), intent(in) :: this
            !! The graph object.
        integer(int32), intent(in) :: v
            !! The index of the vertex of interest.
        integer(int32), allocatable, dimension(:) :: rst
            !! An array containing the indices of the connected edges.

        ! Local Variables
        integer(int32) :: i, n
        integer(int32), allocatable, dimension(:) :: buffer

        ! Input Checking
        if (v < 1 .or. v > this%m_vertices) error stop DYN_INDEX_OUT_OF_RANGE

        ! Process
        allocate(buffer(this%m_count))
        n = 0
        do i = 1, this%m_count
            if (this%m_edges(i)%vertex_1 == v .or. &
                this%m_edges(i)%vertex_2 == v) &
            then
                n = n + 1
                buffer(n) = i
            end if
        end do
        rst = buffer(1:n)
    end function

! ------------------------------------------------------------------------------
    function gr_spanning_tree(this, root) result(rst)
        !! Constructs a spanning tree of the graph by means of a breadth-first
        !! traversal.
        class(graph), intent(in) :: this
            !! The graph object.
        integer(int32), intent(in), optional :: root
            !! The index of the vertex from which to start the traversal.  If
            !! not supplied, the first vertex is used.
        type(spanning_tree) :: rst
            !! The resulting spanning_tree object.

        ! Local Variables
        integer(int32) :: i, k, nv, ne, iv, ihead, itail, other
        integer(int32), allocatable, dimension(:) :: queue

        ! Initialization
        nv = this%m_vertices
        ne = this%m_count
        if (nv < 1) error stop DYN_INVALID_INPUT_ERROR
        rst%root = 1
        if (present(root)) rst%root = root
        if (rst%root < 1 .or. rst%root > nv) error stop DYN_INDEX_OUT_OF_RANGE

        allocate(rst%parent_vertex(nv), source = 0)
        allocate(rst%parent_edge(nv), source = 0)
        allocate(rst%parent_edge_forward(nv), source = .true.)
        allocate(rst%depth(nv), source = -1)
        allocate(rst%edge_in_tree(ne), source = .false.)
        allocate(queue(nv))

        ! Traverse the graph
        ihead = 1
        itail = 1
        queue(1) = rst%root
        rst%depth(rst%root) = 0
        do while (ihead <= itail)
            iv = queue(ihead)
            ihead = ihead + 1
            do i = 1, ne
                if (this%m_edges(i)%vertex_1 == iv) then
                    other = this%m_edges(i)%vertex_2
                else if (this%m_edges(i)%vertex_2 == iv) then
                    other = this%m_edges(i)%vertex_1
                else
                    cycle
                end if
                if (rst%depth(other) >= 0) cycle
                rst%depth(other) = rst%depth(iv) + 1
                rst%parent_vertex(other) = iv
                rst%parent_edge(other) = i
                rst%parent_edge_forward(other) = &
                    this%m_edges(i)%vertex_1 == iv
                rst%edge_in_tree(i) = .true.
                itail = itail + 1
                queue(itail) = other
            end do
        end do

        ! Store the visitation order
        allocate(rst%visit_order(itail))
        do k = 1, itail
            rst%visit_order(k) = queue(k)
        end do
    end function

! ------------------------------------------------------------------------------
    function gr_find_loops(this, tree) result(rst)
        !! Determines the set of independent loops within the graph.  The number
        !! of loops is the cyclomatic number of the graph.
        class(graph), intent(in) :: this
            !! The graph object.
        type(spanning_tree), intent(in) :: tree
            !! A spanning tree of this graph.
        type(graph_loop), allocatable, dimension(:) :: rst
            !! An array of the independent loops.

        ! Local Variables
        integer(int32) :: i, n
        integer(int32), allocatable, dimension(:) :: cuts

        ! Process
        cuts = tree%get_cut_edges()
        n = size(cuts)
        allocate(rst(n))
        do i = 1, n
            rst(i)%cut_edge = cuts(i)
            rst(i)%vertex_1 = this%m_edges(cuts(i))%vertex_1
            rst(i)%vertex_2 = this%m_edges(cuts(i))%vertex_2
        end do
    end function

! ------------------------------------------------------------------------------
    pure function gr_loop_count(this) result(rst)
        !! Gets the number of independent loops in the graph assuming the graph
        !! is connected.  The value is the cyclomatic number \(E - V + 1\).
        class(graph), intent(in) :: this
            !! The graph object.
        integer(int32) :: rst
            !! The number of independent loops.

        rst = this%m_count - this%m_vertices + 1
    end function

! ******************************************************************************
! SPANNING_TREE MEMBERS
! ------------------------------------------------------------------------------
    function st_get_path(this, v) result(rst)
        !! Gets the path from the root of the tree to the requested vertex.
        class(spanning_tree), intent(in) :: this
            !! The spanning_tree object.
        integer(int32), intent(in) :: v
            !! The index of the destination vertex.
        type(graph_path) :: rst
            !! The resulting path.  The path contains no edges if the requested
            !! vertex is the root vertex.

        ! Local Variables
        integer(int32) :: i, n, iv, ie

        ! Input Checking
        if (.not.allocated(this%depth)) error stop DYN_INVALID_INPUT_ERROR
        if (v < 1 .or. v > size(this%depth)) error stop DYN_INDEX_OUT_OF_RANGE
        if (this%depth(v) < 0) error stop DYN_INVALID_INPUT_ERROR

        ! Walk back to the root and store the results in forward order
        n = this%depth(v)
        allocate(rst%vertices(n + 1), rst%edges(n), rst%forward(n))
        iv = v
        rst%vertices(n + 1) = v
        do i = n, 1, -1
            ie = this%parent_edge(iv)
            rst%edges(i) = ie
            rst%vertices(i) = this%parent_vertex(iv)
            rst%forward(i) = this%parent_edge_forward(iv)
            iv = this%parent_vertex(iv)
        end do
    end function

! ------------------------------------------------------------------------------
    function st_get_cut_edges(this) result(rst)
        !! Gets the indices of all edges that are not part of the spanning tree.
        class(spanning_tree), intent(in) :: this
            !! The spanning_tree object.
        integer(int32), allocatable, dimension(:) :: rst
            !! An array containing the indices of the cut edges.

        ! Local Variables
        integer(int32) :: i, n
        integer(int32), allocatable, dimension(:) :: buffer

        if (.not.allocated(this%edge_in_tree)) error stop DYN_INVALID_INPUT_ERROR
        allocate(buffer(size(this%edge_in_tree)))
        n = 0
        do i = 1, size(this%edge_in_tree)
            if (.not.this%edge_in_tree(i)) then
                n = n + 1
                buffer(n) = i
            end if
        end do
        rst = buffer(1:n)
    end function

! ------------------------------------------------------------------------------
    pure function st_is_connected(this) result(rst)
        !! Determines if every vertex was reached by the traversal used to
        !! construct the tree.
        class(spanning_tree), intent(in) :: this
            !! The spanning_tree object.
        logical :: rst
            !! True if the graph is connected; else, false.

        rst = .false.
        if (.not.allocated(this%depth)) return
        rst = all(this%depth >= 0)
    end function

! ------------------------------------------------------------------------------
end module
