package org.academiadecodigo.bootcamp.containers;

import java.util.Iterator;
import java.util.NoSuchElementException;

public class LinkedList<T> implements Iterable<T> {

    private Node head;
    private int length = 0;

    public LinkedList() {
        this.head = new Node(null);
    }

    public int size() {
        return length;
    }

    /**
     * Adds an element to the end of the list
     *
     * @param data the element to add
     */
    public void add(T data) {

        Node node = new Node(data);
        Node iterator = head;
        while (iterator.getNext() != null) {
            iterator = iterator.getNext();
        }
        iterator.setNext(node);
        length++;
    }


    /**
     * Adds an element at requested index of the list
     *
     * This will push the current object at the same index
     * forward.
     *
     * @param index index at list to place object at
     * @param data the element to add
     */
    public void add(int index, T data) {
        // if index is negative or larger than length, do a simple add()
        if (index < 0 || index >= length) {
            add(data);
            return;
        }

        Node node = new Node(data);
        Node current = head.getNext();
        Node previous = head;
        int counter = 0;

        while (counter++ != index) {
            previous = current;
            current = current.getNext();
        }

        node.setNext(current);
        previous.setNext(node);
        length++;
    }

    /**
     * Obtains an element by index
     *
     * @param index the index of the element
     * @return the element
     */
    public T get(int index) {
        // don't bother traversing list if index is equal or bigger than length
        // if negative, subtract it from length
        if (index >= length || length + index < 0) {
            return null;
        } else if (index < 0) {
            return get(length + index);
        }

        Node next = head.getNext();

        // iterate through linked list until we reach the requested idx
        for (int i = index; i > 0; i--) {
            next = next.getNext();
        }

        // return that node's data, not the node.
        return next.getData();
    }

    /**
     * Returns the index of the element in the list
     *
     * @param data element to search for
     * @return the index of the element, or -1 if the list does not contain element
     */
    public int indexOf(T data) {
        int counter = 0;
        Node iterator = head.getNext();
        while (iterator != null) {
            // check if it contains the data we're looking for
            if (iterator.getData().equals(data)) {

                // return the node's index if we did
                return counter;
            }

            // go get the next node
            iterator = iterator.getNext();

            counter++;
        }

        // we couldn't find the data we were looking for
        return -1;
    }

    /**
     * Removes an element from the list
     *
     * @param data the element to remove
     * @return true if element was removed
     */
    public boolean remove(T data) {
        // don't poop ourselves if user tries to remove null
        if (data == null) {
            return false;
        }

        // where we start the iteration
        Node current = head.getNext();

        // where the previous node will be kept
        Node previous = head;

        // iterate through the whole list
        while (current != null) {
            // check if current node contains the data we're looking for
            if (current.getData().equals(data)) {
                // if it does, set the previous'
                // node next reference to this node's next
                previous.setNext(current.getNext());

                // this will remove any reference to 'current'
                length--;
                return true;
            }

            // set previous to the "current" node
            previous = current;

            // set current to the reference to the next node
            current = current.getNext();
        }
        // if we reached the tail without finding the data
        // it means we couldn't remove anything
        return false;
    }


    /**
     * Removes the element at index from the list
     *
     * Alternatively, could be reimplemented with a simple
     * <code>remove(get(index))</code>
     * but this allows repeated object data.
     *
     * @param index the index of object to remove
     * @return true if index was removed
     */
    public boolean remove(int index) {
        // don't bother removing negative or larger than length nodes
        if (length + index < 0 || index >= length) {
            return false;
        } else if (index < 0) {
            remove(length + index);
        }

        Node current = head.getNext();
        Node previous = head;

        for (int idx = index; idx > 0; idx--) {
            previous = current;
            current = current.getNext();
        }

        previous.setNext(current.getNext());
        length--;
        return true;
    }

    @Override
    public Iterator<T> iterator() {
        return new IdxIterator();
    }

    private class Node {

        private T data;
        private Node next;

        public Node(T data) {
            this.data = data;
            next = null;
        }

        public T getData() {
            return data;
        }

        public void setData(T data) {
            this.data = data;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

    private class IdxIterator implements Iterator<T> {

        private int current;
        private int lastRemoved;

        public IdxIterator() {
            current = -1;
        }

        @Override
        public boolean hasNext() {
            return current + 1 < length;
        }

        @Override
        public T next() {
            if (!hasNext())
                throw new NoSuchElementException();

            return get(++current);
        }

        @Override
        public void remove() {
            if (current < 0 || lastRemoved == current + 1) {
                throw new IllegalStateException();
            }

            lastRemoved = current;
            LinkedList.this.remove(current--);
        }
    }
}
