/*  __    __  __  __    __  ___
 * \  \  /  /    \  \  /  /  __/
 *  \  \/  /  /\  \  \/  /  /
 *   \____/__/  \__\____/__/
 *
 * Copyright 2014-2020 Vavr, http://vavr.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.vavr.collection;

import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * THIS CLASS IS INTENDED TO BE USED INTERNALLY ONLY!
 * <p>
 * This helper class provides methods that return views on Java collections.
 * The view creation and back conversion take O(1).
 *
 * @author Daniel Dietrich
 */
class JavaConverters {

    private JavaConverters() {
    }

    @GwtIncompatible
    static <T, C extends Seq<T>> ListView<T, C> asJava(C seq, ChangePolicy changePolicy) {
        return new ListView<>(seq, changePolicy.isMutable());
    }

    enum ChangePolicy {

        IMMUTABLE, MUTABLE;

        boolean isMutable() {
            return this == MUTABLE;
        }
    }

    // -- private view implementations

    /**
     * Encapsulates the access to delegate and performs mutability checks.
     *
     * @param <C> The Vavr collection type
     */
    private static abstract class HasDelegate<C extends Traversable<?>> implements Serializable {

        private static final long serialVersionUID = 1L;

        private C delegate;
        private final boolean mutable;

        HasDelegate(C delegate, boolean mutable) {
            this.delegate = delegate;
            this.mutable = mutable;
        }

        protected boolean isMutable() {
            return mutable;
        }

        C getDelegate() {
            return delegate;
        }

        protected boolean setDelegateAndCheckChanged(Supplier<C> delegate) {
            ensureMutable();
            final C previousDelegate = this.delegate;
            final C newDelegate = delegate.get();
            final boolean changed = newDelegate.size() != previousDelegate.size();
            if (changed) {
                this.delegate = newDelegate;
            }
            return changed;
        }

        protected void setDelegate(Supplier<C> newDelegate) {
            ensureMutable();
            this.delegate = newDelegate.get();
        }

        protected void ensureMutable() {
            if (!mutable) {
                throw new UnsupportedOperationException();
            }
        }
    }

    @GwtIncompatible("reflection is not supported")
    static class ListView<T, C extends Seq<T>> extends HasDelegate<C> implements java.util.List<T> {

        private static final long serialVersionUID = 1L;

        ListView(C delegate, boolean mutable) {
            super(delegate, mutable);
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean add(T element) {
            setDelegate(() -> (C) getDelegate().append(element));
            return true;
        }

        @SuppressWarnings("unchecked")
        @Override
        public void add(int index, T element) {
            setDelegate(() -> (C) getDelegate().insert(index, element));
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean addAll(Collection<? extends T> collection) {
            Objects.requireNonNull(collection, "collection is null");
            return setDelegateAndCheckChanged(() -> (C) getDelegate().appendAll(collection));
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean addAll(int index, Collection<? extends T> collection) {
            Objects.requireNonNull(collection, "collection is null");
            return setDelegateAndCheckChanged(() -> (C) getDelegate().insertAll(index, collection));
        }

        @SuppressWarnings("unchecked")
        @Override
        public void clear() {
            // DEV-NOTE: acts like Java: works for empty immutable collections
            if (isEmpty()) {
                return;
            }
            setDelegate(() -> (C) getDelegate().take(0));
        }

        @Override
        public boolean contains(Object obj) {
            @SuppressWarnings("unchecked") final T that = (T) obj;
            return getDelegate().contains(that);
        }

        @Override
        public boolean containsAll(Collection<?> collection) {
            Objects.requireNonNull(collection, "collection is null");
            @SuppressWarnings("unchecked") final Collection<T> that = (Collection<T>) collection;
            return getDelegate().containsAll(that);
        }

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

        @Override
        public int indexOf(Object obj) {
            @SuppressWarnings("unchecked") final T that = (T) obj;
            return getDelegate().indexOf(that);
        }

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

        @Override
        public java.util.Iterator<T> iterator() {
            return new Iterator<>(this);
        }

        @Override
        public int lastIndexOf(Object obj) {
            @SuppressWarnings("unchecked") final T that = (T) obj;
            return getDelegate().lastIndexOf(that);
        }

        @Override
        public java.util.ListIterator<T> listIterator() {
            return new ListIterator<>(this, 0);
        }

        @Override
        public java.util.ListIterator<T> listIterator(int index) {
            return new ListIterator<>(this, index);
        }

        @SuppressWarnings("unchecked")
        @Override
        public T remove(int index) {
            return setDelegateAndGetPreviousElement(index, () -> (C) getDelegate().removeAt(index));
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean remove(Object obj) {
            final T that = (T) obj;
            return setDelegateAndCheckChanged(() -> (C) getDelegate().remove(that));
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean removeAll(Collection<?> collection) {
            Objects.requireNonNull(collection, "collection is null");
            @SuppressWarnings("unchecked") final Collection<T> that = (Collection<T>) collection;
            return setDelegateAndCheckChanged(() -> (C) getDelegate().removeAll(that));
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean retainAll(Collection<?> collection) {
            Objects.requireNonNull(collection, "collection is null");
            @SuppressWarnings("unchecked") final Collection<T> that = (Collection<T>) collection;
            return setDelegateAndCheckChanged(() -> (C) getDelegate().retainAll(that));
        }

        @SuppressWarnings("unchecked")
        @Override
        public T set(int index, T element) {
            return setDelegateAndGetPreviousElement(index, () -> (C) getDelegate().update(index, element));
        }

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

        @SuppressWarnings("unchecked")
        @Override
        public void sort(Comparator<? super T> comparator) {
            Objects.requireNonNull(comparator, "comparator is null");
            if (isEmpty()) {
                return;
            }
            setDelegate(() -> (C) getDelegate().sorted(comparator));
        }

        @Override
        public java.util.List<T> subList(int fromIndex, int toIndex) {
            return new ListView<>(getDelegate().subSequence(fromIndex, toIndex), isMutable());
        }

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

        @SuppressWarnings("unchecked")
        @Override
        public <U> U[] toArray(U[] array) {
            Objects.requireNonNull(array, "array is null");
            final U[] target;
            final C delegate = getDelegate();
            final int length = delegate.length();
            if (array.length < length) {
                final Class<? extends Object[]> newType = array.getClass();
                target = (newType == Object[].class)
                         ? (U[]) new Object[length]
                         : (U[]) java.lang.reflect.Array.newInstance(newType.getComponentType(), length);
            } else {
                if (array.length > length) {
                    array[length] = null;
                }
                target = array;
            }
            final java.util.Iterator<T> iter = delegate.iterator();
            for (int i = 0; i < length; i++) {
                target[i] = (U) iter.next();
            }
            return target;
        }

        // -- Object.*

        @Override
        public boolean equals(Object o) {
            return o == this || o instanceof java.util.List && Collections.areEqual(getDelegate(), (java.util.List<?>) o);
        }

        @Override
        public int hashCode() {
            // DEV-NOTE: Ensures that hashCode calculation is stable, regardless of delegate.hashCode()
            return Collections.hashOrdered(getDelegate());
        }

        @Override
        public String toString() {
            return getDelegate().mkString("[", ", ", "]");
        }

        // -- private helpers

        private T setDelegateAndGetPreviousElement(int index, Supplier<C> delegate) {
            ensureMutable();
            final T previousElement = getDelegate().get(index);
            setDelegate(delegate);
            return previousElement;
        }

        // DEV-NOTE: Iterator is intentionally not Serializable
        private static class Iterator<T, C extends Seq<T>> implements java.util.Iterator<T> {

            ListView<T, C> list;
            int expectedSize;
            int nextIndex = 0;
            int lastIndex = -1;

            Iterator(ListView<T, C> list) {
                this.list = list;
                expectedSize = list.size();
            }

            @Override
            public boolean hasNext() {
                return nextIndex != list.size();
            }

            @Override
            public T next() {
                checkForComodification();
                if (nextIndex >= list.size()) {
                    throw new NoSuchElementException();
                }
                try {
                    return list.get(lastIndex = nextIndex++);
                } catch (IndexOutOfBoundsException x) {
                    throw new ConcurrentModificationException();
                }
            }

            @Override
            public void remove() {
                list.ensureMutable();
                if (lastIndex < 0) {
                    throw new IllegalStateException();
                }
                checkForComodification();
                try {
                    list.remove(nextIndex = lastIndex);
                    lastIndex = -1;
                    expectedSize = list.size();
                } catch (IndexOutOfBoundsException x) {
                    throw new ConcurrentModificationException();
                }
            }

            @Override
            public void forEachRemaining(Consumer<? super T> consumer) {
                Objects.requireNonNull(consumer, "consumer is  null");
                checkForComodification();
                if (nextIndex >= list.size()) {
                    return;
                }
                int index = nextIndex;
                // DEV-NOTE: intentionally not using hasNext() and next() in order not to modify internal state
                while (expectedSize == list.size() && index < expectedSize) {
                    consumer.accept(list.get(index++));
                }
                nextIndex = index;
                lastIndex = index - 1;
                checkForComodification();
            }

            final void checkForComodification() {
                if (expectedSize != list.size()) {
                    throw new ConcurrentModificationException();
                }
            }
        }

        // DEV-NOTE: ListIterator is intentionally not Serializable
        private static class ListIterator<T, C extends Seq<T>> extends ListView.Iterator<T, C> implements java.util.ListIterator<T> {

            ListIterator(ListView<T, C> list, int index) {
                super(list);
                if (index < 0 || index > list.size()) {
                    throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + list.size());
                }
                this.nextIndex = index;
            }

            @Override
            public boolean hasPrevious() {
                return nextIndex != 0;
            }

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

            @Override
            public int previousIndex() {
                return nextIndex - 1;
            }

            @Override
            public T previous() {
                checkForComodification();
                final int index = nextIndex - 1;
                if (index < 0) {
                    throw new NoSuchElementException();
                }
                if (index >= list.size()) {
                    throw new ConcurrentModificationException();
                }
                try {
                    final T element = list.get(index);
                    // DEV-NOTE: intentionally updating indices _after_ reading the element. This makes a difference in case of a concurrent modification.
                    lastIndex = nextIndex = index;
                    return element;
                } catch (IndexOutOfBoundsException x) {
                    throw new ConcurrentModificationException();
                }
            }

            @Override
            public void set(T element) {
                list.ensureMutable();
                if (lastIndex < 0) {
                    throw new IllegalStateException();
                }
                checkForComodification();
                try {
                    list.set(lastIndex, element);
                } catch (IndexOutOfBoundsException x) {
                    throw new ConcurrentModificationException();
                }
            }

            @Override
            public void add(T element) {
                list.ensureMutable();
                checkForComodification();
                try {
                    final int index = nextIndex;
                    list.add(index, element);
                    // DEV-NOTE: intentionally increasing nextIndex _after_ adding the element. This makes a difference in case of a concurrent modification.
                    nextIndex = index + 1;
                    lastIndex = -1;
                    expectedSize = list.size();
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }
        }
    }
}
