/*
 * 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.sis.math;

import org.apache.sis.util.Numbers;
import org.apache.sis.util.collection.IntegerList;
import org.apache.sis.util.resources.Errors;


/**
 * A vector of integer values backed by an {@link IntegerList}.
 * This offers a compressed storage using only the minimal amount of bits per value.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.8
 * @since   0.8
 * @module
 */
final class PackedVector extends ArrayVector<Long> {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = 5097586732924434042L;

    /**
     * Minimal length for creating a packed vector.
     * This is an arbitrary value that may change in any future version.
     */
    static final int MINIMAL_SIZE = 8;

    /**
     * The compressed list of integer values. This list can store values from 0 to {@code delta} inclusive.
     */
    private final IntegerList data;

    /**
     * The value by which to multiply the {@linkplain #data} before to add the {@linkplain #offset}.
     */
    private final long increment;

    /**
     * The offset to add to the {@link #data} in order to get the values to return.
     */
    private final long offset;

    /**
     * Creates a new compressed vector initialized to a copy of the data provided by the given vector.
     *
     * @param  source     the vector to copy.
     * @param  increment  the common divisor of all (sample minus offset) values.
     * @param  offset     the minimal value in the source vector.
     * @param  delta      the maximal value in the source vector minus {@code offset} divided by {@code increment}.
     */
    private PackedVector(final Vector source, final long increment, final long offset, final int delta) {
        this.increment = increment;
        this.offset    = offset;
        final int length = source.size();
        data = new IntegerList(length, delta, true);
        for (int i=0; i<length; i++) {
            data.setInt(i, Math.toIntExact((source.longValue(i) - offset) / increment));
        }
    }

    /**
     * Creates a new compressed vector initialized to a copy of the data provided by the given vector.
     * All values in the given vector shall be assignable to the {@code long} type (this is not verified).
     *
     * @param  source  the vector to copy.
     * @param  min     the minimal value in the given vector, inclusive.
     * @param  max     the maximal value in the given vector, inclusive.
     * @return the compressed vector, or {@code null} if the vector can not or should not be compressed.
     */
    static PackedVector compress(final Vector source, final long min, final long max) {
        long delta = max - min;
        if (delta > 0) {                                    // Negative if (max - min) overflow.
            long inc = delta;
            final int length = source.size();
            if (length >= MINIMAL_SIZE) {
                for (int i=0; i<length; i++) {
                    long t = source.longValue(i) - min;
                    if (t < 0) return null;                 // May happen if the given 'min' value is wrong.
                    if ((t % inc) != 0) {
                        do {
                            final long r = (inc % t);       // Search for greatest common divisor with Euclid's algorithm.
                            inc = t;
                            t = r;
                        } while (t != 0);
                        if (inc == 1) {
                            /*
                             * If we reach this point, the increment is of no use for compressing data.
                             * If in addition the minimal number of bits required for storing all values:
                             *
                             *     numBits = Long.SIZE - Long.numberOfLeadingZeros(delta);
                             *
                             * is equal to the number of bits of a primitive type (byte, short, int or long)
                             * and all values are in the range of that primitive type, then there is nothing
                             * to win compared to an array of that primitive type.
                             */
                            final long high = Long.highestOneBit(delta);
                            if ((high & ((1L << (Byte   .SIZE - 1)) |
                                         (1L << (Short  .SIZE - 1)) |
                                         (1L << (Integer.SIZE - 1)) |
                                         (1L << (Long   .SIZE - 1)))) != 0)
                            {
                                long limit = high - 1;                  // Maximal value of signed integers.
                                if (min >= 0) {
                                    limit |= high;                      // Maximal value of unsigned integers.
                                    if (limit < 0) return null;         // Overflow the 'long' primitve type.
                                }
                                if (max <= limit && min >= ~limit) {    // Really tild (~), not minus (-).
                                    return null;                        // All values in range of primitive type.
                                }
                            }
                            break;                                      // No need to check other values.
                        }
                    }
                }
                delta /= inc;
                if (delta > Integer.MAX_VALUE) return null;
                return new PackedVector(source, inc, min, (int) delta);
            }
        }
        return null;
    }

    /**
     * Type of elements fixed to {@code Long} even if the actual storage used by this class is more compact.
     * The reason for the {@code Long} type is that this class can return any value in the {@code Long} range,
     * because of the {@link #offset}.
     */
    @Override
    public Class<Long> getElementType() {
        return Long.class;
    }

    /**
     * Returns the number of elements in this vector.
     */
    @Override
    public int size() {
        return data.size();
    }

    /**
     * Returns the value at the given index as a {@code double} primitive type.
     */
    @Override
    public double doubleValue(final int index) {
        return longValue(index);
    }

    /**
     * Returns the value at the given index as a {@code float} primitive type.
     */
    @Override
    public float floatValue(final int index) {
        return longValue(index);
    }

    /**
     * Returns the value at the given index as a {@code long} primitive type.
     */
    @Override
    public long longValue(final int index) {
        return data.getInt(index) * increment + offset;
    }

    /**
     * Returns the string representation of the value at the given index.
     */
    @Override
    public String stringValue(final int index) {
        return Long.toString(longValue(index));
    }

    /**
     * Returns the value at the given index wrapped in a {@link Long} instance.
     */
    @Override
    public Number get(final int index) {
        return longValue(index);
    }

    /**
     * Sets the value at the given index at returns the previous value.
     */
    @Override
    public Number set(final int index, final Number value) {
        verifyType(value, Numbers.LONG);
        long v = value.longValue();
        if (v >= offset) {
            v -= offset;
            if ((v % increment) == 0) {
                v /= increment;
                if (v <= data.maximalValue()) {
                    final Number old = get(index);
                    data.setInt(index, (int) v);
                    modCount++;
                    return old;
                }
            }
        }
        throw new IllegalArgumentException(Errors.format(Errors.Keys.CanNotStoreInVector_1, value));
    }
}
