public class LinkedList<E> {
    protected Node<E> head;
    private int numElems;

    // creates a linked list with 0 elements; head is null
    public LinkedList() {
        head = 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); // exit the program
        }
    }

    // returns node index in the list
    // assumes that index satisfies 0 <= index < numElems
    protected Node<E> getNode(int index) {
        Node<E> current = head;
        for (int skips = 0; skips < index; skips++) {
            // current is now the skips-th node in the list
            current = current.getNext();
        }
        return current;
    }

    // returns the int stored in slot index
    public E get(int index) {
        checkInBounds(index);
        Node<E> current = getNode(index);
        return current.getData();
    }

    // sets slot index to store newElement
    // returns the int previously stored at index
    public E set(int index, E newElement) {
        checkInBounds(index);
        Node<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) {

        Node<E> newNode = new Node<E>(newElement);
        if (numElems == 0) {
            head = newNode;
        } else {
            Node<E> lastInList = getNode(size() - 1);
            lastInList.setNext(newNode);
        }
		numElems++;
    }


    // goal: arr[index] stores newElement
    // push remaining elements down
    public void add(int index, E newElement) {
        // check is different because numElems is allowed
        if (index < 0 || index > numElems) {
            System.out.println("Error: List access out of bounds at index " + index);
            System.exit(1); // exit the program
        }
        Node<E> newNode = new Node<E>(newElement);
        if (index == 0) {
            newNode.setNext(head);
            head = newNode;
        } else {
            Node<E> previous = getNode(index - 1); // find the node immediately before the index-th node
            Node<E> previousnext = previous.getNext();
            previous.setNext(newNode);
            newNode.setNext(previousnext);
        }
        numElems++;
    }

    /*
     * NodeInt previousNext = previous.getNext(); // find the node immediately after
     * previous
     * previous.setNext(newNode); // set it to point to our new node
     * newNode.setNext(previousNext); // continue the list
     */
    // 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();
        } else {
            Node<E> previous = getNode(index - 1);
            Node<E> toRemove = previous.getNext();
            previous.setNext(toRemove.getNext());
            toReturn = toRemove.getData();
        }
        numElems--;
        return toReturn;
    }

    // find the first occurrence of element and return its index
    // if there is no occurrenct, return -1
    public int indexOf(E element) {
        Node<E> current = head;
        for (int index = 0; index < numElems; index++) {
            if (current.getData().equals( element)) {
                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 = "[";
        Node<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) {
        LinkedList<Integer> test = new LinkedList<Integer>();

        test.add(10);
        test.add(20);
        test.add(30);
        test.add(40);
        System.out.println("Added elements 10, 20, 30, 40");
        System.out.println(test);
        System.out.println("New element 100 at slot 1; push remainder down");
        test.add(1, 100);
        System.out.println(test);
        System.out.println("Testing indexOf: 30 is at slot " + test.indexOf(30));
        System.out.println("Testing indexOf: 7 is at slot " + test.indexOf(7));
        test.remove(2);
        System.out.println("Remove the element at slot 2");
        System.out.println(test);
        System.out.println("Now, 30 is at slot " + test.indexOf(30));

    }
}
