/**
 * Copyright (C) 2014-2017 Xavier Witdouck
 *
 * 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 com.zavtech.morpheus.array.dense;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.function.Predicate;

import gnu.trove.set.TIntSet;
import gnu.trove.set.hash.TIntHashSet;

import com.zavtech.morpheus.array.Array;
import com.zavtech.morpheus.array.ArrayBase;
import com.zavtech.morpheus.array.ArrayBuilder;
import com.zavtech.morpheus.array.ArrayCursor;
import com.zavtech.morpheus.array.ArrayException;
import com.zavtech.morpheus.array.ArrayStyle;
import com.zavtech.morpheus.array.ArrayValue;
import com.zavtech.morpheus.array.coding.IntCoding;
import com.zavtech.morpheus.array.coding.WithIntCoding;

/**
 * A dense array implementation that maintains a primitive int array of codes that apply to Object values exposed through the IntCoding interface.
 *
 * @author Xavier Witdouck
 *
 * <p><strong>This is open source software released under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 License</a></strong></p>
 */
class DenseArrayWithIntCoding<T> extends ArrayBase<T> implements WithIntCoding<T> {

    private static final long serialVersionUID = 1L;

    private int[] codes;
    private T defaultValue;
    private int defaultCode;
    private IntCoding<T> coding;

    /**
     * Constructor
     * @param length        the length for this array
     * @param defaultValue  the default value for array
     * @param coding        the coding for this array
     */
    DenseArrayWithIntCoding(int length, T defaultValue, IntCoding<T> coding) {
        super(coding.getType(), ArrayStyle.DENSE, false);
        this.coding = coding;
        this.codes = new int[length];
        this.defaultValue = defaultValue;
        this.defaultCode = coding.getCode(defaultValue);
        Arrays.fill(codes, defaultCode);
    }


    /**
     * Constructor
     * @param source    the source array to copy
     * @param parallel  true for the parallel version
     */
    private DenseArrayWithIntCoding(DenseArrayWithIntCoding<T> source, boolean parallel) {
        super(source.type(), ArrayStyle.DENSE, parallel);
        this.coding = source.coding;
        this.codes = source.codes;
        this.defaultValue = source.defaultValue;
        this.defaultCode = source.defaultCode;
    }


    @Override
    public final IntCoding<T> getCoding() {
        return coding;
    }


    @Override
    public final int length() {
        return codes.length;
    }


    @Override
    public float loadFactor() {
        return 1F;
    }


    @Override
    public final T defaultValue() {
        return defaultValue;
    }


    @Override
    public final Array<T> parallel() {
        return isParallel() ? this : new DenseArrayWithIntCoding<>(this, true);
    }


    @Override
    public final Array<T> sequential() {
        return isParallel() ? new DenseArrayWithIntCoding<>(this, false) : this;
    }


    @Override()
    @SuppressWarnings("unchecked")
    public final Array<T> copy() {
        try {
            final DenseArrayWithIntCoding<T> copy = (DenseArrayWithIntCoding<T>)super.clone();
            copy.defaultValue = this.defaultValue;
            copy.defaultCode = this.defaultCode;
            copy.coding = this.coding;
            copy.codes = this.codes.clone();
            return copy;
        } catch (Exception ex) {
            throw new ArrayException("Failed to copy Array: " + this, ex);
        }
    }


    @Override()
    public final Array<T> copy(int[] indexes) {
        final DenseArrayWithIntCoding<T> clone = new DenseArrayWithIntCoding<>(indexes.length, defaultValue, coding);
        for (int i = 0; i < indexes.length; ++i) {
            clone.codes[i] = this.codes[indexes[i]];
        }
        return clone;
    }


    @Override()
    public final Array<T> copy(int start, int end) {
        final int length = end - start;
        final DenseArrayWithIntCoding<T> clone = new DenseArrayWithIntCoding<>(length, defaultValue, coding);
        System.arraycopy(codes, start, clone.codes, 0, length);
        return clone;
    }


    @Override
    protected final Array<T> sort(int start, int end, int multiplier) {
        return doSort(start, end, (i, j) -> multiplier * Integer.compare(codes[i], codes[j]));
    }


    @Override
    public final int compare(int i, int j) {
        return Integer.compare(codes[i], codes[j]);
    }


    @Override
    public final Array<T> swap(int i, int j) {
        final int v1 = codes[i];
        final int v2 = codes[j];
        this.codes[i] = v2;
        this.codes[j] = v1;
        return this;
    }


    @Override
    public final Array<T> filter(Predicate<ArrayValue<T>> predicate) {
        final ArrayCursor<T> cursor = cursor();
        final ArrayBuilder<T> builder = ArrayBuilder.of(length(), type());
        for (int i = 0; i< length(); ++i) {
            cursor.moveTo(i);
            final boolean match = predicate.test(cursor);
            if (match) {
                builder.add(cursor.getValue());
            }
        }
        return builder.toArray();
    }


    @Override
    public final Array<T> update(Array<T> from, int[] fromIndexes, int[] toIndexes) {
        if (fromIndexes.length != toIndexes.length) {
            throw new ArrayException("The from index array must have the same length as the to index array");
        } else {
            for (int i=0; i<fromIndexes.length; ++i) {
                final int toIndex = toIndexes[i];
                final int fromIndex = fromIndexes[i];
                final T update = from.getValue(fromIndex);
                this.setValue(toIndex, update);
            }
        }
        return this;
    }


    @Override
    public final Array<T> update(int toIndex, Array<T> from, int fromIndex, int length) {
        if (from instanceof DenseArrayWithIntCoding) {
            final DenseArrayWithIntCoding other = (DenseArrayWithIntCoding) from;
            for (int i = 0; i < length; ++i) {
                this.codes[toIndex + i] = other.codes[fromIndex + i];
            }
        } else if (from instanceof DenseArrayOfInts) {
            for (int i = 0; i < length; ++i) {
                this.codes[toIndex + i] = from.getInt(fromIndex + i);
            }
        } else {
            for (int i=0; i<length; ++i) {
                final T update = from.getValue(fromIndex + i);
                this.setValue(toIndex + i, update);
            }
        }
        return this;
    }


    @Override
    public final Array<T> expand(int newLength) {
        if (newLength > codes.length) {
            final int[] newCodes = new int[newLength];
            System.arraycopy(codes, 0, newCodes, 0, codes.length);
            Arrays.fill(newCodes, codes.length, newCodes.length, defaultCode);
            this.codes = newCodes;
        }
        return this;
    }


    @Override
    public Array<T> fill(T value, int start, int end) {
        final int code = coding.getCode(value);
        Arrays.fill(codes, start, end, code);
        return this;
    }


    @Override
    public final boolean isNull(int index) {
        return codes[index] == coding.getCode(null);
    }


    @Override
    public final boolean isEqualTo(int index, T value) {
        if (value == null) {
            return isNull(index);
        } else {
            final int code = coding.getCode(value);
            return code == codes[index];
        }
    }


    @Override
    public int getInt(int index) {
        return codes[index];
    }


    @Override
    public final T getValue(int index) {
        return coding.getValue(codes[index]);
    }


    @Override
    public final T setValue(int index, T value) {
        final T oldValue = getValue(index);
        this.codes[index] = coding.getCode(value);
        return oldValue;
    }


    @Override
    public Array<T> distinct(int limit) {
        final int capacity = limit < Integer.MAX_VALUE ? limit : 100;
        final TIntSet set = new TIntHashSet(capacity);
        final ArrayBuilder<T> builder = ArrayBuilder.of(capacity, type());
        for (int i=0; i<length(); ++i) {
            final int code = getInt(i);
            if (set.add(code)) {
                final T value = getValue(i);
                builder.add(value);
                if (set.size() >= limit) {
                    break;
                }
            }
        }
        return builder.toArray();
    }


    @Override
    public final void read(ObjectInputStream is, int count) throws IOException {
        for (int i=0; i<count; ++i) {
            this.codes[i] = is.readInt();
        }
    }


    @Override
    public final void write(ObjectOutputStream os, int[] indexes) throws IOException {
        for (int index : indexes) {
            os.writeInt(codes[index]);
        }
    }

    /** Custom serialization */
    private void writeObject(ObjectOutputStream os) throws IOException {
        os.writeObject(coding);
        os.writeInt(codes.length);
        for (int value : codes) {
            os.writeInt(value);
        }
    }


    @SuppressWarnings("unchecked")
    /** Custom serialization */
    private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
        this.coding = (IntCoding<T>)is.readObject();
        final int length = is.readInt();
        this.codes = new int[length];
        for (int i=0; i<length; ++i) {
            codes[i] = is.readInt();
        }
    }

}