/*
 * Copyright 2015, 2019 StreamEx contributors
 *
 * 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 one.util.streamex;

import java.util.List;
import java.util.Map.Entry;
import java.util.Spliterator;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleConsumer;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.LongBinaryOperator;
import java.util.function.LongConsumer;

import static one.util.streamex.Internals.CloneableSpliterator;
import static one.util.streamex.Internals.ObjIntBox;

/**
 * @author Tagir Valeev
 */
/* package */abstract class RangeBasedSpliterator<T, S extends RangeBasedSpliterator<T, ?>> extends CloneableSpliterator<T, S> {
    int cur;
    int limit;

    public RangeBasedSpliterator(int fromInclusive, int toExclusive) {
        this.cur = fromInclusive;
        this.limit = toExclusive;
    }

    @Override
    public long estimateSize() {
        return limit - cur;
    }

    @Override
    public int characteristics() {
        return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
    }

    @Override
    public S trySplit() {
        int size = limit - cur;
        if (size >= 2) {
            S clone = doClone();
            clone.limit = this.cur = this.cur + size / 2;
            return clone;
        }
        return null;
    }

    static final class AsEntry<T> extends RangeBasedSpliterator<Entry<Integer, T>, AsEntry<T>> {
        private final List<T> list;

        public AsEntry(List<T> list) {
            super(0, list.size());
            this.list = list;
        }

        @Override
        public boolean tryAdvance(Consumer<? super Entry<Integer, T>> action) {
            if (cur < limit) {
                action.accept(new ObjIntBox<>(list.get(cur), cur));
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(Consumer<? super Entry<Integer, T>> action) {
            int l = limit, c = cur;
            List<T> list = this.list;
            while (c < l) {
                action.accept(new ObjIntBox<>(list.get(c), c));
                c++;
            }
            cur = limit;
        }
    }

    static final class OfSubLists<T> extends RangeBasedSpliterator<List<T>, OfSubLists<T>> {
        private final List<T> source;
        private final int length;
        private final int shift;
        private final int listSize;

        public OfSubLists(List<T> source, int length, int shift) {
            super(0, Math.max(0, source.size() - Math.max(length - shift, 0) - 1) / shift + 1);
            this.source = source;
            this.listSize = source.size();
            this.shift = shift;
            this.length = length;
        }

        @Override
        public boolean tryAdvance(Consumer<? super List<T>> action) {
            if (cur < limit) {
                int start = cur * shift;
                int stop = listSize - length > start ? start + length : listSize;
                action.accept(source.subList(start, stop));
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(Consumer<? super List<T>> action) {
            int l = limit, c = cur, ll = length, sf = shift, ls = listSize;
            int start = cur * sf;
            while (c < l) {
                int stop = ls - ll > start ? start + ll : ls;
                action.accept(source.subList(start, stop));
                start += sf;
                c++;
            }
            cur = limit;
        }
    }

    static final class ZipRef<U, V, T> extends RangeBasedSpliterator<T, ZipRef<U, V, T>> {
        private final List<U> l1;
        private final List<V> l2;
        private final BiFunction<? super U, ? super V, ? extends T> mapper;

        public ZipRef(int fromInclusive, int toExclusive, BiFunction<? super U, ? super V, ? extends T> mapper,
                List<U> l1, List<V> l2) {
            super(fromInclusive, toExclusive);
            this.l1 = l1;
            this.l2 = l2;
            this.mapper = mapper;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            if (cur < limit) {
                action.accept(mapper.apply(l1.get(cur), l2.get(cur)));
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(Consumer<? super T> action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(mapper.apply(l1.get(c), l2.get(c)));
                c++;
            }
            cur = limit;
        }
    }

    static final class ZipInt extends RangeBasedSpliterator<Integer, ZipInt> implements Spliterator.OfInt {
        private final IntBinaryOperator mapper;
        private final int[] arr1, arr2;

        public ZipInt(int fromInclusive, int toExclusive, IntBinaryOperator mapper, int[] arr1, int[] arr2) {
            super(fromInclusive, toExclusive);
            this.mapper = mapper;
            this.arr1 = arr1;
            this.arr2 = arr2;
        }

        @Override
        public boolean tryAdvance(IntConsumer action) {
            if (cur < limit) {
                action.accept(mapper.applyAsInt(arr1[cur], arr2[cur]));
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(IntConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(mapper.applyAsInt(arr1[c], arr2[c]));
                c++;
            }
            cur = limit;
        }
    }

    static final class OfByte extends RangeBasedSpliterator<Integer, OfByte> implements Spliterator.OfInt {
        private final byte[] array;

        public OfByte(int fromInclusive, int toExclusive, byte[] array) {
            super(fromInclusive, toExclusive);
            this.array = array;
        }

        @Override
        public boolean tryAdvance(IntConsumer action) {
            if (cur < limit) {
                action.accept(array[cur]);
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(IntConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(array[c++]);
            }
            cur = limit;
        }
    }

    static final class OfChar extends RangeBasedSpliterator<Integer, OfChar> implements Spliterator.OfInt {
        private final char[] array;

        public OfChar(int fromInclusive, int toExclusive, char[] array) {
            super(fromInclusive, toExclusive);
            this.array = array;
        }

        @Override
        public boolean tryAdvance(IntConsumer action) {
            if (cur < limit) {
                action.accept(array[cur]);
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(IntConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(array[c++]);
            }
            cur = limit;
        }
    }

    static final class OfShort extends RangeBasedSpliterator<Integer, OfShort> implements Spliterator.OfInt {
        private final short[] array;

        public OfShort(int fromInclusive, int toExclusive, short[] array) {
            super(fromInclusive, toExclusive);
            this.array = array;
        }

        @Override
        public boolean tryAdvance(IntConsumer action) {
            if (cur < limit) {
                action.accept(array[cur]);
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(IntConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(array[c++]);
            }
            cur = limit;
        }
    }

    static final class ZipLong extends RangeBasedSpliterator<Long, ZipLong> implements Spliterator.OfLong {
        private final LongBinaryOperator mapper;
        private final long[] arr1, arr2;

        public ZipLong(int fromInclusive, int toExclusive, LongBinaryOperator mapper, long[] arr1, long[] arr2) {
            super(fromInclusive, toExclusive);
            this.mapper = mapper;
            this.arr1 = arr1;
            this.arr2 = arr2;
        }

        @Override
        public boolean tryAdvance(LongConsumer action) {
            if (cur < limit) {
                action.accept(mapper.applyAsLong(arr1[cur], arr2[cur]));
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(LongConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(mapper.applyAsLong(arr1[c], arr2[c]));
                c++;
            }
            cur = limit;
        }
    }

    static final class OfFloat extends RangeBasedSpliterator<Double, OfFloat> implements Spliterator.OfDouble {
        private final float[] array;

        public OfFloat(int fromInclusive, int toExclusive, float[] array) {
            super(fromInclusive, toExclusive);
            this.array = array;
        }

        @Override
        public boolean tryAdvance(DoubleConsumer action) {
            if (cur < limit) {
                action.accept(array[cur]);
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(DoubleConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(array[c++]);
            }
            cur = limit;
        }
    }

    static final class ZipDouble extends RangeBasedSpliterator<Double, ZipDouble> implements Spliterator.OfDouble {
        private final DoubleBinaryOperator mapper;
        private final double[] arr1, arr2;

        public ZipDouble(int fromInclusive, int toExclusive, DoubleBinaryOperator mapper, double[] arr1, double[] arr2) {
            super(fromInclusive, toExclusive);
            this.mapper = mapper;
            this.arr1 = arr1;
            this.arr2 = arr2;
        }

        @Override
        public boolean tryAdvance(DoubleConsumer action) {
            if (cur < limit) {
                action.accept(mapper.applyAsDouble(arr1[cur], arr2[cur]));
                cur++;
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(DoubleConsumer action) {
            int l = limit, c = cur;
            while (c < l) {
                action.accept(mapper.applyAsDouble(arr1[c], arr2[c]));
                c++;
            }
            cur = limit;
        }
    }
}
