/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.sets;

import java.util.Set;
import java.util.function.ToIntFunction;
import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.sets.MaskSet.Empty;
import net.ranides.assira.collection.sets.MaskSet.Immutable;
import net.ranides.assira.collection.sets.MaskSet.Mutable;

/**
 * This class can be used as static mapper of int values into set of predefined values.
 *
 * At construction time, you specify predefined type and mapping functions which
 * associates int value with every predefined value. Later you can use this instance
 * as factory which creates MaskSet from integer or vice versa.
 *
 * Preferably, int values assigned to T constants should be stored in different bits,
 * if you want predictable mapping. Whats more, preferred type for MaskFactory is some enum type,
 * which offers closed list of possible values.
 *
 * Every set returned by factory stores its content inside just one int.
 * Please note, that because of that, maximal number of distinct values is 32.
 *
 * @param <T> T
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class MaskFactory<T> {

    static final String NO_ELEMENT = "Stored value can't be mapped to any constant: ";

    final T[] universe;
    final ToIntFunction<T> function;
    final int usedbits;
    final Class<T> type;
    final MaskSet<T> empty;

    /**
     * Creates new factory for specified enum type.
     * Mapping function should returns distinct value for every enum value, preferably distinct bit.
     *
     * @param universe list of enum values
     * @param function mapping
     */
    @SuppressWarnings({"PMD.ArrayIsStoredDirectly", "unchecked"})
    public MaskFactory(T[] universe, ToIntFunction<T> function) {
        this.universe = ArrayUtils.copy(universe);
        this.function = function;
        this.type = (Class<T>)universe.getClass().getComponentType();
        int sum = 0;
        for(T item : universe) {
            sum |= function.applyAsInt(item);
        }
        this.usedbits = sum;
        this.empty = new Empty<>(this);
    }

    /**
     * Returns immutable empty set.
     *
     * @return immutable empty mask
     */
    public MaskSet<T> empty() {
        return empty;
    }

    /**
     * Creates immutable set with specified enum values.
     * Returned set uses bitwise operations and stores content in just one int.
     *
     * @param values values
     * @return immutable mask
     */
    @SafeVarargs
    public final MaskSet<T> constant(T... values) {
        return new Immutable<>(this, mask(values));
    }

    /**
     * Converts integer into immutable set with specified enum values.
     * Extracts information about content using mapping function associated with this factory.
     * Returned set uses bitwise operations and stores content in just one int.
     *
     * @param value value
     * @return immutable mask
     */
    public final MaskSet<T> constant(int value) {
        if(value != (value & usedbits)) {
            throw new IllegalArgumentException(unknownValueMessage(value));
        }
        return new Immutable<>(this, value);
    }

    /**
     * Creates mutable set with specified enum values.
     * Returned set uses bitwise operations and stores content in just one int.
     *
     * @param values values
     * @return mutable mask
     */
    @SafeVarargs
    public final MaskSet<T> collect(T... values) {
        return new Mutable<>(this, mask(values));
    }

    /**
     * Converts integer into mutable set with specified enum values.
     * Extracts information about content using mapping function associated with this factory.
     * Returned set uses bitwise operations and stores content in just one int.
     *
     * @param value value
     * @return mutable mask
     */
    public MaskSet<T> collect(int value) {
        if(value != (value & usedbits)) {
            throw new IllegalArgumentException(unknownValueMessage(value));
        }
        return new Mutable<>(this, value);
    }

    /**
     * Converts set of enum values into integer representation.
     * Converts values into int using mapping function associated with this factory.
     *
     * @param values values
     * @return int
     */
    public int valueOf(Set<T> values) {
        int out = 0;
        for(T value : values) {
            out |= function.applyAsInt(value);
        }
        return out;
    }

    /**
     * Converts list of enum values into integer representation.
     * Converts values into int using mapping function associated with this factory.
     *
     * @param values values
     * @return int
     */
    @SafeVarargs
    public final int valueOf(T... values) {
        int out = 0;
        for(T value : values) {
            out |= function.applyAsInt(value);
        }
        return out;
    }

    /**
     * Creates new mutable set with the same values as in provided set.
     * Returned set uses bitwise operations and stores content in just one int.
     *
     * @param values values
     * @return mutable mask
     */
    public MaskSet<T> clone(Set<T> values) {
        if(values instanceof MaskSet<?>) {
            return ((MaskSet<T>)values).toMutable();
        }
        return new Mutable<>(this, valueOf(values));
    }

    private int mask(T[] values) {
        int mask = 0;
        for(T item : values) {
            mask |= function.applyAsInt(item);
        }
        return mask;
    }

    int objectToInt(Object object) {
        return function.applyAsInt(type.cast(object));
    }

    int genericToInt(T object) {
        return function.applyAsInt(object);
    }

    String unknownValueMessage(int value) {
        return NO_ELEMENT + Integer.toHexString(value);
    }

}
