// Generic version of DoublyLinkedListInt.java
// Replace: int data -> E data, DLLNodeInt -> DLLNode<E>
// Key change: indexOf uses .equals() instead of ==
public class DoublyLinkedList<E> {
    private DLLNode<E> head;
    private DLLNode<E> tail;
    private int numElems;

    // creates a linked list with 0 elements; head and tail are null
    public DoublyLinkedList() {
        head = null;
        tail = null;
        numElems = 0;
    }

    // returns the number of elements currently stored in the list
    public int size() {
        return numElems;
    }

    // if index is in bounds: index >= 0 and index < numElems, does nothing
    // if index is out of bounds, displays an error and quits
    private void checkInBounds(int index) {
        if (index < 0 || index >= numElems) {
            System.out.println("Error: List access out of bounds at index " + index);
            System.exit(1);
        }
    }

    // returns node at position index in the list
    // assumes that index satisfies 0 <= index < numElems
    private DLLNode<E> getNode(int index) {
        if (index >= size() / 2) {
            DLLNode<E> current = tail; // node size() - 1
            for (int skips = size() - 1; skips > index; skips--) {
                current = current.getPrevious();
            }
            return current;
        } else {
            DLLNode<E> current = head;
            for (int skips = 0; skips < index; skips++) {
                current = current.getNext();
            }
            return current;
        }
    }

    // returns the element stored in slot index
    public E get(int index) {
        checkInBounds(index);
        return getNode(index).getData();
    }

    // sets slot index to store newElement
    // returns the element previously stored at index
    public E set(int index, E newElement) {
        checkInBounds(index);
        DLLNode<E> current = getNode(index);
        E oldValue = current.getData();
        current.setData(newElement);
        return oldValue;
    }

    // add newElement to the end of the list
    public void add(E newElement) {
        DLLNode<E> newNode = new DLLNode<>(newElement);
        newNode.setPrevious(tail);
        if (numElems > 0) {
            tail.setNext(newNode);
        } else {
            head = newNode;
        }
        tail = newNode;
        numElems++;
    }

    // add newElement to the front of the list
    public void addFirst(E newElement) {
        DLLNode<E> newNode = new DLLNode<>(newElement);
        newNode.setNext(head);
        if (numElems > 0) {
            head.setPrevious(newNode);
        } else {
            tail = newNode;
        }
        head = newNode;
        numElems++;
    }

    // goal: slot index stores newElement; push remaining elements down
    public void add(int index, E newElement) {
        if (index == numElems) {
            add(newElement);
            return;
        }
        checkInBounds(index);
        DLLNode<E> newNode = new DLLNode<>(newElement);
        DLLNode<E> previous = getNode(index - 1);
        DLLNode<E> previousNext = previous.getNext();
        previous.setNext(newNode);
        newNode.setNext(previousNext);
        newNode.setPrevious(previous);
        if (previousNext != null) {
            previousNext.setPrevious(newNode);
        }
        numElems++;
    }

    // removes the element at index, moving all later elements up one slot
    // returns the removed element
    public E remove(int index) {
        checkInBounds(index);
        E toReturn;
        if (index == 0) {
            toReturn = head.getData();
            head = head.getNext();
            if (head != null) {
                head.setPrevious(null);
            } else {
                tail = null;
            }
        } else {
            DLLNode<E> previous = getNode(index - 1);
            DLLNode<E> toRemove = previous.getNext();
            DLLNode<E> nodeAfter = toRemove.getNext();
            previous.setNext(nodeAfter);
            if (nodeAfter != null) {
                nodeAfter.setPrevious(previous);
            } else {
                tail = previous;
            }
            toReturn = toRemove.getData();
        }
        numElems--;
        return toReturn;
    }

    // find the first occurrence of element and return its index
    // if there is no occurrence, return -1
    // Note: uses .equals() instead of == to compare objects
    public int indexOf(E element) {
        DLLNode<E> current = head;
        for (int index = 0; index < numElems; index++) {
            if (element.equals(current.getData())) {
                return index;
            }
            current = current.getNext();
        }
        return -1;
    }

    // returns true if element is in the list; false otherwise
    public boolean contains(E element) {
        return indexOf(element) != -1;
    }

    public String toString() {
        if (numElems == 0) {
            return "[ ]";
        }
        String ret = "[";
        DLLNode<E> current = head;
        for (int index = 0; index < numElems - 1; index++) {
            ret += current.getData() + ", ";
            current = current.getNext();
        }
        ret += current.getData() + "]";
        return ret;
    }

    public static void main(String[] args) {
        DoublyLinkedList<String> test = new DoublyLinkedList<>();
        test.add("hello");
        test.add("world");
        test.add("CS");
        System.out.println("Added hello, world, CS");
        System.out.println(test);
        System.out.println("New element 136 at slot 1");
        test.add(1, "136");
        System.out.println(test);
        System.out.println("indexOf 'world': " + test.indexOf("world"));
        System.out.println("indexOf 'xyz':   " + test.indexOf("xyz"));
        test.remove(2);
        System.out.println("Remove element at slot 2");
        System.out.println(test);
    }
}
