/*
 *
 *
 *    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.
 *
 *
 */

/**
 * ****************** ISSUE LIST ******************************
 *
 *
 *
 * 1. getConditioningVariables change to getParentsVariables()
 *
 * 2. Rewrite the naturalmparameters and momementsparametes interfaces to allow sparse implementations and translage
 * that to the implementation of setNatural and setMoments
 *
 *
 *
 * **********************************************************
 */

//TODO: Consider the log-base-measure when defining the base distribution.

package eu.amidst.core.exponentialfamily;

import com.google.common.util.concurrent.AtomicDouble;
import eu.amidst.core.distribution.BaseDistribution_MultinomialParents;
import eu.amidst.core.distribution.ConditionalDistribution;
import eu.amidst.core.distribution.Distribution;
import eu.amidst.core.utils.MultinomialIndex;
import eu.amidst.core.utils.Vector;
import eu.amidst.core.variables.Assignment;
import eu.amidst.core.variables.DistributionTypeEnum;
import eu.amidst.core.variables.Variable;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This class defines conditional distributions with a set of multinomial parents. It aims to exploit some underlying
 * structure when representing them in exponential family.
 *
 * <p> For further details about how these models are considered in this toolbox look at the following paper </p>
 * <p> <i>Representation, Inference and Learning of Bayesian Networks as Conjugate Exponential Family Models. Technical Report.</i>
 * (<a href="http://amidst.github.io/toolbox/docs/ce-BNs.pdf">pdf</a>)
 * </p>
 *
 * @param <E>, the type of EF_ConditionalDistribution of the base distribution.
 */
public class EF_BaseDistribution_MultinomialParents<E extends EF_ConditionalDistribution> extends EF_ConditionalDistribution {

    /** Represents the list of base distributions. */
    private final List<E> distributions;

    /** Represents the list of the multinomial parents. */
    private final List<Variable> multinomialParents;

    /** Indicates whether the base distributions are conditional or univariate. */
    private boolean isBaseConditionalDistribution;

    /**
     * Creates a new EF_BaseDistribution_MultinomialParents object from a list of multinomial parents and base distributions.
     * @param multinomialParents1 a list of {@code Variable} objects.
     * @param distributions1 a list of {@code EF_ConditionalDistribution} objects.
     */
    public EF_BaseDistribution_MultinomialParents(List<Variable> multinomialParents1, List<E> distributions1) {
        this(multinomialParents1,distributions1,true);
    }

    /**
     * Creates a new EF_BaseDistribution_MultinomialParents object from a list of multinomial parents and base distributions.
     * @param multinomialParents1 a list of {@code Variable} objects.
     * @param distributions1 a list of {@code EF_ConditionalDistribution} objects.
     * @param initializeMomentNaturalParameters a boolean value indicating whether the moment and natural parameters
     *                                           of the base distributions should be initialized.
     */
    public EF_BaseDistribution_MultinomialParents(List<Variable> multinomialParents1, List<E> distributions1, boolean initializeMomentNaturalParameters) {

        //if (multinomialParents1.size()==0) throw new IllegalArgumentException("Size of multinomial parents is zero");
        if (distributions1.size() == 0) throw new IllegalArgumentException("Size of base distributions is zero");

        int size = MultinomialIndex.getNumberOfPossibleAssignments(multinomialParents1);
        if (size != distributions1.size())
            throw new IllegalArgumentException("Size of base distributions list does not match with the number of parents configurations");

        this.var = distributions1.get(0).getVariable();
        this.multinomialParents = multinomialParents1;
        this.distributions = distributions1;

        this.parents = new ArrayList();
        for (Variable v : this.multinomialParents)
            this.parents.add(v);

        if (distributions.get(0) instanceof EF_ConditionalDistribution) {
            this.isBaseConditionalDistribution = true;
            for (int i = 0; i < size; i++) {
                for (Variable v : this.getBaseEFConditionalDistribution(i).getConditioningVariables()) {
                    if (!this.parents.contains(v))
                        this.parents.add(v);
                }
            }
        } else {
            this.isBaseConditionalDistribution = false;
        }

        this.naturalParameters = null;
        this.momentParameters = null;

        if (initializeMomentNaturalParameters) {
            CompoundVector vectorNatural = this.createCompoundVector();

            for (int i = 0; i < numberOfConfigurations(); i++) {
                vectorNatural.setBaseConf(i, -this.getBaseEFDistribution(i).computeLogNormalizer());
                vectorNatural.setVectorByPosition(i, this.getBaseEFDistribution(i).getNaturalParameters());
            }

            this.naturalParameters = vectorNatural;
        }

        //Make them unmodifiable
        //this.parents = Collections.unmodifiableList(this.parents);
    }

    /**
     * Returns the list of multinomial parents.
     * @return a list of {@code Variable} objects.
     */
    public List<Variable> getMultinomialParents() {
        return multinomialParents;
    }

    /**
     * Returns whether the base distributions are conditional or not.
     * @return {@code true} if the base distributions are conditional, {@code false} otherwise.
     */
    public boolean isBaseConditionalDistribution() {
        return isBaseConditionalDistribution;
    }

    /**
     * Returns the base EF conditional distribution for a given indexed configuration of the multinomial parents.
     * @param multinomialIndex a positive integer.
     * @return a {@code EF_ConditionalDistribution} object.
     */
    public EF_ConditionalDistribution getBaseEFConditionalDistribution(int multinomialIndex) {
        return (EF_ConditionalDistribution)this.getBaseEFDistribution(multinomialIndex);
    }

    /**
     * Gets the base EF univariate distribution for a given indexed configuration of the multinomial parents.
     * @param multinomialIndex, a positive integer.
     * @return a {@code EF_UnivariateDistribution} object.
     */
    public EF_UnivariateDistribution getBaseEFUnivariateDistribution(int multinomialIndex) {
        return (EF_UnivariateDistribution)this.getBaseEFDistribution(multinomialIndex);
    }

    /**
     * Sets the base EF conditional distribution for a given indexed configuration of the multinomial parents.
     * @param indexMultinomial a positive integer.
     * @param baseDist a {@code EF_ConditionalDistribution} object.
     */
    public void setBaseEFDistribution(int indexMultinomial, E baseDist) {
        this.distributions.set(indexMultinomial, baseDist);
    }

    /**
     * Returns a base EF distribution for a given indexed configuration of the multinomial parents.
     * @param indexMultinomial a positive integer.
     * @return a {@code EF_ConditionalDistribution} object.
     */
    public E getBaseEFDistribution(int indexMultinomial) {
        return distributions.get(indexMultinomial);
    }

    /**
     * Returns the number of configurations of the multinomial parents.
     * @return an {@code int} value that represents the number of configurations of the multinomial parents.
     */
    public int numberOfConfigurations() {
        return this.distributions.size();
    }

    /**
     * Returns the size of the sufficient statistics of the base EF distributions.
     * @return an {@code int} value that represents the size of the sufficient statistics of the base EF distributions..
     */
    public int sizeOfBaseSufficientStatistics() {
        return this.getBaseEFDistribution(0).sizeOfSufficientStatistics();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SufficientStatistics getSufficientStatistics(Assignment instance) {

        CompoundVector vector = this.createCompoundVector();

        int position = MultinomialIndex.getIndexFromVariableAssignment(this.multinomialParents, instance);

        vector.setBaseConf(position, 1.0);

        SufficientStatistics sufficientStatisticsBase = this.getBaseEFDistribution(position).getSufficientStatistics(instance);

        vector.setVectorByPosition(position, sufficientStatisticsBase);

        return vector;

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int sizeOfSufficientStatistics() {
        return numberOfConfigurations() + numberOfConfigurations() * sizeOfBaseSufficientStatistics();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateNaturalFromMomentParameters() {

        CompoundVector globalMomentsParam = (CompoundVector) this.momentParameters;

        for (int i = 0; i < numberOfConfigurations(); i++) {
            MomentParameters moment = (MomentParameters) globalMomentsParam.getVectorByPosition(i);
            moment.divideBy(globalMomentsParam.getBaseConf(i));
            this.getBaseEFDistribution(i).setMomentParameters(moment);
        }

        CompoundVector vectorNatural = this.createCompoundVector();


        for (int i = 0; i < numberOfConfigurations(); i++) {
            vectorNatural.setBaseConf(i, -this.getBaseEFDistribution(i).computeLogNormalizer());
            vectorNatural.setVectorByPosition(i, this.getBaseEFDistribution(i).getNaturalParameters());
        }

        this.naturalParameters = vectorNatural;

        return;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateMomentFromNaturalParameters() {
        throw new UnsupportedOperationException("This method does not apply in this case!");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double computeLogBaseMeasure(Assignment dataInstance) {
        int position = MultinomialIndex.getIndexFromVariableAssignment(this.multinomialParents, dataInstance);
        return this.getBaseEFDistribution(position).computeLogBaseMeasure(dataInstance);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double computeLogNormalizer() {
        return 0;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Vector createZeroVector() {
        return this.createCompoundVector();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SufficientStatistics createInitSufficientStatistics() {
        CompoundVector vector = this.createCompoundVector();

        for (int i = 0; i < this.numberOfConfigurations() ; i++) {
            Vector vec = this.getBaseEFDistribution(i).createInitSufficientStatistics();
            vector.setBaseConf(i, 1.0/(double)this.numberOfConfigurations());
            vec.multiplyBy(1.0/(double)this.numberOfConfigurations());
            vector.setVectorByPosition(i,vec);
        }

        return vector;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public double getExpectedLogNormalizer(Variable parent, Map<Variable, MomentParameters> momentChildCoParents) {
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double getExpectedLogNormalizer(Map<Variable, MomentParameters> momentParents) {

        int nConf = MultinomialIndex.getNumberOfPossibleAssignments(this.multinomialParents);

        double expectedLogNormalizer = 0;

        for (int i = 0; i < nConf; i++) {
            double[] assignment = MultinomialIndex.getVariableArrayAssignmentFromIndex(this.multinomialParents, i);
            double momentValue = 1;
            for (int j = 0; j < assignment.length; j++) {
                momentValue*=momentParents.get(this.multinomialParents.get(j)).get((int)assignment[j]);
            }
            if (momentValue==0)
                continue;

            double partialLogNormalizer = 0;

            if (this.isBaseConditionalDistribution) {
                partialLogNormalizer = this.getBaseEFConditionalDistribution(i).getExpectedLogNormalizer(momentParents);
            }else{
                partialLogNormalizer = this.getBaseEFUnivariateDistribution(i).computeLogNormalizer();
            }

            expectedLogNormalizer+=momentValue*partialLogNormalizer;
        }

        return expectedLogNormalizer;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NaturalParameters getExpectedNaturalFromParents(Map<Variable, MomentParameters> momentParents) {

        int nConf = MultinomialIndex.getNumberOfPossibleAssignments(this.multinomialParents);

        NaturalParameters expectedNaturalFromParents = null;

        for (int i = 0; i < nConf; i++) {
            double[] assignment = MultinomialIndex.getVariableArrayAssignmentFromIndex(this.multinomialParents, i);
            double momentValue = 1;
            for (int j = 0; j < assignment.length; j++) {
                momentValue*=momentParents.get(this.multinomialParents.get(j)).get((int)assignment[j]);
            }
            NaturalParameters paritalExpectedNatural = null;

            if (this.isBaseConditionalDistribution) {
                paritalExpectedNatural = this.getBaseEFConditionalDistribution(i).getExpectedNaturalFromParents(momentParents);
            }else{
                paritalExpectedNatural = this.getBaseEFUnivariateDistribution(i).createZeroNaturalParameters();
                paritalExpectedNatural.copy(this.getBaseEFUnivariateDistribution(i).getNaturalParameters());
            }

            paritalExpectedNatural.multiplyBy(momentValue);
            if (expectedNaturalFromParents==null){
                expectedNaturalFromParents=paritalExpectedNatural;
            }else {
                expectedNaturalFromParents.sum(paritalExpectedNatural);
            }
        }

        return expectedNaturalFromParents;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NaturalParameters getExpectedNaturalToParent(Variable parent, Map<Variable, MomentParameters> momentChildCoParents) {
        NaturalParameters expectedNaturalToParents = null;

        int indexOfMultinomialParent = this.multinomialParents.indexOf(parent);

        if (indexOfMultinomialParent==-1 && parent.isMultinomial())
            throw new IllegalArgumentException("Parent Variable is multinomial and not included in the list of multinomial parents: "+parent.getName());


        if (indexOfMultinomialParent!=-1) {
            expectedNaturalToParents =  new EF_Multinomial(parent).createZeroNaturalParameters();//  new ArrayVector(parent.getNumberOfStates());

            int nConf = MultinomialIndex.getNumberOfPossibleAssignments(this.multinomialParents);

            for (int state = 0; state<parent.getNumberOfStates(); state++) {
                double partialSum = 0;
                for (int i = 0; i < nConf; i++) {
                    double[] assignment = MultinomialIndex.getVariableArrayAssignmentFromIndex(this.multinomialParents, i);

                    if (assignment[indexOfMultinomialParent]!=state)
                        continue;

                    double momentValue = 1;
                    for (int j = 0; j < assignment.length; j++) {
                        if (j==indexOfMultinomialParent)
                            continue;
                        momentValue *= momentChildCoParents.get(this.multinomialParents.get(j)).get((int)assignment[j]);
                    }
                    NaturalParameters paritalExpectedNatural = null;

                    double localSum = 0;
                    if (this.isBaseConditionalDistribution) {
                        localSum = this.getBaseEFConditionalDistribution(i).computeLogProbability(momentChildCoParents);
                        //paritalExpectedNatural = this.getBaseEFConditionalDistribution(i).getExpectedNaturalFromParents(momentChildCoParents);
                        //localSum += paritalExpectedNatural.dotProduct(momentChildCoParents.get(this.getVariable()));
                        //localSum -= this.getBaseEFConditionalDistribution(i).getExpectedLogNormalizer(momentChildCoParents);
                    } else {
                        paritalExpectedNatural = this.getBaseEFUnivariateDistribution(i).createZeroNaturalParameters();
                        paritalExpectedNatural.copy(this.getBaseEFUnivariateDistribution(i).getNaturalParameters());
                        localSum += paritalExpectedNatural.dotProduct(momentChildCoParents.get(this.getVariable()));
                        localSum -= this.getBaseEFUnivariateDistribution(i).computeLogNormalizer();
                    }

                    if (Double.isNaN(momentValue) || Double.isNaN(localSum) || Double.isNaN(partialSum))
                        throw new IllegalStateException("NAN VALUE!");

                    localSum*=momentValue;

                    partialSum+=localSum;
                }

                expectedNaturalToParents.set(state,partialSum);
            }

        }else{

            if (!this.isBaseConditionalDistribution())
                throw new IllegalArgumentException("Parent Variable is no multinomial and based distribution has no parents");

            //int indexOfNonMultinomialParent = this..getBaseEFConditionalDistribution(0).getConditioningVariables().indexOf(parent);
            //if (indexOfMultinomialParent==-1)

            if (!this.parents.contains(parent))
                throw new IllegalArgumentException("Parent Variable is no multinomial and is not included in the list of parents of the base distribution");



            int nConf = MultinomialIndex.getNumberOfPossibleAssignments(this.multinomialParents);

            for (int i = 0; i < nConf; i++) {
                //TODO This might be inefficient for the use of sequential search in a list
                if (!this.getBaseEFConditionalDistribution(i).getConditioningVariables().contains(parent))
                    continue;

                double[] assignment = MultinomialIndex.getVariableArrayAssignmentFromIndex(this.multinomialParents, i);
                double momentValue = 1;
                for (int j = 0; j < assignment.length; j++) {
                        momentValue *= momentChildCoParents.get(this.multinomialParents.get(j)).get((int)assignment[j]);
                }
                NaturalParameters paritalExpectedNatural = null;


                if (this.isBaseConditionalDistribution) {
                    paritalExpectedNatural = this.getBaseEFConditionalDistribution(i).getExpectedNaturalToParent(parent, momentChildCoParents);
                } else {
                    paritalExpectedNatural = this.getBaseEFUnivariateDistribution(i).createZeroNaturalParameters();
                    paritalExpectedNatural.copy(this.getBaseEFUnivariateDistribution(i).getNaturalParameters());
                }

                paritalExpectedNatural.multiplyBy(momentValue);
                if (expectedNaturalToParents == null) {
                    expectedNaturalToParents = paritalExpectedNatural;
                } else {
                    expectedNaturalToParents.sum(paritalExpectedNatural);
                }
            }


        }
        return expectedNaturalToParents;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<EF_ConditionalDistribution> toExtendedLearningDistribution(ParameterVariables parameters, String nameSuffix) {

        if (this.getConditioningVariables().size()==0){
            return this.getBaseEFDistribution(0).toExtendedLearningDistribution(parameters);
        }else {

            List<EF_ConditionalDistribution> totalDists = new ArrayList<>();
            for (int i = 0; i < this.distributions.size(); i++) {
                Assignment assignment =  MultinomialIndex.getVariableAssignmentFromIndex(this.multinomialParents,i);
                String conf = assignment.outputString(this.multinomialParents);
                totalDists.addAll(this.distributions.get(i).toExtendedLearningDistribution(parameters,nameSuffix+conf));
            }

            //List<EF_ConditionalDistribution> totalDists = this.distributions.stream()
            //        .flatMap(dist -> dist.toExtendedLearningDistribution(parameters,nameSuffix).stream())
            //        .collect(Collectors.toList());

            List<EF_ConditionalDistribution> dist_NoParameter = totalDists.stream()
                    .filter(dist -> !dist.getVariable().isParameterVariable())
                    .collect(Collectors.toList());

            List<EF_ConditionalDistribution> dist_Parameter = totalDists.stream()
                    .filter(dist -> dist.getVariable().isParameterVariable())
                    .collect(Collectors.toList());

            EF_BaseDistribution_MultinomialParents<EF_ConditionalDistribution> base =
                    new EF_BaseDistribution_MultinomialParents<>(this.multinomialParents, dist_NoParameter, false);

            dist_Parameter.add(base);
            return dist_Parameter;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <E extends ConditionalDistribution> E toConditionalDistribution() {
        BaseDistribution_MultinomialParents<Distribution> base = new BaseDistribution_MultinomialParents(this.var,this.getConditioningVariables());
        if (this.isBaseConditionalDistribution()) {
            for (int i = 0; i < this.numberOfConfigurations(); i++) {
                base.setBaseDistribution(i, this.getBaseEFConditionalDistribution(i).toConditionalDistribution());
            }
        }else{
            for (int i = 0; i < this.numberOfConfigurations(); i++) {
                base.setBaseDistribution(i, this.getBaseEFUnivariateDistribution(i).toUnivariateDistribution());
            }
        }

        return (E)DistributionTypeEnum.FromBaseDistributionToConditionalDistribution(base);
    }

    private CompoundVector createCompoundVector() {
        return new CompoundVector(this.getBaseEFDistribution(0), this.numberOfConfigurations());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionalDistribution toConditionalDistribution(Map<Variable, Vector> expectedValueParameterVariables) {

        List<Distribution> distributionList = new ArrayList();
        for (EF_Distribution dist: this.distributions){
            EF_ConditionalDistribution learningDistribution = (EF_ConditionalDistribution)dist;
            ConditionalDistribution conditionalDistribution = learningDistribution.toConditionalDistribution(expectedValueParameterVariables);
            if (conditionalDistribution instanceof BaseDistribution_MultinomialParents){
                BaseDistribution_MultinomialParents base = (BaseDistribution_MultinomialParents)conditionalDistribution;
                distributionList.add(base.getBaseDistribution(0));
            }else {
                distributionList.add(conditionalDistribution);
            }
        }

        return DistributionTypeEnum.FromBaseDistributionToConditionalDistribution(new BaseDistribution_MultinomialParents(this.multinomialParents,distributionList));
    }

    //TODO: Replace this CompoundVector by the compoundvector of indicator
    private static class CompoundVector implements SufficientStatistics, MomentParameters, NaturalParameters, Serializable {
        /** Represents the serial version ID for serializing the object. */
        private static final long serialVersionUID = -3436599636425587512L;

        int nConf;
        int baseSSLength;
        double[] baseConf;
        EF_Distribution baseDist;

        SparseVector baseVectors;

        public CompoundVector(EF_Distribution baseDist1, int nConf1) {
            nConf = nConf1;
            this.baseConf = new double[nConf];
            baseDist = baseDist1;
            baseVectors = new SparseVector(baseDist1,nConf);
            baseSSLength = baseDist.sizeOfSufficientStatistics();

        }

        public void setVectorByPosition(int position, Vector vec) {
            baseVectors.setVectorByPosition(position, vec);
        }

        public Vector getVectorByPosition(int position) {
            Vector vector =  this.baseVectors.getVectorByPosition(position);
            if (vector==null){
                return this.baseDist.createZeroVector();
            }else {
                return vector;
            }
        }

        public double getBaseConf(int i) {
            return this.baseConf[i];
        }

        public void setBaseConf(int i, double val) {
            this.baseConf[i] = val;
        }

        @Override
        public double get(int i) {
            if (i < nConf) {
                return this.baseConf[i];
            } else {
                i -= nConf;
                return baseVectors.get(i);
            }
        }

        @Override
        public void set(int i, double val) {
            if (i < nConf) {
                baseConf[i] = val;
            } else {
                i -= nConf;
                baseVectors.set(i,val);
            }
        }

        @Override
        public int size() {
            return nConf + nConf * baseSSLength;
        }

        @Override
        public void sum(Vector vector) {
            this.sum((CompoundVector) vector);
        }

        public SparseVector getBaseVectors() {
            return baseVectors;
        }

        public void sum(CompoundVector vector) {
            if (vector.size() != this.size())
                throw new IllegalArgumentException("Error in variable Vector. Method copy. The parameter vec has a different size.");

            for (int i = 0; i < baseConf.length; i++) {
                baseConf[i] += vector.getBaseConf(i);
            }

            this.baseVectors.sum(vector.getBaseVectors());
        }

        @Override
        public void copy(Vector vector) {
            this.copy((CompoundVector) vector);
        }

        public void copy(CompoundVector vector) {
            if (vector.size() != this.size())
                throw new IllegalArgumentException("Error in variable Vector. Method copy. The parameter vec has a different size.");

            System.arraycopy(vector.baseConf, 0, this.baseConf, 0, this.nConf);
            this.baseVectors.copy(vector.getBaseVectors());
        }

        @Override
        public void divideBy(double val) {
            for (int i = 0; i < this.baseConf.length; i++) {
                this.baseConf[i] /= val;
            }
            this.baseVectors.divideBy(val);
        }

        @Override
        public double dotProduct(Vector vector) {
            return this.dotProduct((CompoundVector) vector);
        }

        public double dotProduct(CompoundVector vector) {
            if (vector.size() != this.size())
                throw new IllegalArgumentException("Error in variable Vector. Method copy. The parameter vec has a different size.");

            double sum = 0;

            for (int i = 0; i < baseConf.length; i++) {
                sum += baseConf[i] * vector.getBaseConf(i);
            }

            sum += this.baseVectors.dotProduct(vector.getBaseVectors());

            return sum;
        }


    }

    private static class SparseVector implements Vector, Serializable {

        /** Represents the serial version ID for serializing the object. */
        private static final long serialVersionUID = -3436599636425587512L;

        EF_Distribution vectorBuilder;

        Map<Integer, Vector> vectorMap;

        int numVectors;

        int baseSize;

        int nonZeroEntries;

        public SparseVector(EF_Distribution vectorBuilder1, int numVectors1) {
            this.vectorBuilder = vectorBuilder1;
            Vector baseVector = this.vectorBuilder.createZeroVector();
            this.baseSize = baseVector.size();
            this.numVectors = numVectors1;
            nonZeroEntries=0;
            vectorMap = new ConcurrentHashMap<Integer,Vector>();
        }

        public void setVectorByPosition(int position, Vector vec) {
            if (vectorMap.containsKey(position)){
                vectorMap.put(position,vec);
            }else{
                vectorMap.put(position,vec);
                this.nonZeroEntries++;
            }
        }

        public Vector getVectorByPosition(int position) {
            return this.vectorMap.get(position);
        }

        public int getNumberNonZeroEntries() {
            return nonZeroEntries;
        }

        @Override
        public double get(int i) {
            int baseIndex = Math.floorDiv(i, this.baseSize);
            if (vectorMap.containsKey(baseIndex))
                return vectorMap.get(baseIndex).get(i % baseSize);
            else
                return 0;
        }

        @Override
        public void set(int i, double val) {
            int baseIndex = Math.floorDiv(i, this.baseSize);
            if (vectorMap.containsKey(baseIndex)) {
                vectorMap.get(baseIndex).set(i % baseSize, val);
            } else {
                Vector baseVector = this.vectorBuilder.createZeroVector();
                baseVector.set(i % baseSize, val);
                vectorMap.put(baseIndex, baseVector);
                nonZeroEntries++;
            }
        }

        @Override
        public int size() {
            return baseSize * numVectors;
        }

        @Override
        public void sum(Vector vector) {
            this.sum((SparseVector)vector);
        }

        public void sum(SparseVector vector) {
            if (vector.size() != this.size())
                throw new IllegalArgumentException("Error in variable Vector. Method copy. The parameter vec has a different size.");

            vector.nonZeroEntries().forEach(entry -> {
                    Vector localVector = this.getVectorByPosition(entry.getKey());
                    Vector outerVector = entry.getValue();
                    if (localVector != null) {
                        localVector.sum(outerVector);
                    }else{
                        Vector newVector = this.vectorBuilder.createZeroVector();
                        newVector.sum(outerVector);
                        this.setVectorByPosition(entry.getKey(),newVector);
                    }
                });
        }

        @Override
        public void copy(Vector vector) {
            this.copy((SparseVector)vector);
        }

        public void copy(SparseVector vector) {
            if (vector.size() != this.size())
                throw new IllegalArgumentException("Error in variable Vector. Method copy. The parameter vec has a different size. ");

            vectorMap = new ConcurrentHashMap<Integer,Vector>();

            vector.nonZeroEntries().forEach(entry -> {
                Vector newVector = this.vectorBuilder.createZeroVector();
                newVector.copy(entry.getValue());
                this.setVectorByPosition(entry.getKey(),newVector);
            });
        }

        @Override
        public void divideBy(double val) {
            this.nonZeroEntries().forEach( entry -> entry.getValue().divideBy(val));
        }

        @Override
        public double dotProduct(Vector vec) {
            return dotProduct((SparseVector)vec);
        }

        public double dotProduct(SparseVector vec) {
            AtomicDouble sum= new AtomicDouble(0);

            if (this.getNumberNonZeroEntries()<vec.getNumberNonZeroEntries()){
                this.nonZeroEntries().forEach(entry ->{
                    Vector outerVector = vec.getVectorByPosition(entry.getKey());
                    if (outerVector!=null)
                        sum.addAndGet(outerVector.dotProduct(entry.getValue()));
                });
            }else{
                vec.nonZeroEntries().forEach(entry ->{
                    Vector localVector = this.getVectorByPosition(entry.getKey());
                    if (localVector!=null)
                        sum.addAndGet(localVector.dotProduct(entry.getValue()));
                });
            }
            return sum.doubleValue();
        }

        public Stream<Map.Entry<Integer,Vector>> nonZeroEntries(){
            return this.vectorMap.entrySet().stream();
        }
    }

    //@FunctionalInterface
    private interface VectorBuilder {
            Vector createZeroedVector();
    }
}
