/home/caleb/ASDV-Java/Semester 3/Assignments/MP6_CalebFontenot/src/main/java/edu/slcc/asdv/caleb/mp6_calebfontenot/PriorityQueueASDV.java
/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package edu.slcc.asdv.caleb.mp6_calebfontenot;

/**
 *
 * @author caleb
 */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.function.Consumer;

public class PriorityQueueASDV<E extends Comparable<E>>
        implements Queue<E>, Cloneable {

    private Node<E> head;//head
    private Node<E> tail;//tail

    class Node<E> {

        E e;
        Node<E> l;
        Node<E> r;
    }

    /**
     * Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions, returning true upon success and throwing an IllegalStateException if no space is currently available.
     *
     * Specified by: add in interface Collection<E>
     * Parameters: e - the element to add Returns: true (as specified by Collection.add(E)) Throws: IllegalStateException - if the element cannot be added at this time due to capacity restrictions ClassCastException - if the class of the specified element prevents it from being added to this queue NullPointerException - if the specified element is null and this queue does not permit null elements IllegalArgumentException - if some property of this element prevents it from being added to this queue
     *
     * @param e - the element to add
     * @return true if this collection changed as a result of the call
     * @throws IllegalStateException - if the element cannot be added at this time due to capacity restrictions
     * @throws ClassCastException - if the class of the specified element
     * @throws NullPointerException - if the specified element is null and this queue does not permit null elements
     * @throws IllegalArgumentException - if some property of this element prevents it from being added to this queue
     */
    @Override
    public boolean add(E e) {
        if (e == null) {
            throw new NullPointerException("NULL elements not allowed!");
        }

        Node<E> newNode = new Node<E>();
        newNode.e = e;

        //1. empty queue
        if (this.head == null && this.tail == null) {
            this.head = this.tail = newNode;
            return true;
        }

        int index = findCorrectPositionToInsertElement(e);
        //int index = findCorrectPositionToInsertElementHashCode(e);

        //2. we add at size ( last node)
        if (index == size()) {
            tail.r = newNode;//1
            newNode.l = tail;//2
            tail = newNode;//3
        } //3. we add at 0 in the front
        else if (index == 0) {
            newNode.r = head;
            this.head.l = newNode;
            this.head = newNode;
            if (size() == 1) {
                tail = head;
            }
        } //4. we add in the middle
        else {
            Node<E> p = head;

            for (int i = 0; i < index - 1; ++i) {
                p = p.r;
            }

            //after for loop p point one position before insertion
            newNode.l = p;//we connect the left of the new node 
            //to the node that is BEFORE 
            //the node to be inserted

            newNode.r = p.r;//we connect the right of the new node
            // to the node thta is AFTER 
            //the node to be inserted

            p.r = newNode; //we connect the right the  node BEFORE the node
            //to be inserted to the new node

            p.r.r.l = newNode;//we connect the left of the node AFTER the node 
            //to be iserted to the new node
        }

        return true;
    }

    @Override
    public int size() {
        Node<E> p = this.head;
        int count = 0;
        while (p != null) {
            p = p.r;
            count++;
        }
        return count;
    }

    private int findCorrectPositionToInsertElement(E e) {
        Node<E> p = this.head;
        int pos = 0;
        while (p != null) {
            if (e.compareTo(p.e) > 0) {
                p = p.r;
                ++pos;
            } else {
                break;
            }
        }

        return pos;
    }

    private int findCorrectPositionToInsertElementHashCode(E e) {
        Node<E> p = this.head;
        int pos = 0;
        while (p != null) {
            if (e.hashCode() > p.e.hashCode()) {
                p = p.r;
                ++pos;
            } else {
                break;
            }
        }

        return pos;
    }

    /**
     * Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions. When using a capacity-restricted queue, this method is generally preferable to add(E), which can fail to insert an element only by throwing an exception.
     *
     * @param e - the element to add
     * @throws IllegalStateException - if the element cannot be added at this time due to capacity restrictions
     * @throws ClassCastException - if the class of the specified element
     * @throws NullPointerException - if the specified element is null and this queue does not permit null elements
     * @throws IllegalArgumentException - if some property of this element prevents it from being added to this queue
     * @return true if the element was added
     */
    @Override
    public boolean offer(E e) {
        return add(e);
    }

    /**
     * Retrieves and removes the head of this queue. This method differs from {@link #poll poll} only in that it throws an exception if this queue is empty.
     *
     * <p>
     * This implementation returns the result of {@code poll} unless the queue is empty.
     *
     * @return the head of this queue
     * @throws NoSuchElementException if this queue is empty
     */
    @Override
    public E remove() {
        Node<E> pointer = head.r;
        E removedElement = head.e;
        head = head.r;
        head.l = null;

        return removedElement;
    }

    /**
     * Retrieves and removes the head of this queue, or returns null if this queue is empty.
     *
     * Returns: the head of this queue, or null if this queue is empty
     *
     * @return
     */
    @Override
    public E poll() {
        if (size() == 0) {
            return null;
        }
        if (size() > 1) {
            head = head.r;

            E e = head.l.e;
            head.l = null;
            return e;
        } else //size 1
        {
            E e = head.e;
            head = tail = null;
            return e;
        }
    }

    /**
     * Retrieves, but does not remove, the head of this queue. This method differs from {@link #peek peek} only in that it throws an exception if this queue is empty.
     *
     * @return the head of this queue
     * @throws NoSuchElementException if this queue is empty
     */
    @Override
    public E element() {
        if (head != null) {
            return (E) head;
        } else {
            throw new NoSuchElementException("Element does not exist.");
        }
    }

    /**
     * Retrieves, but does not remove, the head of this queue, or returns {@code null} if this queue is empty.
     *
     * @return the head of this queue, or {@code null} if this queue is empty
     */
    @Override
    public E peek() {
        return (E) head;
    }

    @Override
    public boolean isEmpty() {
        return head == null && tail == null ? true : false;

    }

    /**
     * Returns {@code true} if this collection contains the specified element. More formally, returns {@code true} if and only if this collection contains at least one element {@code e} such that {@code Objects.equals(o, e)}.
     *
     * @param o element whose presence in this collection is to be tested
     * @return {@code true} if this collection contains the specified element
     * @throws ClassCastException if the type of the specified element is incompatible with this collection ({@linkplain Collection##optional-restrictions optional})
     * @throws NullPointerException if the specified element is null and this collection does not permit null elements ({@linkplain Collection##optional-restrictions optional})
     */
    @Override
    public boolean contains(Object o) {
        Node<E> pointer = head;
        do {
            if (pointer.equals(o)) {
                return true;
            } else {
                pointer = pointer.r;
            }
        } while (pointer != null);
        return false;
    }

    @Override
    public Iterator<E> iterator() {
        Iterator<E> it = new Iterator<E>() {
            Node<E> p = head;

            @Override
            public boolean hasNext() {
                return p == null ? false : true;
            }

            @Override
            public E next() {
                if (hasNext() == false) {
                    throw new NoSuchElementException("the is no next element");
                }
                E e = p.e;
                p = p.r;
                return e;
            }

            @Override
            public void forEachRemaining(Consumer<? super E> action) {
                while (hasNext()) {
                    action.accept(p.e);
                    p = p.r;
                }
            }
        };

        return it;

    }

    @Override
    public Object[] toArray() {
        Node<E> pointer = head;
        Object[] returnArray = new Object[this.size()];
        int i = 0;
        while (pointer.r != null) {
            returnArray[i++] = pointer.e;
            pointer = pointer.r;
        }
        returnArray[i++] = pointer.e;
        return returnArray;
    }

    /**
     * Returns an array containing all of the elements in this collection; the runtime type of the returned array is that of the specified array. If the collection fits in the specified array, it is returned therein. Otherwise, a new array is allocated with the runtime type of the specified array and the size of this collection.
     *
     * <p>
     * If this collection fits in the specified array with room to spare (i.e., the array has more elements than this collection), the element in the array immediately following the end of the collection is set to {@code null}. (This is useful in determining the length of this collection <i>only</i> if the caller knows that this collection does not contain any {@code null} elements.)
     *
     * <p>
     * If this collection makes any guarantees as to what order its elements are returned by its iterator, this method must return the elements in the same order.
     *
     * @apiNote This method acts as a bridge between array-based and collection-based APIs. It allows an existing array to be reused under certain circumstances. Use {@link #toArray()} to create an array whose runtime type is {@code Object[]}, or use {@link #toArray(IntFunction)} to control the runtime type of the array.
     *
     * <p>
     * Suppose {@code x} is a collection known to contain only strings. The following code can be used to dump the collection into a previously allocated {@code String} array:
     *
     * <pre>
     *     String[] y = new String[SIZE];
     *     ...
     *     y = x.toArray(y);</pre>
     *
     * <p>
     * The return value is reassigned to the variable {@code y}, because a new array will be allocated and returned if the collection {@code x} has too many elements to fit into the existing array {@code y}.
     *
     * <p>
     * Note that {@code toArray(new Object[0])} is identical in function to {@code toArray()}.
     *
     * @param <T> the component type of the array to contain the collection
     * @param a the array into which the elements of this collection are to be stored, if it is big enough; otherwise, a new array of the same runtime type is allocated for this purpose.
     * @return an array containing all of the elements in this collection
     * @throws ArrayStoreException if the runtime type of any element in this collection is not assignable to the {@linkplain Class#getComponentType
     *         runtime component type} of the specified array
     * @throws NullPointerException if the specified array is null
     */
    @Override
    public <T> T[] toArray(T[] a) {
        a = Arrays.copyOf(a, this.size());
        Node<E> pointer = head;
        System.out.println(a.getClass());
        System.out.println(pointer.getClass());
        System.out.println(pointer.e.getClass());

        for (int i = 0; i < this.size(); ++i) {
            a[i] = (T) pointer.e;
            pointer = pointer.r;
        }
        return a;
    }

    /**
     * Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that (o==null ? e==null : o.equals(e)), if this collection contains one or more such elements. Returns true if this collection contained the specified element (or equivalently, if this collection changed as a result of the call).
     *
     * @param o - element to be removed from this collection, if present
     * @throws ClassCastException - if the type of the specified element is incompatible with this collection
     * @throws NullPointerException - if the specified element is null and this collection does not permit null elements
     * @return true if an element was removed as a result of this call
     */
    @Override
    public boolean remove(Object o) {
        if (o == null) {
            throw new NullPointerException("null vales not allowed");
        }
        if (size() == 0) {
            return false;
        }

        Node<E> p = this.head;
        int pos = 0;
        while (p != this.tail) {
            if (p.e.equals(o)) {
                if (size() == 1) {
                    this.head = this.tail = null;
                    return true;
                }
                this.removeAt(pos, (E) o);
                break;
            }
            ++pos;
            p = p.r;
        }
        if (p == tail && p.e.equals(o)) {
            this.removeAt(size() - 1, (E) o);
        }
        return true;
    }

    /**
     *
     * @param pos
     * @param e
     * @throws IndexOutOfBoundsException - if pos less 0 OR pos greater-equal size()
     * @return
     */
    private boolean removeAt(int pos, E e) {
        if (pos < 0 || pos >= size()) {
            throw new IndexOutOfBoundsException(pos + " is out of bounds");
        }
        //1.list is empty
        if (isEmpty()) {
            return false;
        } //2. one node exists
        else if (size() == 1) {
            this.head = this.tail = null;
        } //3. remove in the front( head)
        else if (pos == 0) {
            this.head = this.head.r;
            head.l = null;
        } //4. remove in the end ( tail)
        else if (pos == size() - 1) {
            this.tail = this.tail.l;
            this.tail.r = null;
        } //5. remove in the middle ( at least 3 nodes are in the queue)
        else {
            Node<E> p = head;
            for (int i = 0; i < pos - 1; ++i) {
                p = p.r;
            }
            p.r = p.r.r;
            p.r.l = p;
        }
        return true;
    }

    /**
     * Returns {@code true} if this collection contains all of the elements in the specified collection.
     *
     * @param c collection to be checked for containment in this collection
     * @return {@code true} if this collection contains all of the elements in the specified collection
     * @throws ClassCastException if the types of one or more elements in the specified collection are incompatible with this collection ({@linkplain Collection##optional-restrictions optional})
     * @throws NullPointerException if the specified collection contains one or more null elements and this collection does not permit null elements ({@linkplain Collection##optional-restrictions optional}) or if the specified collection is null.
     * @see #contains(Object)
     */
    @Override
    public boolean containsAll(Collection<?> c) {
        // Java already throws a CastCastException if you give it the wrong type, so we don't have to throw that ourselves
        if (c.contains(null) || c == null) {
            throw new NullPointerException("The collection you passed to containsAll() contains a null element. Cannot continue.");
        }
        // Unpack the collection so we can compare them
        Object[] compareArray = c.toArray();
        Node<E> pointer = null;
        int matchCount = 0;
        for (Object compare : compareArray) {
            pointer = head;
            for (int i = 0; i < size() - 1; ++i) {
                if (pointer.e.equals(compare)) {
                    matchCount++;
                }
                pointer = pointer.r;
            }
        }
        if (matchCount == compareArray.length - 1) {
            return true;
        }
        return false;
    }

    /**
     * Adds all of the elements in the specified collection to this collection (optional operation). The behavior of this operation is undefined if the specified collection is modified while the operation is in progress. (This implies that the behavior of this call is undefined if the specified collection is this collection, and this collection is nonempty.)
     *
     * @param c - collection containing elements to be added to this collection
     * @throws ClassCastException - if the class of an element of the specified collection prevents it from being added to this collection.
     * @throws NullPointerException - if the specified collection contains a null element and this collection does not permit null elements, or if the specified collection is null
     * @throws IllegalArgumentException - if some property of an element of the specified collection prevents it from being added to this collection
     * @throws IllegalArgumentException - if some property of an element of the specified collection prevents it from being added to this collection
     * @return true if this collection changed as a result of the call
     */
    @Override
    public boolean addAll(Collection<? extends E> c) {
        int sizeBefore = size();
        for (E e : c) {
            add(e);
        }
        int sizeAfter = size();
        return sizeAfter > sizeBefore;
    }

    /**
     * Removes all of this collection's elements that are also contained in the specified collection (optional operation). After this call returns, this collection will contain no elements in common with the specified collection.
     *
     * @implSpec This implementation iterates over this collection, checking each element returned by the iterator in turn to see if it's contained in the specified collection. If it's so contained, it's removed from this collection with the iterator's {@code remove} method.
     *
     * <p>
     * Note that this implementation will throw an {@code UnsupportedOperationException} if the iterator returned by the {@code iterator} method does not implement the {@code remove} method and this collection contains one or more elements in common with the specified collection.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     *
     * @see #remove(Object)
     * @see #contains(Object)
     */
    @Override
    public boolean removeAll(Collection<?> c) {
        if (c.contains(null) || c == null) {
            throw new NullPointerException("The collection you passed to removeAll() contains a null element. Cannot continue.");
        }
        // Unpack the collection so we can remove them
        Object[] compareArray = c.toArray();
        Node<E> pointer = null;
        boolean removeSuccessful = false;
        for (Object compare : compareArray) {
            pointer = head;
            for (int i = 0; i < size() - 1; ++i) {
                if (pointer.e.equals(compare)) {
                    remove(pointer.e);
                    removeSuccessful = true;
                }
                pointer = pointer.r;
            }
        }
        return removeSuccessful;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        if (c.contains(null) || c == null) {
            throw new NullPointerException("The collection you passed to retainAll() contains a null element. Cannot continue.");
        }
        Node<E> pointer = null;
        boolean removeSuccessful = false;
        for (int j = 0; j < c.size() - 1; ++j) {
            pointer = head;
            for (int i = 0; i < size(); ++i) {
                if (!c.contains(pointer.e)) {
                    remove(pointer.e);
                    removeSuccessful = true;
                }
                pointer = pointer.r;
            }
        }
        return removeSuccessful;
    }

    @Override
    public void clear() {
        // head = tail = null;

        //extra, no necessary to set the link of every node
        Node<E> p = head;
        while (p != tail) {
            p = p.r;
            p.l = null;
        }
        head = tail = null;
    }

    @Override
    public void forEach(Consumer<? super E> action) {
        //1. use a pointer that points to the head
        //2. while the pointer has not reached the end of the queue 
        //consume it
        Node<E> p = head;
        while (p != null) {
            action.accept(p.e);
            p = p.r;
        }

    }

    @Override
    public String toString() {
        String s = "PriorityQueueASDV {";
        Node<E> p = head;
        while (p != null) {
            s += p.e.toString();
            if (p != tail) {
                s += ", ";
            }
            p = p.r;
        }

        s += "}";
        return s;
    }

    @Override
    protected Object clone()
            throws CloneNotSupportedException {
        PriorityQueueASDV<E> c = (PriorityQueueASDV<E>) super.clone();
        return c;
    }

    public static void main(String[] args) {
        System.out.println("\n--> PriorityQueueASDV  testing add");
        PriorityQueueASDV<String> pq1 = new PriorityQueueASDV();
        pq1.add("Paris");
        pq1.add("Athens");
        pq1.add("London");
        pq1.add("Lafayette");
        pq1.add("Berlin");

        System.out.println(pq1);

        System.out.println("\n--> Colllections PriorityQueue testing add");

        PriorityQueue<String> pq2 = new PriorityQueue();
        pq2.add("Paris");
        pq2.add("Athens");
        pq2.add("London");
        pq2.add("Lafayette");
        pq2.add("Berlin");

        System.out.println(pq2);

        //TEST IT FULLY HERE. FOR ALL METHODS AND ALL CASES.
        //Have the Jzva PriorityQueue below
        System.out.println("\n--> PriorityQueueASDV  testing remove(object o)");
        System.out.println("\n\tremove from front Athens");
        pq1.remove("Athens");
        pq2.remove("Athens");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\tremove from end Paris");
        pq1.remove("Paris");
        pq2.remove("Paris");
        System.out.println(pq1);
        System.out.println(pq1);

        System.out.println("\n\tremove from the middle Lafayette");
        pq1.remove("Lafayette");
        pq2.remove("Lafayette");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\tadd at the end Stocholm");
        pq1.add("Stocholm");
        pq2.add("Stocholm");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\tremove from the middle London");
        pq1.remove("London");
        pq2.remove("London");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\tremove from the front Berlin");
        pq1.remove("Berlin");
        pq2.remove("Berlin");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\tremove from the front/end Stocholm");
        pq1.remove("Stocholm");
        pq2.remove("Stocholm");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\tremove from empty queue");
        pq1.remove("Stocholm");
        pq2.remove("Stocholm");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n--> PriorityQueueASDV  recreate priority queues from empty");
        pq1.add("Paris");
        pq1.add("Athens");
        pq1.add("London");
        pq1.add("Lafayette");
        pq1.add("Berlin");
        pq1.add("Zurich");

        pq2.add("Paris");
        pq2.add("Athens");
        pq2.add("London");
        pq2.add("Lafayette");
        pq2.add("Berlin");
        pq2.add("Zurich");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\n+++HERE YOU TEST ALL YOUR METHODS FULLY, and the methods of Colleciion PriorityQueue");

        System.out.println("\n\t  offer New York");
        pq1.offer("New York");
        pq2.offer("New York");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  offer Miami");
        pq1.offer("Miami");
        pq2.offer("Miami");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  offer null");
        try {
            pq1.offer(null);
        } catch (Exception e) {
            System.err.println(e);
        }

        try {
            pq2.offer(null);
        } catch (Exception e) {
            System.err.println(e);
        }
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  offer ClassCastException with Object");
        try {
            pq1.offer((String) new Object());
        } catch (Exception e) {
            System.err.println(e);
        }

        try {
            pq2.offer((String) new Object());
        } catch (Exception e) {
            System.err.println(e);
        }
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  poll suposed to be Athens");
        System.out.println(pq1.poll());
        System.out.println(pq2.poll());
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  Iterator");
        Iterator<String> it1 = pq1.iterator();
        Iterator<String> it2 = pq2.iterator();

        while (it1.hasNext()) {
            System.out.print(it1.next() + " ");
        }

        System.out.println("");

        while (it2.hasNext()) {
            System.out.print(it2.next() + " ");
        }
        System.out.println("");

        System.out.println("\n\t  Iterator NoSuchElementException ");

        try {
            System.out.println(it1.next());
        } catch (NoSuchElementException e) {
            System.err.println(e);
        }
        try {
            System.out.println(it2.next());
        } catch (NoSuchElementException e) {
            System.err.println(e);
        }

        System.out.println("\n\t  Iterator foreach ");
        it1 = pq1.iterator();
        it2 = pq2.iterator();
        it1.forEachRemaining(new Consumer() {
            @Override
            public void accept(Object t) {
                System.out.print(t + "***  ");
            }
        });
        System.out.println("");
        it2.forEachRemaining(new Consumer() {
            @Override
            public void accept(Object t) {
                System.out.print(t + "+++  ");
            }
        });
        System.out.println("");

        System.out.println("\n\t  addAll  Houston Chicago");
        List<String> ar1 = Arrays.asList("Houston", "Chicago");
        pq1.addAll(ar1);
        pq2.addAll(ar1);
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  clear");
        pq1.clear();
        pq2.clear();
        System.out.println(pq1);
        System.out.println(pq2);
        System.out.println("");

        System.out.println("\n--> PriorityQueueASDV  recreate priority queues from empty");
        pq1.add("Paris");
        pq1.add("Athens");
        pq1.add("London");
        pq1.add("Lafayette");
        pq1.add("Berlin");
        pq1.add("Zurich");

        pq2.add("Paris");
        pq2.add("Athens");
        pq2.add("London");
        pq2.add("Lafayette");
        pq2.add("Berlin");
        pq2.add("Zurich");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("\n\t  forEach");
        pq1.forEach(new Consumer() {
            @Override
            public void accept(Object t) {
                System.out.print(t + "*** ");
            }
        });
        System.out.println("");
        pq2.forEach(new Consumer() {
            @Override
            public void accept(Object t) {
                System.out.print(t + "+++ ");
            }
        });
        System.out.println("");

        System.out.println("\n\t  clone");
        try {
            PriorityQueueASDV<String> pq1Cloned
                    = (PriorityQueueASDV<String>) pq1.clone();
            System.out.println(pq1Cloned);
            pq1Cloned.add("Las Vegas");
            System.out.println(pq1Cloned);
            System.out.println(pq1);

        } catch (CloneNotSupportedException e) {
            System.err.println(e);
        }
        pq1.clear();
        pq2.clear();
        pq1.add("Paris");
        pq1.add("Athens");
        pq1.add("London");
        pq1.add("Lafayette");
        pq1.add("Berlin");
        pq1.add("Zurich");

        pq2.add("Paris");
        pq2.add("Athens");
        pq2.add("London");
        pq2.add("Lafayette");
        pq2.add("Berlin");
        pq2.add("Zurich");
        System.out.println("----------------");
        System.out.println(pq1);
        System.out.println(pq2);

        System.out.println("Attempt to remove an element.");
        pq1.remove();
        pq2.remove();
        System.out.println(pq1);
        System.out.println(pq2);
        System.out.println("Get array of the priority queues.");
        Object pqArray1[] = pq1.toArray();
        Object pqArray2[] = pq2.toArray();

        printArrays(pqArray1);
        printArrays(pqArray2);
        System.out.println();
        System.out.println("----------------");
        System.out.println("Test .toArray(T[])");
        String[] pqArray3 = pq1.toArray(new String[0]);
        printArrays(pqArray3);
        System.out.println("----------------");
        System.out.println("Test containsAll()");
        ArrayList<String> testArray = new ArrayList<>();
        testArray.add("Lafayette");
        testArray.add("Berlin");
        testArray.add("Zurich");
        System.out.println("Does pq1 contain Lafayette, Berlin, and Zurich? " + (pq1.containsAll(testArray) ? "yes" : "no"));
        System.out.println("Does pq1 contain the contents of pq2? " + (pq1.containsAll(pq2) ? "yes" : "no"));
        System.out.println("Does pq2 contain the contents of pq1? " + (pq2.containsAll(pq1) ? "yes" : "no"));
        System.out.println("Adding funkytown to testArray...");
        testArray.add("Funkytown");
        System.out.println("Does pq1 contain Lafayette, Berlin, Zurich, and Funkytown? " + (pq1.containsAll(testArray) ? "yes" : "no"));
        System.out.println("Test if containsAll() correctly throws a NullPointerException...");
        try {
            testArray.add(null);
            System.out.println("Does pq1 contain Lafayette, Berlin, Zurich, Funkytown, and null? " + (pq1.containsAll(testArray) ? "yes" : "no"));
        } catch (NullPointerException ex) {
            System.out.println(ex);
        }
        testArray.remove(null);
        System.out.println("That worked! Continuing with tests...");
        System.out.println("----------------");
        System.out.println(pq1);
        System.out.println(pq2);
        System.out.println("Testing removeAll(Collection<?>)...");
        System.out.println("Removing the elements in the test array...");
        pq1.removeAll(testArray);
        pq2.removeAll(testArray);
        System.out.println(pq1);
        System.out.println(pq2);
        System.out.println("----------------");
        System.out.println("Testing retainAll()...");
        ArrayList<String> testArray2 = new ArrayList<>();
        testArray2.add("London");
        testArray2.add("Paris");
        pq1.retainAll(testArray2);
        pq2.retainAll(testArray2);
        System.out.println(pq1);
        System.out.println(pq2);
    }

    static void printArrays(Object[] arr) {
        for (Object element : arr) {
            System.out.print(element + ", ");
        }
    }

}