/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 accord.primitives;

import accord.api.RoutingKey;
import accord.utils.Invariants;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;

import static accord.primitives.AbstractRanges.UnionMode.MERGE_OVERLAPPING;
import static accord.primitives.Routables.Slice.Overlapping;

public class Ranges extends AbstractRanges implements Iterable<Range>, Seekables<Range, Ranges>, Unseekables<Range>, Participants<Range>
{
    public static final Ranges EMPTY = new Ranges(new Range[0]);

    private Ranges(@Nonnull Range[] ranges)
    {
        super(ranges);
    }

    public static Ranges of(Range... ranges)
    {
        return AbstractRanges.of(Ranges::ofSortedAndDeoverlappedUnchecked, ranges);
    }

    public static Ranges ofSorted(Range... ranges)
    {
        return AbstractRanges.deoverlapSorted(Ranges::ofSortedAndDeoverlappedUnchecked, ranges, ranges.length, MERGE_OVERLAPPING);
    }

    public static Ranges ofSortedAndDeoverlapped(Range... ranges)
    {
        return AbstractRanges.ofSortedAndDeoverlapped(Ranges::ofSortedAndDeoverlappedUnchecked, ranges);
    }

    static Ranges ofSortedAndDeoverlappedUnchecked(Range... ranges)
    {
        if (ranges.length == 0)
            return EMPTY;

        return new Ranges(ranges);
    }

    public static Ranges single(Range range)
    {
        Invariants.require(range != null);
        return new Ranges(new Range[]{range});
    }

    public Ranges select(int[] indexes)
    {
        if (indexes.length == size())
        {
            if (Invariants.isParanoid())
            {
                for (int i = 0 ; i < indexes.length ; ++i)
                    Invariants.require(i == indexes[i]);
            }
            return this;
        }
        Range[] selection = new Range[indexes.length];
        for (int i=0; i<indexes.length; i++)
            selection[i] = ranges[indexes[i]];
        return ofSortedAndDeoverlapped(Ranges::ofSortedAndDeoverlappedUnchecked, selection);
    }

    @Override
    public Stream<Range> stream()
    {
        return Stream.of(ranges);
    }

    @Override
    public final Ranges overlapping(Ranges ranges)
    {
        return slice(ranges, Overlapping);
    }

    @Override
    public final Ranges slice(int from, int to)
    {
        Invariants.requireArgument(from <= to);
        Invariants.requireArgument(to <= ranges.length);
        if (from == 0 && to == size())
            return this;
        return Ranges.ofSortedAndDeoverlapped(Arrays.copyOfRange(ranges, from, to));
    }

    @Override
    public final Ranges slice(Ranges ranges, Slice slice)
    {
        return slice(ranges, slice, this, null, (i1, i2, rs) -> i1.ranges == rs ? i1 : Ranges.ofSortedAndDeoverlapped(rs));
    }

    private Ranges sliceOverlapping(AbstractRanges ranges, Slice slice)
    {
        return slice(ranges, slice, this, this, (i1, i2, rs) -> i2.ranges == rs ? i2 : Ranges.ofSortedAndDeoverlapped(rs));
    }

    @Override
    public Participants<Range> overlapping(Seekables<?, ?> intersecting)
    {
        return intersecting(intersecting, Overlapping);
    }

    @Override
    public Participants<Range> intersecting(Seekables<?, ?> intersecting, Slice slice)
    {
        switch (intersecting.domain())
        {
            default: throw new AssertionError("Unhandled domain: " + intersecting.domain());
            case Range: return sliceOverlapping((AbstractRanges) intersecting, slice);
            case Key: return intersecting((Keys) intersecting, this, null, (i1, i2, rs) -> i1.ranges == rs ? i1 : new Ranges(rs), slice);
        }
    }

    @Override
    public Ranges overlapping(Unseekables<?> intersecting)
    {
        return intersecting(intersecting, Overlapping);
    }

    @Override
    public final Ranges intersecting(Unseekables<?> intersecting, Slice slice)
    {
        switch (intersecting.domain())
        {
            default: throw new AssertionError("Unhandled domain: " + intersecting.domain());
            case Range: return sliceOverlapping((AbstractRanges) intersecting, slice);
            case Key: return intersecting((AbstractUnseekableKeys) intersecting, this, null, (i1, i2, rs) -> i1.ranges == rs ? i1 : new Ranges(rs), slice);
        }
    }

    @Override
    public Ranges with(Unseekables<Range> with)
    {
        return with((AbstractRanges) with);
    }

    @Override
    public Participants<Range> with(Participants<Range> with)
    {
        return with((AbstractRanges) with);
    }

    @Override
    public Ranges with(Ranges that)
    {
        return union(MERGE_OVERLAPPING, that);
    }

    public Ranges with(AbstractRanges that)
    {
        return union(MERGE_OVERLAPPING, that);
    }

    @Override
    public Unseekables<Range> with(RoutingKey withKey)
    {
        if (contains(withKey))
            return this;

        return with(Ranges.of(withKey.asRange()));
    }

    // TODO (expected): should return self if no changes
    public Ranges without(Unseekables<?> keysOrRanges)
    {
        if (keysOrRanges.domain() == Routable.Domain.Key)
            keysOrRanges = keysOrRanges.toRanges();
        return ofSortedAndDeoverlappedUnchecked(withoutInternal((AbstractRanges) keysOrRanges));
    }

    // TODO (expected): should return self if no changes
    /**
     * Subtracts the given set of ranges from this
     */
    public Ranges without(Ranges that)
    {
        return ofSortedAndDeoverlappedUnchecked(withoutInternal(that));
    }

    @Override
    public UnseekablesKind kind()
    {
        return UnseekablesKind.RoutingRanges;
    }

    @Override
    public Ranges toParticipants()
    {
        return this;
    }

    @Override
    public Ranges toRanges()
    {
        return this;
    }

    @Override
    public FullRangeRoute toRoute(RoutingKey homeKey)
    {
        if (!contains(homeKey))
            throw new IllegalArgumentException("Home key must be contained in the route: " + homeKey + " not in " + this);

        return new FullRangeRoute(homeKey, ranges);
    }

    public Ranges union(UnionMode mode, Ranges that)
    {
        return union(mode, this, that, this, that, (left, right, ranges) -> {
            if (ranges == left.ranges) return left;
            if (ranges == right.ranges) return right;
            return Ranges.ofSortedAndDeoverlapped(ranges);
        });
    }

    public Ranges union(UnionMode mode, AbstractRanges that)
    {
        return union(mode, this, that, this, that, (left, right, ranges) -> {
            if (ranges == left.ranges) return left;
            return Ranges.ofSortedAndDeoverlapped(ranges);
        });
    }

    public Ranges mergeTouching()
    {
        return mergeTouching(this, Ranges::ofSortedAndDeoverlapped);
    }

    public <T extends Enum<T>> Map<T, Ranges> partitioningBy(Function<? super Range, T> split, Class<T> clazz)
    {
        if (isEmpty())
            return Collections.emptyMap();
        Map<T, List<Range>> collector = new EnumMap<>(clazz);
        for (Range range : this)
            collector.computeIfAbsent(split.apply(range), ignore -> new ArrayList<>()).add(range);

        EnumMap<T, Ranges> result = new EnumMap<>(clazz);
        for (Map.Entry<T, List<Range>> e : collector.entrySet())
            result.put(e.getKey(), Ranges.ofSortedAndDeoverlapped(e.getValue().toArray(Range[]::new)));
        return result;
    }
}
