# SPDX-License-Identifier: Apache-2.0
# Copyright 2025 LinkedList Example Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
A comprehensive implementation of a singly linked list data structure.

This module provides a complete linked list implementation with common operations
including insertion, deletion, searching, and traversal. The implementation
follows Google Python style guidelines and includes proper error handling.
"""

from typing import Optional, Any, Iterator


class ListNode:
    """
    A node in a singly linked list.
    
    Attributes:
        data: The data stored in the node.
        next: Reference to the next node in the list.
    """
    
    def __init__(self, data: Any) -> None:
        """
        Initialize a new list node.
        
        Args:
            data: The data to store in the node.
        """
        self.data = data
        self.next: Optional['ListNode'] = None
    
    def __repr__(self) -> str:
        """Return string representation of the node."""
        return f"ListNode({self.data})"


class LinkedList:
    """
    A singly linked list implementation.
    
    This class provides a complete implementation of a singly linked list
    with methods for insertion, deletion, searching, and traversal.
    
    Attributes:
        head: Reference to the first node in the list.
        size: Current number of nodes in the list.
    """
    
    def __init__(self) -> None:
        """Initialize an empty linked list."""
        self.head: Optional[ListNode] = None
        self.size: int = 0
    
    def __len__(self) -> int:
        """Return the number of nodes in the list."""
        return self.size
    
    def __bool__(self) -> bool:
        """Return True if the list is not empty."""
        return self.head is not None
    
    def __iter__(self) -> Iterator[Any]:
        """Iterate over the data in the list."""
        current = self.head
        while current:
            yield current.data
            current = current.next
    
    def __repr__(self) -> str:
        """Return string representation of the linked list."""
        if not self.head:
            return "LinkedList([])"
        
        elements = []
        current = self.head
        while current:
            elements.append(repr(current.data))
            current = current.next
        
        return f"LinkedList([{' -> '.join(elements)}])"
    
    def append(self, data: Any) -> None:
        """
        Add a new node with the given data to the end of the list.
        
        Args:
            data: The data to add to the list.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        new_node = ListNode(data)
        
        if not self.head:
            self.head = new_node
        else:
            current = self.head
            while current.next:
                current = current.next
            current.next = new_node
        
        self.size += 1
    
    def prepend(self, data: Any) -> None:
        """
        Add a new node with the given data to the beginning of the list.
        
        Args:
            data: The data to add to the list.
        
        Time Complexity: O(1)
        Space Complexity: O(1)
        """
        new_node = ListNode(data)
        new_node.next = self.head
        self.head = new_node
        self.size += 1
    
    def insert_at_index(self, index: int, data: Any) -> None:
        """
        Insert a new node with the given data at the specified index.
        
        Args:
            index: The position to insert the new node (0-based).
            data: The data to add to the list.
        
        Raises:
            IndexError: If the index is out of bounds.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        if index < 0 or index > self.size:
            raise IndexError(f"Index {index} out of bounds for list of size {self.size}")
        
        if index == 0:
            self.prepend(data)
            return
        
        new_node = ListNode(data)
        current = self.head
        
        # Navigate to the node before the insertion point
        for _ in range(index - 1):
            current = current.next
        
        new_node.next = current.next
        current.next = new_node
        self.size += 1
    
    def delete(self, data: Any) -> bool:
        """
        Delete the first occurrence of the given data from the list.
        
        Args:
            data: The data to remove from the list.
        
        Returns:
            True if the data was found and removed, False otherwise.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        if not self.head:
            return False
        
        # Handle deletion of head node
        if self.head.data == data:
            self.head = self.head.next
            self.size -= 1
            return True
        
        current = self.head
        while current.next:
            if current.next.data == data:
                current.next = current.next.next
                self.size -= 1
                return True
            current = current.next
        
        return False
    
    def delete_at_index(self, index: int) -> Any:
        """
        Delete the node at the specified index and return its data.
        
        Args:
            index: The position of the node to delete (0-based).
        
        Returns:
            The data from the deleted node.
        
        Raises:
            IndexError: If the index is out of bounds.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        if index < 0 or index >= self.size:
            raise IndexError(f"Index {index} out of bounds for list of size {self.size}")
        
        if index == 0:
            data = self.head.data
            self.head = self.head.next
            self.size -= 1
            return data
        
        current = self.head
        for _ in range(index - 1):
            current = current.next
        
        data = current.next.data
        current.next = current.next.next
        self.size -= 1
        return data
    
    def find(self, data: Any) -> int:
        """
        Find the index of the first occurrence of the given data.
        
        Args:
            data: The data to search for.
        
        Returns:
            The index of the data if found, -1 otherwise.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        current = self.head
        index = 0
        
        while current:
            if current.data == data:
                return index
            current = current.next
            index += 1
        
        return -1
    
    def get(self, index: int) -> Any:
        """
        Get the data at the specified index.
        
        Args:
            index: The position of the node to retrieve (0-based).
        
        Returns:
            The data at the specified index.
        
        Raises:
            IndexError: If the index is out of bounds.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        if index < 0 or index >= self.size:
            raise IndexError(f"Index {index} out of bounds for list of size {self.size}")
        
        current = self.head
        for _ in range(index):
            current = current.next
        
        return current.data
    
    def reverse(self) -> None:
        """
        Reverse the linked list in place.
        
        Time Complexity: O(n)
        Space Complexity: O(1)
        """
        previous = None
        current = self.head
        
        while current:
            next_node = current.next
            current.next = previous
            previous = current
            current = next_node
        
        self.head = previous
    
    def clear(self) -> None:
        """
        Remove all nodes from the list.
        
        Time Complexity: O(1)
        Space Complexity: O(1)
        """
        self.head = None
        self.size = 0
    
    def to_list(self) -> list:
        """
        Convert the linked list to a Python list.
        
        Returns:
            A list containing all the data from the linked list.
        
        Time Complexity: O(n)
        Space Complexity: O(n)
        """
        return list(self)


def demonstrate_linked_list() -> None:
    """Demonstrate the usage of the LinkedList class."""
    print("=== LinkedList Demonstration ===\n")
    
    # Create a new linked list
    ll = LinkedList()
    print(f"Created empty list: {ll}")
    print(f"List length: {len(ll)}")
    print(f"List is empty: {not bool(ll)}\n")
    
    # Add elements
    print("Adding elements...")
    ll.append(1)
    ll.append(2)
    ll.append(3)
    print(f"After appending 1, 2, 3: {ll}")
    
    ll.prepend(0)
    print(f"After prepending 0: {ll}")
    
    ll.insert_at_index(2, 1.5)
    print(f"After inserting 1.5 at index 2: {ll}")
    print(f"List length: {len(ll)}\n")
    
    # Search operations
    print("Search operations...")
    print(f"Find element 2: index {ll.find(2)}")
    print(f"Find element 99: index {ll.find(99)}")
    print(f"Get element at index 3: {ll.get(3)}\n")
    
    # Iteration
    print("Iterating through list:")
    for i, data in enumerate(ll):
        print(f"  Index {i}: {data}")
    print()
    
    # Deletion operations
    print("Deletion operations...")
    print(f"Delete element 1.5: {ll.delete(1.5)}")
    print(f"After deletion: {ll}")
    
    deleted_data = ll.delete_at_index(1)
    print(f"Deleted element at index 1: {deleted_data}")
    print(f"After deletion: {ll}\n")
    
    # Reverse the list
    print("Reversing the list...")
    ll.reverse()
    print(f"After reversal: {ll}\n")
    
    # Convert to Python list
    python_list = ll.to_list()
    print(f"Converted to Python list: {python_list}\n")
    
    # Clear the list
    ll.clear()
    print(f"After clearing: {ll}")
    print(f"List length: {len(ll)}")


if __name__ == "__main__":
    demonstrate_linked_list()