/*
 * Copyright (C) 2017 avigier
 *
 * 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 3 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, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 */
package simulationplans;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.Writer;
import java.io.File;
import java.io.FileReader;
import java.util.Arrays;
import static java.util.Arrays.asList;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.nuiton.math.matrix.*;
import org.nuiton.util.*;
import org.nuiton.topia.*;
import resultinfos.*;

import fr.ifremer.isisfish.util.Doc;
import fr.ifremer.isisfish.*;
import fr.ifremer.isisfish.simulator.SimulationContext;
import fr.ifremer.isisfish.types.TimeStep;
import fr.ifremer.isisfish.datastore.ResultStorage;
import fr.ifremer.isisfish.entities.*;
import fr.ifremer.isisfish.simulator.SimulationPlan;
import fr.ifremer.isisfish.simulator.SimulationPlanContext;
import fr.ifremer.isisfish.simulator.SimulationParameter;
import fr.ifremer.isisfish.datastore.SimulationStorage;
import fr.ifremer.isisfish.simulator.ResultManager;

/**
 * LHSTargetFactors.java
 */
public class LHSTargetFactors implements SimulationPlan {

    /** to use log facility, just put in your code: log.info("..."); */
    private static Log log = LogFactory.getLog(LHSTargetFactors.class);
    static private final String MATRIX = "matrix";
    private MatrixND matrix = null;
    
    @Doc("Population which parameters are calibrated")
    public Population param_Population = null;
    @Doc("File containing weightings on weight data")
    public String param_weightingsweightFile = "input/Weightingsweight.csv";    
    @Doc("File containing weightings on length composition data")
    public String param_weightingsLFDFile = "input/WeightingsLFD.csv";
    int param_parameterNumber = 37;
    @Doc("GA parameter: the alpha parameter in the objective function. The higher alpha, the higher the weight accorded to length compositions compared to total weight.")
    public double param_alphaOF= 10;
    public int param_first = 0;
    public int param_simulationNumber = 10;
    public String param_directory = "input/LHSSimulationPlanTest10.csv";
    public String param_exportPath = "output/ExportLHSTargetfactors.csv";
    public String param_observationFile = "input/CatchWeightPerLengthSemantics.csv";
    protected String exportHisto = "";
    protected File observationsFile;
    protected MatrixND matrixObservations;
    protected MatrixND matrixWeightingsweight;
    protected MatrixND matrixWeightingsLFD;
    protected File exportHistoric;
    protected File weightingsweightFile;
    protected File weightingsLFDFile;
    String [] metierString = {"Tarf.PTBV_VIIIabd","Tarf.OBTS_VIIIabd","Tarf.Metier_ChalutMixte_NordPC","Tarf.Metier_ChaluMixte_InterC","Tarf.Metier_ChalutBenth_NordAPC","Tarf.Metier_ChalutBenth_NordC","Tarf.Metier_ChalutBenth_APCS","Tarf.Metier_Lang_NordPC","Tarf.Metier_Lang_InterC","Tarf.Metier_FiletSole_NordC","Tarf.Metier_FiletSole_NordIntPC","Tarf.Metier_FiletSole_InterSudPC","Tarf.Metier_FiletSole_InterC","Tarf.Metier_ChalutSole_InterC","Tarf.Metier_PalangreMerlu_InterSudAC","Tarf.Metier_PalangreMerlu_InterSudC","Tarf.Metier_PalangreMixte_NordC","Tarf.Metier_PalangreMixte_InterC","Tarf.Metier_ChalutSole_InterSudC","Tarf.Metier_ChalutSole_NordCet","Tarf.Metier_FiletMerlu_NordC","Tarf.Metier_FiletMerlu_NordPC","Tarf.Metier_FiletMixte_NordPC","Tarf.Metier_FiletMixte_NordC","Tarf.Metier_FiletMixte_NordInterPC","Tarf.Metier_FiletMixte_InterSudC","Tarf.Metier_FiletMIxte_InterC","Tarf.Metier_FiletMerlu_InterSudAPC","Tarf.Metier_FiletMerlu_InterSudC","Tarf.Metier_ChalutMixte_NordC","Tarf.Metier_FiletMerlu_InterSudPC","Tarf.Metier_ChalutMixte_APCS","Tarf.Metier_Lang_InterPC"};
    String [] seasonString = {"Tarf.season1","Tarf.season2","Tarf.season3","Tarf.season4"};
    int compteurSimus;
    
    protected String[] necessaryResult = {
        // put here all necessary result for this rule
        // example:
        // MatrixBiomass.NAME,
        // MatrixNetValueOfLandingsPerStrategyMet.NAME,
    };

    @Override
    public String[] getNecessaryResult() {
        return this.necessaryResult;
    }

    /**
     * Permet d'afficher a l'utilisateur une aide sur le plan.
     * @return L''aide ou la description du plan
     */
    @Override
    public String getDescription() throws Exception {
        // TODO change description
        return "LHS on target factors to reduce the parametrization space";
    }

    /**
     * Appel au demarrage de la simulation, cette methode permet d'initialiser
     * des valeurs
     * @param simulation La simulation pour lequel on utilise cette regle
     */
    public void init(SimulationPlanContext context) throws Exception {
        //Get parameter values computed by LHS
        exportHistoric = new File(param_exportPath);
        File dir = new File(param_directory);
        matrix = MatrixFactory.getInstance().create(new int[]{param_simulationNumber, param_parameterNumber});
        matrix.importCSV(new FileReader(dir), new int[]{0,0});

        //Get observations
        observationsFile = new File(param_observationFile);
        matrixObservations = MatrixFactory.getInstance().create(observationsFile);

        //Get weightings
        weightingsweightFile = new File(param_weightingsweightFile);
        matrixWeightingsweight = MatrixFactory.getInstance().create(weightingsweightFile);
        weightingsLFDFile = new File(param_weightingsLFDFile);
        matrixWeightingsLFD = MatrixFactory.getInstance().create(weightingsLFDFile);
     }

    /**
     * Call before each simulation, changes database.
     * 
     * @param context plan context
     * @param nextSimulation storage used for next simulation
     * @return true if we must do next simulation, false to stop plan
     * @throws Exception
     */
    @Override
    public boolean beforeSimulation(SimulationPlanContext context,SimulationStorage nextSimulation) throws Exception {
        int simNum = nextSimulation.getParameter().getSimulationPlanNumber();
        if (simNum + param_first < param_simulationNumber) {
            int counter = 0;
            int countSeas = 0;
            
            // Get simulationContext and alter it. DO NOT use ToPIA contexts, to much risks of locking the database for nothing.
            // Use a pre-script, that will be written by the optimization script, to change the database without risk.
            SimulationParameter params = nextSimulation.getParameter();
            
            //Change target factors values for each metier. Parameters are always stocked in the same order : first all the m��tiers, in metierList order, then the 4 seasons.
            String script = "";
            for (String metier : metierString) {
                int [] coord = {simNum,counter};
                double tf = matrix.getValue(coord);
                script = script + "context.setComputeValue(\""+metier+"\", "+tf+");\n";
                counter+=1;
                //System.out.println("Generation 0, experience "+exp.id+", metier "+metier.getName()+ ", nouvelle valeur ciblage "+exp.parametrisation[metierList.indexOf(metier)].value +" espece : " + ts.getSpecies().getName());
            }
    
            //Change target factors for each season
            for(String seasLabel : seasonString){
                int [] coord = {simNum,counter};
                String label = seasonString[countSeas];
                double tf = matrix.getValue(coord);
                script = script + "context.setComputeValue(\""+seasLabel+"\", "+tf+");\n";
                counter+=1;
                countSeas+=1;
            }
    
            if (!params.getUsePreScript()) {
                params.setUsePreScript(true);
            }
    
            params.setPreScript(script);
            nextSimulation.setParameter(params); // on force la sauvegarde            
         	return true;
        } else {
            return false;
        }     
    }

    /**
     * Call after each simulation.
     * 
     * @param context plan context
     * @param lastSimulation storage used for simulation
     * @return true if we must do next simulation, false to stop plan
     * @throws Exception
     */
    @Override
    public boolean afterSimulation(SimulationPlanContext context,SimulationStorage lastSimulation) throws Exception {
        int simNum = lastSimulation.getParameter().getSimulationPlanNumber();
        ResultStorage result = lastSimulation.getResultStorage();
        boolean bool=true;
        compteurSimus=lastSimulation.getParameter().getSimulationPlanNumber(); //numero de la simulation qui vient de se terminer
        
        //Get estimates from a specific export. It would be betetr to implement it in a specific result, but is heavy to implement...
        //Get export location. This is tricky, since the getter for the simulation path cannot be used
        String exportFilename = lastSimulation.getSimulationLogFile(); 
        exportFilename = exportFilename.substring(0,exportFilename.length()-14) + "resultExports/CapturesPoidsAgregPourOptim.csv";
        
        //Import the .csv file
        File estimatesFile = new File(exportFilename);
        MatrixND matrixEstimates = MatrixFactory.getInstance().create(estimatesFile);

        //Convert from kilgrams to tonnes
        matrixEstimates = matrixEstimates.divs(1000);

        //Add spanish longliners and gillnetters catch (landings + discards)
        File spanishFile = new File("C:/Users/avigier/isis-fish-4.4.1.0-rc-1/output/catchSpanishLGWithRule" + simNum + ".csv");
        MatrixND matrixEstimatesSpLG = MatrixFactory.getInstance().create(spanishFile);
        matrixEstimates = matrixEstimates.add(matrixEstimatesSpLG);
      
        //Here, matrixObservations and matrixEstimates should have exactly the same semantics.
        //Convert estimates steps to seasons and aggregate them
        matrixEstimates = matrixEstimates.sumOverDim(0,9,3);
        matrixEstimates = matrixEstimates.sumOverDim(0,6,3);
        matrixEstimates = matrixEstimates.sumOverDim(0,3,3);
        matrixEstimates = matrixEstimates.sumOverDim(0,0,3);
        List<Integer> seasonNums = asList(1,2,3,4);
        matrixEstimates.setSemantic(0,seasonNums);
        
        //Here, matrixObservations and matrixEstimates should have exactly the same semantics and dimensions.
        //Compute objective function components and store them in the Experience objects
        double objectiveComponent1 =0;
        double objectiveComponent2 =0;
        MatrixND matrixObservationsSumLength = matrixObservations.sumOverDim(3).reduce(); //Sum on length bins
        MatrixND matrixEstimatesSumLength = matrixEstimates.sumOverDim(3).reduce(); //Sum on length bins
        
        // Compute first objective function component per SS3 metier
        double firstComponentTab [] = new double[4];
        for (int i=0; i<4; i++){
            MatrixND subMatrixEst = matrixEstimates.copy();
            subMatrixEst = subMatrixEst.getSubMatrix(1,i,1);
            for(MatrixIterator matIter = matrixEstimates.iterator(); matIter.hasNext();){ // For each observation.estimate couple, compute objective function components
                matIter.next();
                Object[] sems = matIter.getSemanticsCoordinates();
                Object[] semsSumLength = {sems[0],sems[1],sems[2]}; // No semantics on length classes
                String metierName = (String) sems[1];
                String partitionName = (String) sems[2];
                double estValue = matIter.getValue();
                double obsValue = matrixObservations.getValue(sems);
                //Sums on length always sum at 0 because of a bad use of semantics?
                double obsValueSumLength = matrixObservationsSumLength.getValue(semsSumLength);
                double estValueSumLength = matrixEstimatesSumLength.getValue(semsSumLength);
                double weightingLFD = matrixWeightingsLFD.getValue(semsSumLength);
                
                //If loops = securities to avoid to divide by 0
                if((obsValueSumLength!=0)&(estValueSumLength!=0)){
                    objectiveComponent1= objectiveComponent1 + param_alphaOF*weightingLFD*Math.pow(((obsValue/obsValueSumLength)-(estValue/estValueSumLength)),2);
                }else if((obsValueSumLength==0)){// May happen for fleets not discarding in the observations. We do not optimize on "non observations" => OF=0
                    objectiveComponent1= objectiveComponent1 + 0;
                }else if((obsValueSumLength!=0)&(estValueSumLength==0)){
                    objectiveComponent1= objectiveComponent1 + param_alphaOF*weightingLFD*Math.pow(((obsValue/obsValueSumLength)-(0)),2);
                }
            }
            firstComponentTab[i] = objectiveComponent1;
        }

       // Compute second objective function component per SS3 metier
        double secondComponentTab [] = new double[4];
        for (int i=0; i<4; i++){
            MatrixND subMatrixSumLengthEst = matrixEstimatesSumLength.copy();
            subMatrixSumLengthEst = subMatrixSumLengthEst.getSubMatrix(1,i,1);
            for(MatrixIterator matIter = subMatrixSumLengthEst.iterator(); matIter.hasNext();){
                matIter.next();
                Object[] sems = matIter.getSemanticsCoordinates();
                String metierName = (String) sems[1];
                String partitionName = (String) sems[2];
                double estValueSumLength = matIter.getValue();
                double obsValueSumLength = matrixObservationsSumLength.getValue(sems);
                double weightingweight = matrixWeightingsweight.getValue(sems);
                //If loops = securities to avoid to divide by 0
                if((obsValueSumLength!=0)){
                    objectiveComponent2= objectiveComponent2 + weightingweight*Math.pow(((obsValueSumLength-estValueSumLength)/obsValueSumLength),2);
                }else if((obsValueSumLength==0)){// May happen for fleets not discarding in the observations. We do not optimize on "non observations" => OF=0
                    objectiveComponent2= objectiveComponent2 + 0;
                }                
            }
            secondComponentTab[i] = objectiveComponent2;
        }
        //Save in the trace file. There is no need to save the parametriztaion, as it is already there in the input file. Use the simulation number to associate objective function components to the parametrization.
        exportHisto=""+simNum;
        for(int i =0; i<firstComponentTab.length; i++){
            exportHisto = exportHisto +";";
            exportHisto = exportHisto + firstComponentTab[i];
        }
        for(int i =0; i<secondComponentTab.length; i++){
            exportHisto = exportHisto +";";
            exportHisto = exportHisto + firstComponentTab[i];
        }
        exportHisto = exportHisto+ "\n";
        FileUtils.writeStringToFile(exportHistoric,exportHisto,true);
    
        return true;
    }
}
