/*
 * 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 org.apache.flink.runtime.state;

import org.apache.flink.util.Preconditions;

import java.io.Serializable;
import java.util.Iterator;

/**
 * This class defines a range of key-group indexes. Key-groups are the granularity into which the
 * keyspace of a job is partitioned for keyed state-handling in state backends. The boundaries of
 * the range are inclusive.
 */
public class KeyGroupRange implements KeyGroupsList, Serializable {

    private static final long serialVersionUID = 4869121477592070607L;

    /** The empty key-group */
    public static final KeyGroupRange EMPTY_KEY_GROUP_RANGE = new KeyGroupRange();

    private final int startKeyGroup;
    private final int endKeyGroup;

    /** Empty KeyGroup Constructor */
    private KeyGroupRange() {
        this.startKeyGroup = 0;
        this.endKeyGroup = -1;
    }

    /**
     * Defines the range [startKeyGroup, endKeyGroup]
     *
     * @param startKeyGroup start of the range (inclusive)
     * @param endKeyGroup end of the range (inclusive)
     */
    public KeyGroupRange(int startKeyGroup, int endKeyGroup) {
        Preconditions.checkArgument(startKeyGroup >= 0);
        Preconditions.checkArgument(startKeyGroup <= endKeyGroup);
        this.startKeyGroup = startKeyGroup;
        this.endKeyGroup = endKeyGroup;
        Preconditions.checkArgument(getNumberOfKeyGroups() >= 0, "Potential overflow detected.");
    }

    /**
     * Checks whether or not a single key-group is contained in the range.
     *
     * @param keyGroup Key-group to check for inclusion.
     * @return True, only if the key-group is in the range.
     */
    @Override
    public boolean contains(int keyGroup) {
        return keyGroup >= startKeyGroup && keyGroup <= endKeyGroup;
    }

    /**
     * Create a range that represent the intersection between this range and the given range.
     *
     * @param other A KeyGroupRange to intersect.
     * @return Key-group range that is the intersection between this and the given key-group range.
     */
    public KeyGroupRange getIntersection(KeyGroupRange other) {
        int start = Math.max(startKeyGroup, other.startKeyGroup);
        int end = Math.min(endKeyGroup, other.endKeyGroup);
        return start <= end ? new KeyGroupRange(start, end) : EMPTY_KEY_GROUP_RANGE;
    }

    /**
     * @return The number of key-groups in the range
     */
    @Override
    public int getNumberOfKeyGroups() {
        return 1 + endKeyGroup - startKeyGroup;
    }

    /**
     * @return The first key-group in the range.
     */
    public int getStartKeyGroup() {
        return startKeyGroup;
    }

    /**
     * @return The last key-group in the range.
     */
    public int getEndKeyGroup() {
        return endKeyGroup;
    }

    @Override
    public int getKeyGroupId(int idx) {
        if (idx < 0 || idx > getNumberOfKeyGroups()) {
            throw new IndexOutOfBoundsException("Key group index out of bounds: " + idx);
        }
        return startKeyGroup + idx;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof KeyGroupRange)) {
            return false;
        }

        KeyGroupRange that = (KeyGroupRange) o;
        return startKeyGroup == that.startKeyGroup && endKeyGroup == that.endKeyGroup;
    }

    @Override
    public int hashCode() {
        int result = startKeyGroup;
        result = 31 * result + endKeyGroup;
        return result;
    }

    @Override
    public String toString() {
        return "KeyGroupRange{"
                + "startKeyGroup="
                + startKeyGroup
                + ", endKeyGroup="
                + endKeyGroup
                + '}';
    }

    public String prettyPrintInterval() {
        return "[" + startKeyGroup + ", " + endKeyGroup + "]";
    }

    @Override
    public Iterator<Integer> iterator() {
        return new KeyGroupIterator();
    }

    private final class KeyGroupIterator implements Iterator<Integer> {

        public KeyGroupIterator() {
            this.iteratorPos = 0;
        }

        private int iteratorPos;

        @Override
        public boolean hasNext() {
            return iteratorPos < getNumberOfKeyGroups();
        }

        @Override
        public Integer next() {
            int rv = startKeyGroup + iteratorPos;
            ++iteratorPos;
            return rv;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Unsupported by this iterator!");
        }
    }

    /**
     * Factory method that also handles creation of empty key-groups.
     *
     * @param startKeyGroup start of the range (inclusive)
     * @param endKeyGroup end of the range (inclusive)
     * @return the key-group from start to end or an empty key-group range.
     */
    public static KeyGroupRange of(int startKeyGroup, int endKeyGroup) {
        return startKeyGroup <= endKeyGroup
                ? new KeyGroupRange(startKeyGroup, endKeyGroup)
                : EMPTY_KEY_GROUP_RANGE;
    }
}
