! =============================================================================
!> Module that contains the implementation of a single row of nodes in the
!! linked-list matrix representation.
module mod_matrix_row
  use mod_matrix_node, only: node_t, new_node
  implicit none

  private

  !> Linked list for a given row index, contains the column values
  type, public :: row_t
    !> number of elements in linked list
    integer :: nb_elements
    !> pointer to head (first element added)
    type(node_t), pointer :: head
    !> pointer to tail (last element added)
    type(node_t), pointer :: tail

    contains

    procedure :: add_node
    procedure :: get_node
    procedure :: delete_row
    procedure :: delete_node_from_row

    procedure, private :: create_first_node
    procedure, private :: append_node
  end type row_t

  public :: new_row

contains

  !> Constructor for a new row, initialises the linked list datastructure
  !! and sets the current head and tail pointers to `null()`.
  pure function new_row() result(row)
    type(row_t) :: row

    row%nb_elements = 0
    row%head => null()
    row%tail => null()
  end function new_row


  !> Adds a new node to the linked list with a given column index and value.
  subroutine add_node(this, column, element)
    !> type instance
    class(row_t), intent(inout) :: this
    !> column position of the element
    integer, intent(in) :: column
    !> the element to be added
    class(*), intent(in) :: element

    if (.not. associated(this%head)) then
      call this%create_first_node(column, element)
    else
      call this%append_node(column, element)
    end if
  end subroutine add_node


  !> Subroutine to add the first node to the linked list. Allocates a new node and
  !! sets both the head and tail to this node.
  pure subroutine create_first_node(this, column, element)
    !> type instance
    class(row_t), intent(inout) :: this
    !> column position of element
    integer, intent(in) :: column
    !> the element to be added
    class(*), intent(in) :: element

    allocate(this%head, source=new_node(column, element))
    this%tail => this%head
    this%nb_elements = this%nb_elements + 1
  end subroutine create_first_node


  !> Subroutine to append a new node to an already existing list of nodes.
  !! A new node is created, appended, and the tail is updated.
  subroutine append_node(this, column, element)
    !> type instance
    class(row_t), intent(inout) :: this
    !> column position of element
    integer, intent(in) :: column
    !> the element to be added
    class(*), intent(in) :: element

    type(node_t), pointer :: node

    node => this%get_node(column)
    ! check if node already exists
    if (associated(node)) then
      call node%add_to_node_element(element)
    else
      allocate(this%tail%next, source=new_node(column, element))
      ! update tail to last element added
      this%tail => this%tail%next
      this%nb_elements = this%nb_elements + 1
    end if
    nullify(node)
  end subroutine append_node


  !> Returns a pointer to the node corresponding to the given column.
  !! Returns a nullified pointer if no node containing the given column index
  !! was found.
  function get_node(this, column) result(node)
    !> type instance
    class(row_t), intent(in) :: this
    !> column index
    integer, intent(in) :: column
    !> the node with a column value that matches column
    type(node_t), pointer :: node

    type(node_t), pointer :: current_node
    integer :: i

    node => null()
    current_node => this%head
    ! loop over nodes, return node if column index matches
    do i = 1, this%nb_elements
      if (column == current_node%column) then
        node => current_node
        nullify(current_node)
        exit
      end if
      current_node => current_node%next
    end do
    nullify(current_node)
  end function get_node


  !> Deletes a given node from the current row.
  subroutine delete_node_from_row(this, column)
    !> type instance
    class(row_t), intent(inout) :: this
    !> column index of node to be deleted
    integer, intent(in) :: column

    type(node_t), pointer :: node
    type(node_t), pointer :: next_node
    integer :: i

    ! do nothing for empty rows
    if (.not. associated(this%head)) return

    node => this%head
    ! check if head is the one being deleted
    if (column == node%column) then
      this%head => this%head%next
      call node%delete()
      call decrement_and_nullify()
      return
    end if

    next_node => this%head%next
    ! -1 since we're working with current and next nodes, head and tail are checked
    do i = 1, this%nb_elements - 1
      ! check if next node is tail and will be deleted
      if (associated(next_node, this%tail) .and. column == next_node%column) then
        call next_node%delete()
        node%next => null()
        this%tail => node
        call decrement_and_nullify()
        exit
      end if
      if (column == next_node%column) then
        node%next => next_node%next
        call next_node%delete()
        call decrement_and_nullify()
        exit
      end if
      node => next_node
      next_node => next_node%next
    end do
    nullify(node)
    nullify(next_node)

    contains

    !> Decrements the total number of nodes in the row and nullifies the node pointers
    !! of the containing subroutine. Called whenever a node gets deleted.
    subroutine decrement_and_nullify()
      this%nb_elements = this%nb_elements - 1
      nullify(node)
      nullify(next_node)
    end subroutine decrement_and_nullify
  end subroutine delete_node_from_row


  !> Deletes a given linked list row by recursively iterating over all nodes.
  !! Nullifies the pointers and deallocates the elements.
  pure subroutine delete_row(this)
    !> type instance
    class(row_t), intent(inout) :: this

    if (associated(this%head)) call delete_node(node=this%head)
    nullify(this%head)
    nullify(this%tail)
    this%nb_elements = 0

    contains

    !> Recursive subroutine that deallocates a given node in the linked list.
    pure recursive subroutine delete_node(node)
      type(node_t), intent(inout) :: node
      type(node_t), pointer :: next_node

      next_node => null()
      if (associated(node%next)) next_node => node%next
      call node%delete()
      if (associated(next_node)) call delete_node(next_node)
      nullify(next_node)
    end subroutine delete_node
  end subroutine delete_row
end module mod_matrix_row
