package net.ranides.assira.collection.lists;

import net.ranides.assira.annotations.Meta;
import net.ranides.assira.generic.SerializableCode;

import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;

/**
 * This list is a view which takes input list (or array) of values and does not copy them at construction time.
 * When you try to modify this list, it will detach from original source and copy elements "on first write".
 * Then, detached list will behave as ordinary mutable list.
 *
 * It is usefull when we want to return mutable copies of internal data, but we want to avoid unnecessary copying.
 *
 * Please note, that this list protected from "client" changes, but it does not protect against changes made internally
 * to source collection. If you change source collection, not detached CopyList will reflect those changes.
 *
 * @param <T> T
 */
public class CopyList<T> implements List<T>, Serializable {

    private transient List<T> source;

    private transient boolean original;

    /**
     * Create new list from specified array.
     * It does not copy provided array at construction time.
     *
     * @param source source
     */
    public CopyList(T[] source) {
        this.source = Arrays.asList(source);
        this.original = true;
    }

    /**
     * Create new list from specified array.
     * It does not copy provided array at construction time.
     *
     * @param source source
     */
    public CopyList(List<T> source) {
        this.source = source;
        this.original = true;
    }

    @Override
    public int size() {
        return source.size();
    }

    @Override
    public boolean isEmpty() {
        return source.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return source.contains(o);
    }

    @Override
    public void forEach(Consumer<? super T> action) {
        source.forEach(action);
    }

    @Override
    public Object[] toArray() {
        return source.toArray();
    }

    @Override
    public <E> E[] toArray(E[] a) {
        return source.toArray(a);
    }

    @Override
    public boolean add(T t) {
        detach();
        return source.add(t);
    }

    @Override
    public boolean remove(Object o) {
        detach();
        return source.remove(o);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return source.containsAll(c);
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {
        detach();
        return source.addAll(c);
    }

    @Override
    public boolean addAll(int index, Collection<? extends T> c) {
        detach();
        return source.addAll(index, c);
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        detach();
        return source.removeAll(c);
    }

    @Override
    public boolean removeIf(Predicate<? super T> filter) {
        detach();
        return source.removeIf(filter);
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        detach();
        return source.retainAll(c);
    }

    @Override
    public void replaceAll(UnaryOperator<T> operator) {
        detach();
        source.replaceAll(operator);
    }

    @Override
    public void sort(Comparator<? super T> c) {
        detach();
        source.sort(c);
    }

    @Override
    public void clear() {
        if (!original) {
            source.clear();
        } else {
            source = Collections.emptyList();
        }
    }

    @Override
    public T get(int index) {
        return source.get(index);
    }

    @Override
    public T set(int index, T element) {
        detach();
        return source.set(index, element);
    }

    @Override
    public void add(int index, T element) {
        detach();
        source.add(index, element);
    }

    @Override
    public T remove(int index) {
        detach();
        return source.remove(index);
    }

    @Override
    public int indexOf(Object o) {
        return source.indexOf(o);
    }

    @Override
    public int lastIndexOf(Object o) {
        return source.lastIndexOf(o);
    }

    @Override
    public Iterator<T> iterator() {
        if (!original) {
            return source.iterator();
        } else {
            return new CopyIterator(source.listIterator());
        }
    }

    @Override
    public ListIterator<T> listIterator() {
        if (!original) {
            return source.listIterator();
        } else {
            return new CopyIterator(source.listIterator());
        }
    }

    @Override
    public ListIterator<T> listIterator(int index) {
        if (!original) {
            return source.listIterator(index);
        } else {
            return new CopyIterator(source.listIterator(index));
        }
    }

    @Override
    public List<T> subList(int fromIndex, int toIndex) {
        if(!original) {
            return source.subList(fromIndex, toIndex);
        } else {
            return new CopyFragment(null, fromIndex, toIndex);
        }
    }

    @Override
    public Spliterator<T> spliterator() {
        return source.spliterator();
    }

    @Override
    public Stream<T> stream() {
        return source.stream();
    }

    @Override
    public Stream<T> parallelStream() {
        return source.parallelStream();
    }

    @Override
    public int hashCode() {
        return source.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof CopyList) {
            return source.equals(((CopyList<?>)obj).source);
        }
        return source.equals(obj);
    }

    @Override
    public String toString() {
        return source.toString();
    }

    private void writeObject(java.io.ObjectOutputStream ostream) throws java.io.IOException {
        ostream.defaultWriteObject();
        ostream.writeInt(source.size());
        for(T v : source) {
            ostream.writeObject(v);
        }
    }

    @SuppressWarnings("unchecked")
    private void readObject(java.io.ObjectInputStream istream) throws java.io.IOException, ClassNotFoundException {
        istream.defaultReadObject();
        int size = istream.readInt();
        source = new ArrayList<>(size);
        for(int i=0; i<size; i++) {
            source.add((T)istream.readObject());
        }
        original = false;
    }

    private void detach() {
        if(original) {
            source = new ArrayList<>(source);
            original = false;
        }
    }

    private class CopyIterator implements ListIterator<T> {

        private ListIterator<T> itr;

        private int index = -1;

        public CopyIterator(ListIterator<T> itr) {
            this.itr = itr;
        }

        @Override
        public boolean hasNext() {
            return itr.hasNext();
        }

        @Override
        public T next() {
            index = itr.nextIndex();
            return itr.next();
        }

        @Override
        public boolean hasPrevious() {
            return itr.hasPrevious();
        }

        @Override
        public T previous() {
            index = itr.previousIndex();
            return itr.previous();
        }

        @Override
        public int nextIndex() {
            return itr.nextIndex();
        }

        @Override
        public int previousIndex() {
            return itr.previousIndex();
        }

        @Override
        public void set(T t) {
            if(!original) {
                itr.set(t);
                return;
            }
            if(index == -1) {
                throw new IllegalStateException();
            }
            CopyList.this.set(index, t);
            itr = source.listIterator(index+1);
        }

        @Override
        public void add(T t) {
            if(!original) {
                itr.add(t);
                return;
            }
            CopyList.this.add(itr.nextIndex(), t);
            itr = source.listIterator(itr.nextIndex()+1);
        }

        @Override
        public void remove() {
            if(!original) {
                itr.remove();
                return;
            }
            if(index == -1) {
                throw new IllegalStateException();
            }
            CopyList.this.remove(index);
            itr = source.listIterator(index);
        }
    }

    private class CopyFragment extends AbstractList<T> implements Serializable {

        private final CopyFragment parent;

        private final int offset;

        private int size;

        CopyFragment(CopyFragment parent, int fromIndex, int toIndex) {
            this.parent = parent;
            if (fromIndex < 0) {
                throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
            }
            if (toIndex > source.size()) {
                throw new IndexOutOfBoundsException("toIndex = " + toIndex);
            }
            if (fromIndex > toIndex) {
                throw new IllegalArgumentException("fromIndex(" + fromIndex +") > toIndex(" + toIndex + ")");
            }
            offset = fromIndex;
            size = toIndex - fromIndex;
        }

        public T set(int index, T element) {
            rangeCheck(index);
            return CopyList.this.set(index+offset, element);
        }

        public T get(int index) {
            rangeCheck(index);
            return source.get(index+offset);
        }

        public int size() {
            return size;
        }

        public void add(int index, T element) {
            rangeCheckForAdd(index);
            CopyList.this.add(index+offset, element);
            size++;
            if(parent != null) {
                parent.size++;
            }
        }

        public T remove(int index) {
            rangeCheck(index);
            T result = CopyList.this.remove(index+offset);
            size--;
            if(parent != null) {
                parent.size--;
            }
            return result;
        }

        protected void removeRange(int fromIndex, int toIndex) {
            detach();
            source.subList(offset+fromIndex, offset+toIndex).clear();
            size -= (toIndex-fromIndex);
            if(parent != null) {
                parent.size -= (toIndex-fromIndex);
            }
        }

        public boolean addAll(Collection<? extends T> c) {
            return addAll(size, c);
        }

        public boolean addAll(int index, Collection<? extends T> c) {
            rangeCheckForAdd(index);
            int n = c.size();
            if (n==0) {
                return false;
            }

            CopyList.this.addAll(offset+index, c);
            size += n;
            if(parent != null) {
                parent.size += n;
            }
            return true;
        }

        public Iterator<T> iterator() {
            return listIterator();
        }

        public ListIterator<T> listIterator(final int index) {
            rangeCheckForAdd(index);
            return new FragmentIterator(index);
        }

        public List<T> subList(int fromIndex, int toIndex) {
            rangeCheck(fromIndex);
            rangeCheckForAdd(toIndex);
            return new CopyFragment(this, offset+fromIndex, offset+toIndex);
        }

        protected Object writeReplace() {
            return new CopyList<>(this);
        }

        private void rangeCheck(int index) {
            if (index < 0 || index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }

        private void rangeCheckForAdd(int index) {
            if (index < 0 || index > size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }

        private String outOfBoundsMsg(int index) {
            return "Index: "+index+", Size: "+size;
        }

        private class FragmentIterator implements ListIterator<T> {
            private final ListIterator<T> i;

            public FragmentIterator(int index) {
                i = CopyList.this.listIterator(index + offset);
            }

            public boolean hasNext() {
                return nextIndex() < size;
            }

            public T next() {
                if (hasNext()) {
                    return i.next();
                } else {
                    throw new NoSuchElementException();
                }
            }

            public boolean hasPrevious() {
                return previousIndex() >= 0;
            }

            public T previous() {
                if (hasPrevious()) {
                    return i.previous();
                } else {
                    throw new NoSuchElementException();
                }
            }

            public int nextIndex() {
                return i.nextIndex() - offset;
            }

            public int previousIndex() {
                return i.previousIndex() - offset;
            }

            public void remove() {
                i.remove();
                size--;
                if(parent != null) {
                    parent.size--;
                }
            }

            public void set(T e) {
                i.set(e);
            }

            public void add(T e) {
                i.add(e);
                size++;
                if(parent != null) {
                    parent.size++;
                }
            }
        }
    }

}
