///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below.       //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,       //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard        //
// Scheines, Joseph Ramsey, and Clark Glymour.                               //
//                                                                           //
// This program is free software; you can redistribute it and/or modify      //
// it under the terms of the GNU General Public License as published by      //
// the Free Software Foundation; either version 2 of the License, or         //
// (at your option) any later version.                                       //
//                                                                           //
// This program is distributed in the hope that it will be useful,           //
// but WITHOUT ANY WARRANTY; without even the implied warranty of            //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             //
// GNU General Public License for more details.                              //
//                                                                           //
// You should have received a copy of the GNU General Public License         //
// along with this program; if not, write to the Free Software               //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA //
///////////////////////////////////////////////////////////////////////////////

package edu.cmu.tetrad.study.gene.tetradapp.model;

import edu.cmu.tetrad.data.*;
import edu.cmu.tetrad.study.gene.tetrad.gene.graph.DisplayNameHandler;
import edu.cmu.tetrad.graph.Node;
import edu.cmu.tetrad.session.SessionModel;
import edu.cmu.tetrad.study.gene.tetrad.gene.history.*;
import edu.cmu.tetrad.util.Matrix;
import edu.cmu.tetrad.util.Parameters;
import edu.cmu.tetrad.util.dist.Distribution;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * Wraps the MeasurementSimulator class as an instantiated model.
 *
 * @author josephramsey
 */
public class BooleanGlassGeneIm implements SessionModel {
    static final long serialVersionUID = 23L;

    /**
     * @serial Can be null.
     */
    private String name;

    /**
     * @serial Cannot be null.
     */
    private BooleanGlassGenePm genePm;

    /**
     * @serial Cannot be null.
     */
    private BooleanGlassFunction glassFunction;

    /**
     * @serial Cannot be null.
     */
    private BasalInitializer initializer;

    /**
     * @serial Cannot be null.
     */
    private GeneHistory history;

    /**
     * @serial Cannot be null.
     */
    private MeasurementSimulatorParams simulator;

    //============================CONSTRUCTORS============================//

    /**
     * Obtains a boolean Glass function from the boolean Glass gene PM provided
     * and uses it to create a Glass history and a measurement simulator.
     * Editing this IM consists in editing the wrapped measurement simulator.
     *
     * @param genePm the BooleanGlassGenePm from which the BooleanGlassFunction
     *               is extracted.
     */
    public BooleanGlassGeneIm(BooleanGlassGenePm genePm, Parameters parameters) {
        try {
            this.genePm = genePm;

            // These are the two objects which this IM mainly edits.
            this.glassFunction = new BooleanGlassFunction(genePm.getLagGraph());
            this.initializer = new BasalInitializer(this.glassFunction, 0, 1);
            this.history = new GeneHistory(this.initializer, this.glassFunction);
            this.simulator = new MeasurementSimulatorParams(parameters);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Generates a simple exemplar of this class to test serialization.
     */
    public static BooleanGlassGeneIm serializableInstance() {
        return new BooleanGlassGeneIm(
                BooleanGlassGenePm.serializableInstance(), new Parameters());
    }

    //==========================PUBLIC METHODS============================//

    /**
     * Returns the list of factors in the history.
     */
    public List<String> getFactors() {
        List<String> factors = new ArrayList<>();
        IndexedLagGraph connectivity = this.glassFunction.getIndexedLagGraph();

        for (int i = 0; i < connectivity.getNumFactors(); i++) {
            factors.add(connectivity.getFactor(i));
        }

        return factors;
    }

    /**
     * Returns the list of parents of the given factor as String's formatted in
     * the style used by the Tetrad IV display-- e.g. "V1:L2" for factor "V1" at
     * a lag of 2.
     *
     * @param factor the factor, e.g. "V3", for which parents are requested.
     * @return the list of lagged factors which are parents of 'factor'.
     */
    public List<String> getParents(int factor) {
        IndexedLagGraph connectivity = getBooleanGlassFunction()
                .getIndexedLagGraph();
        List<String> displayParents = new ArrayList<>();

        for (int i = 0; i < connectivity.getNumParents(factor); i++) {
            IndexedParent parent = connectivity.getParent(factor, i);
            String name = connectivity.getFactor(parent.getIndex());
            LaggedFactor laggedFactor = new LaggedFactor(name, parent.getLag());
            String displayString =
                    DisplayNameHandler.getDisplayString(laggedFactor);

            displayParents.add(displayString);
        }

        return displayParents;
    }

    /**
     * Returns the MeasurementSimulator. The simulation parameters can be edited
     * directly with this. (Needs to be cast to MeasurementSimulator.)
     */
    public Object getSimulationParams() {
        return this.simulator;
    }

    /**
     * Returns the number of parents that a given factor has.
     *
     * @param factor the given factor, e.g. "V3", formatted as a String name.
     */
    public int getNumParents(int factor) {
        IndexedLagGraph connectivity = getBooleanGlassFunction()
                .getIndexedLagGraph();

        return connectivity.getNumParents(factor);
    }

    /**
     * Returns the value in the given row of the boolean table for the given
     * factor.
     *
     * @return true or false.
     */
    public boolean getRowValueAt(int factor, int row) {
        return getBooleanGlassFunction().getSubFunction(factor).getValue(row);
    }

    /**
     * Sets the value in the given row of the boolean table for the given
     * factor to the given value (true/false).
     */
    public void setRowValueAt(int factor, int row, boolean value) {
        getBooleanGlassFunction().getSubFunction(factor).setValue(row, value);
    }

    public void setSimulator(MeasurementSimulatorParams simulator) {
        simulator.setHistory(getHistory());
        this.simulator = simulator;
    }

    /**
     * Uses the MeasurementSimulator class to simulate a set of measurement data
     * and optionally a set of raw cell expression data. For details of the
     * measurement simulator, see that class.
     *
     * @return a DataModelList containing either one or two models, depending on
     * whether measurement data alone is saved or whether raw data is
     * additionally saved.
     */
    public DataModelList simulateData() {

        // Simulate the data using the simulator.
        this.simulator.simulate(this.history);

        // This is the object that will be returned; it can store
        // multiple data sets.
        DataModelList dataModelList = new DataModelList();

        List<Node> variables = new LinkedList<>();

        if (this.simulator.isIncludeDishAndChipVariables()) {
            DiscreteVariable dishVar = new DiscreteVariable("Dish");
            DiscreteVariable chipVar = new DiscreteVariable("Chip");

            variables.add(dishVar);
            variables.add(chipVar);
        }

        // Fetch the measured data and convert it.
        double[][][] measuredData = this.simulator.getMeasuredData();
        int[] timeSteps = this.simulator.getTimeSteps();
        List<String> factors =
                new ArrayList<>(this.genePm.getLagGraph().getFactors());

        // Order: G1:t1, G2:t1, G3:t1, G1:t1, G2:t2, G3:t2,...
        for (int i = 0; i < measuredData[0].length; i++) {
            for (int j = 0; j < measuredData.length; j++) {
                String name = factors.get(j) + ":t" + timeSteps[i];
                ContinuousVariable var = new ContinuousVariable(name);
                variables.add(var);
            }
        }

        DataSet measuredDataSet =
                new BoxDataSet(new DoubleDataBox(measuredData[0][0].length, variables.size()), variables);

//        System.out.println(measuredDataSet);

        // Order: G1:t1, G2:t1, G3:t1, G1:t1, G2:t2, G3:t2,...
        for (int i = 0; i < measuredData[0].length; i++) {
            for (int j = 0; j < measuredData.length; j++) {
                double[] _data = measuredData[j][i];
                String name = factors.get(j) + ":t" + timeSteps[i];
                ContinuousVariable var =
                        (ContinuousVariable) measuredDataSet.getVariable(name);
                int col = measuredDataSet.getVariables().indexOf(var);

                for (int i1 = 0; i1 < _data.length; i1++) {
                    measuredDataSet.setDouble(i1, col, _data[i1]);
                }
            }
        }


        measuredDataSet.setName("Measurement Data");
        dataModelList.add(measuredDataSet);

        if (this.simulator.isIncludeDishAndChipVariables()) {
            for (int i = 0; i < measuredData[0][0].length; i++) {
                int samplesPerDish = this.simulator.getNumSamplesPerDish();
                measuredDataSet.setInt(i, 0, i / samplesPerDish + 1);
                measuredDataSet.setInt(i, 1, i + 1);
            }
        }

        // Fetch the measured data and convert it.
        if (this.simulator.isRawDataSaved()) {
            double[][][] rawData = this.simulator.getRawData();
            List<Node> _variables = new LinkedList<>();

            // Order: G0:t1, G1:t1, G2:t1, G0:t1, G1:t2, G2:t2,...
            for (int i = 0; i < rawData[0].length; i++) {
                for (int j = 0; j < rawData.length; j++) {
                    String name = "G" + (j + 1) + ":t" + timeSteps[i];
                    _variables.add(new ContinuousVariable(name));
                }
            }

            DataSet rawDataSet =
                    new BoxDataSet(new DoubleDataBox(rawData[0][0].length, _variables.size()), variables);

            for (int i = 0; i < rawData[0].length; i++) {
                for (int j = 0; j < rawData.length; j++) {
                    double[] _data = rawData[j][i];
                    String name = "G" + (j + 1) + ":t" + timeSteps[i];
                    Node var = rawDataSet.getVariable(name);
                    int col = rawDataSet.getVariables().indexOf(var);

                    for (int i1 = 0; i1 < _data.length; i1++) {
                        rawDataSet.setDouble(i1, col, _data[i1]);
                    }
                }
            }

            int n = rawData[0][0].length;
            int cellsPerDish = this.simulator.getNumCellsPerDish();

            if (this.simulator.isIncludeDishAndChipVariables()) {
                DiscreteVariable dishVar2 =
                        new DiscreteVariable("Dish", n / cellsPerDish + 1);

                rawDataSet.addVariable(0, dishVar2);
            }
            rawDataSet.setName("Raw Data");

            if (this.simulator.isIncludeDishAndChipVariables()) {
                for (int i = 0; i < n; i++) {
                    rawDataSet.setInt(i, 0, i / cellsPerDish + 1);
                }
            }

            dataModelList.add(rawDataSet);
        }

        if (measuredData[0][0].length == 1) {
            dataModelList.add(0, asTimeSeriesData(measuredData, factors));
        }

        return dataModelList;
    }

    private TimeSeriesData asTimeSeriesData(double[][][] cube,
                                            List<String> factors) {
        int numTimeSteps = cube[0].length;
        int numFactors = cube.length;
        double[][] square = new double[numTimeSteps][numFactors];

        for (int timeStep = 0; timeStep < numTimeSteps; timeStep++) {
            for (int factor = 0; factor < numFactors; factor++) {
                square[timeStep][factor] = cube[factor][timeStep][0];
            }
        }

        List<String> varNames = new ArrayList<>();
        for (int i = 0; i < numFactors; i++) {
            varNames.add(factors.get(i));
        }

        return new TimeSeriesData(new Matrix(square), varNames);
    }

    /**
     * Returns the Glass function case to BooleanGlassFunction for convenience.
     */
    public BooleanGlassFunction getBooleanGlassFunction() {
        return this.glassFunction;
    }

    /**
     * Returns the Glass history case to GlassHistory for convenience.
     */
    public GeneHistory getHistory() {
        return this.history;
    }

    /**
     * Sets the error distribution for the given factor to the given
     * distribution. Values for the transcription error for this factor will be
     * drawn from this distribution.
     */
    public void setErrorDistribution(int factor, Distribution distribution) {
        getBooleanGlassFunction().setErrorDistribution(factor, distribution);
    }

    /**
     * Returns the error distribution for the given factor.
     *
     * @see #setErrorDistribution
     */
    public Distribution getErrorDistribution(int factor) {
        return getBooleanGlassFunction().getErrorDistribution(factor);
    }

    /**
     * Adds semantic checks to the default deserialization method. This method
     * must have the standard signature for a readObject method, and the body of
     * the method must begin with "s.defaultReadObject();". Other than that, any
     * semantic checks can be specified and do not need to stay the same from
     * version to version. A readObject method of this form may be added to any
     * class, even if Tetrad sessions were previously saved out using a version
     * of the class that didn't include it. (That's what the
     * "s.defaultReadObject();" is for. See J. Bloch, Effective Java, for help.
     */
    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();

        if (this.genePm == null) {
            throw new NullPointerException();
        }

        if (this.glassFunction == null) {
            throw new NullPointerException();
        }

        if (this.initializer == null) {
            throw new NullPointerException();
        }

        if (this.history == null) {
            throw new NullPointerException();
        }

        if (this.simulator == null) {
            throw new NullPointerException();
        }
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}






