/* *##%
 * Copyright (C) 2006
 *     Ifremer, Code Lutin, Cédric Pineau, Benjamin Poussin
 *
 * 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.
 *##%*/

/* *
 * DefaultSimulator.java
 *
 * Created: 21 août 2006 10:57:46
 *
 * @author poussin
 * @version $Revision: 1.19 $
 *
 * Last update: $Date: 2007-11-06 18:16:14 $
 * by : $Author: bpoussin $
 */

package simulators;

import static org.codelutin.i18n.I18n._;
import static org.codelutin.i18n.I18n.n_;

import java.util.Collection;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codelutin.math.matrix.MatrixFactory;
import org.codelutin.math.matrix.MatrixND;
import org.codelutin.topia.TopiaContext;
import org.codelutin.topia.TopiaException;

import scripts.GravityModel;
import scripts.ResultName;
import scripts.SiMatrix;
import fr.ifremer.isisfish.IsisFishException;
import fr.ifremer.isisfish.datastore.ResultStorage;
import fr.ifremer.isisfish.entities.Metier;
import fr.ifremer.isisfish.entities.Population;
import fr.ifremer.isisfish.entities.PopulationSeasonInfo;
import fr.ifremer.isisfish.entities.Strategy;
import fr.ifremer.isisfish.entities.Zone;
import fr.ifremer.isisfish.rule.Rule;
import fr.ifremer.isisfish.simulator.MetierMonitor;
import fr.ifremer.isisfish.simulator.PopulationMonitor;
import fr.ifremer.isisfish.simulator.ResultManager;
import fr.ifremer.isisfish.simulator.RuleMonitor;
import fr.ifremer.isisfish.simulator.SimulationContext;
import fr.ifremer.isisfish.simulator.SimulationControl;
import fr.ifremer.isisfish.simulator.SimulationParameter;
import fr.ifremer.isisfish.simulator.Simulator;
import fr.ifremer.isisfish.types.Date;
import fr.ifremer.isisfish.types.Month;

/**
 * @author poussin
 *
 */

public class DefaultSimulator implements Simulator {
    
    /** to use log facility, just put in your code: log.info("..."); */
    static private Log log = LogFactory.getLog(DefaultSimulator.class);

    /**
     * Called by isis don't modify signature
     */
    public void simulate(SimulationContext context) throws Exception {

        SimulationParameter param = context.getSimulationStorage().getParameter();
        SimulationControl control = context.getSimulationControl();        
        int lastYear = param.getNumberOfYear();
        int lastDate = lastYear * Month.NUMBER_OF_MONTH;
        Date date = control.getDate();
        control.setProgressMax(lastDate);
        
        ResultManager resManager = context.getResultManager();
        TopiaContext db = context.getDB();
        
        SiMatrix siMatrix = SiMatrix.getSiMatrix(context);
        GravityModel gravityModel = new GravityModel(context, siMatrix);
        
        PopulationMonitor populationMonitor = context.getPopulationMonitor();
        MetierMonitor metierMonitor = context.getMetierMonitor();
        RuleMonitor ruleMonitor = context.getRuleMonitor();
           
        for (Population pop : siMatrix.getPopulations(date)) {
            MatrixND N = param.getNumberOf(pop);
            N.setName(ResultName.MATRIX_ABUNDANCE);
            populationMonitor.setN(pop, N);
        }
        
        //
        // Rule initialisation
        //
        List<Rule> rules = param.getRules();
        control.setText("Rules initialisation:" + rules);
        for (Rule rule : rules) {
            rule.init(context);
            log.info("Rule " +rule.getClass().getSimpleName()+ " initialized");
        }
        
        //
        // Commit all change done un init rules methods.
        //
        context.getDB().commitTransaction();
        
        //
        // Simulation loop 
        //
        while (date.getDate() < lastDate) {
            //
            // if user stop simulation before last year
            //
            if (control.isStopSimulationRequest()) {
                break;
            }
            
            control.setDate(date);
            control.setProgress(date.getDate());
            control.setText(_("begin step " + date));
            
            // raz des metiers interdits et des licences
            metierMonitor.clear();
            
            if (date.getMonth().equals(Month.JANUARY)) {
                populationMonitor.clearCatch();
            }

            // only if there are one or more strategy
            if (siMatrix.getStrategies(date).size() > 0) {
                if (resManager.isEnabled(ResultName.MATRIX_NO_ACTIVITY)) {
                    MatrixND mat = metierMonitor.getOrCreateNoActivity(date,
                        ResultName.MATRIX_NO_ACTIVITY,
                        siMatrix.getStrategies(date),
                        siMatrix.getMetiers(date));                            
                    resManager.addResult(date, mat);
                }
            }
            
            //
            // Rule condition evaluation
            //
            control.setText("Evalute Rules conditions ("+rules.size()+" rules)");
            for (Rule rule : rules) {
                for (Metier metier : siMatrix.getMetiers(date)) {
                    boolean active = false;
                    try {
                        active = rule.condition(context, date, metier);
                    } catch (Exception eee) {
                        if (log.isWarnEnabled()) {
                            log.warn("Can't evaluate rule condition for: " + rule, eee);
                        }
                    }
                    ruleMonitor.setEvaluationCondition(date, rule, metier, active);
                    if (active) {
                        log.info("Activate rule: " + rule.getClass().getSimpleName());
                        resManager.addActiveRule(date, rule);
                    }
                }
            }
            
            //
            // Rule pre action
            //
            control.setText("Do pre action Rules");
            for (Rule rule : rules) {
                for (Metier metier : siMatrix.getMetiers(date)) {
                    boolean condition = ruleMonitor.getEvalutionCondition(date, rule, metier); 
                    if (condition) {
                        rule.preAction(context, date, metier);
                    }
                }
            }
            
            //
            // Keep modification's information done in rule
            //
            if (resManager.isEnabled(ResultName.MATRIX_METIER_ZONE)) {
                MatrixND metierZone = siMatrix.getMetierZone(date);
                resManager.addResult(date, metierZone);
            }
            
            //
            // Simulate one step for all pop
            // 
            control.setText("Simulate one month");
            for (Population pop : siMatrix.getPopulations(date)) {
                computeMonth(context, siMatrix, date, pop);
            }
            
            //
            // Add some result not population dependante
            //
            
            // only if there are one or more strategy
            if (siMatrix.getStrategies(date).size() > 0) {

                control.setText("Add some results");
                if (resManager.isEnabled(ResultName.MATRIX_EFFORT_PER_STRATEGY_MET)) {
                    MatrixND effortPerStrategyMet = siMatrix.matrixEffortPerStrategyMet(date); 
                    resManager.addResult(date, effortPerStrategyMet);
                }
                
                if (resManager.isEnabled(ResultName.MATRIX_STD_TRAVEL_EFFORT_PER_STRATEGY_MET)) {
                    MatrixND stdTravelEffortPerStrategyMet = siMatrix.matrixEffortPerStrategyMet(date); 
                    resManager.addResult(date, stdTravelEffortPerStrategyMet);
                }
                
                if (resManager.isEnabled(ResultName.MATRIX_EFFORT_PER_ZONE_POP)) {
                    MatrixND effortPerZonePop = siMatrix.matrixEffortPerStrategyMet(date); 
                    resManager.addResult(date, effortPerZonePop);
                }                    

            
                //
                // Add economics results
                //                        
                if (!"false".equalsIgnoreCase(param.getTagValue().get("ecoResult"))) {
                    control.setText("Add economics results");
                    saveGravityModel(date, resManager, gravityModel);
                }
            }
             
            //
            // Add economics results
            //                        
            if (resManager.isEnabled(ResultName.MATRIX_PRICE)) {
                for (Population pop : siMatrix.getPopulations(date)) {
                    MatrixND matPrice = siMatrix.matrixPrice(date, pop);
                    resManager.addResult(date, pop, matPrice);
                }
            }

            
            
            //
            // Rule post action
            //
            control.setText("Do post action Rules");
            for (Rule rule : rules) {
                for (Metier metier : siMatrix.getMetiers(date)) {
                    if (ruleMonitor.getEvalutionCondition(date, rule, metier)) {
                        rule.postAction(context, date, metier);
                    }
                }
            }

            // discard and landing must be done after post action rules
            // only if there are one or more strategy
            if (siMatrix.getStrategies(date).size() > 0) {

                control.setText("Compute discard and landing");
                for (Population pop : siMatrix.getPopulations(date)) {
                    //
                    // discard computation
                    //
                    MatrixND discard = populationMonitor.getDiscard(date, pop);
                    if (discard != null || date.getDate() == 0) { // force discard for the first month to have discard in result
                        if (discard == null) {
                            discard =  MatrixFactory.getInstance().create(ResultName.MATRIX_DISCARDS_PER_STR_MET,
                                    new List[]{siMatrix.getStrategies(date),
                                    siMatrix.getMetiers(date),
                                    pop.getPopulationGroup(),
                                    pop.getPopulationZone()},
                                    new String[]{n_("Strategies"), n_("Metiers"), n_("Groups"), n_("Zones")});                        
                        }
                        resManager.addResult(date, pop, discard);

                        if (resManager.isEnabled(ResultName.MATRIX_DISCARDS_WEIGHT_PER_STR_MET)) {
                            MatrixND discardWeightPerStrategyMet = siMatrix.matrixDiscardWeightPerStrategyMet(pop, date, discard); 
                            resManager.addResult(date, pop, discardWeightPerStrategyMet);
                        }

                    }

                    //
                    // add landing result
                    //
                    if (resManager.isEnabled(ResultName.MATRIX_LANDING_PER_MET)) {
                        MatrixND landing = MatrixFactory.getInstance().create(populationMonitor.getCatch(pop));
                        if (discard != null) {
                            landing = landing.minus(discard);
                        }
                        landing.setName(ResultName.MATRIX_LANDING_PER_MET);
                        resManager.addResult(date, pop, landing);
                    }
                }
            }

            //
            // revert modification for next step
            //
            control.setText("Rollback rules changes");
            db.rollbackTransaction();
            
            //
            // commit result
            //
            control.setText("Commit results");
            TopiaContext tx = context.getDbResult();
            tx.commitTransaction();
            
            //
            // Go next step
            //
            date = date.next();
        }
    }
    
    protected void computeMonth(SimulationContext context, SiMatrix siMatrix, Date date, Population pop) throws IsisFishException, TopiaException {
        // to add result
        ResultStorage resManager = context.getSimulationStorage().getResultStorage();

        PopulationMonitor popMon = context.getPopulationMonitor();
        MatrixND N = popMon.getN(pop);
        
        if (log.isInfoEnabled()) {
            log.info("====================== begin "+ date + " - " + pop +" =========================== " + System.currentTimeMillis());
            log.info("N: " + N);
        }
        
        // add N and biomass result now, before computation
        // N is reassigned during computation        
        resManager.addResult(date,  pop, N);

        if (resManager.isEnabled(ResultName.MATRIX_BIOMASS)) {
            MatrixND biomass = siMatrix.matrixBiomass(N, pop, date);
            resManager.addResult(date, pop, biomass);
        }
                
        Month month = date.getMonth();
        PopulationSeasonInfo info = pop.getPopulationSeasonInfo(month);
                
        // group change
        MatrixND CA = null;
        if (date.getDate() == 0) {
            CA = MatrixFactory.getInstance().matrixId(pop.sizePopulationGroup()*pop.sizePopulationZone());
        } else {
            CA = info.getGroupChangeMatrix(month);
        }

        log.debug("CA: " + CA);
        
        //migration
        MatrixND M = info.getMigrationMatrix(month, N);        
        log.debug("M: " + M);
        //emigration
        MatrixND EM = info.getEmigrationMatrix(month, N);        
        log.debug("EM: " + EM);
        //immigration
        MatrixND IM = info.getImmigrationMatrix(month, N).transpose();
        log.debug("IM: " + IM);

        // pour les premiers calculs on met N en une matrice 1D
        MatrixND N1D = pop.N2DToN1D(N);
        log.debug("N1D: " + N1D);

        MatrixND tmp0 = N1D.mult(CA);
        MatrixND tmp1 = M.minus(EM);
        MatrixND tmp2 = tmp0.mult(tmp1);
        MatrixND tmp3 = tmp2.add(IM);
        
        log.debug("N1D after mig: " + tmp3);
        
        // On reconvertie en une matrice Semantique
        N = pop.split2D(tmp3);

        log.debug("N after mig: " + N);

        //Recrutement
        MatrixND R = info.getReproductionMatrix(month, N);
        log.debug("R: " + R);
        
        // ajout de la matrice R dans le suivi de la pop comme etant
        // la reproduction pour le mois courant.
        popMon.setReproduction(date, pop, R);
        
        // recrutement
        MatrixND recruitment = popMon.getRecruitment(date, pop);
        log.debug("recruitment: " + recruitment);
                
        // mortalite de la reproduction
        popMon.applyReproductionMortality(pop);
        
        N = N.add(recruitment);
        log.debug("N after recru: " + N);

        MatrixND abundance = N;
        // compute fishing matrix only if there are one or more strategy
        if (siMatrix.getStrategies(date).size() > 0) {
            // compute some Matrix and add result
            MatrixND matrixFishingMortality = siMatrix.matrixFishingMortality(date, pop);
            resManager.addResult(date, pop, matrixFishingMortality);
            
            abundance = siMatrix.matrixAbundance(N, pop, date, matrixFishingMortality);

            // this matrix is necessary for matrixCatchPerStrategyMet
            MatrixND catchRatePerStrategyMet = siMatrix.matrixCatchRatePerStrategyMet(pop, date, matrixFishingMortality);
            resManager.addResult(date, pop, catchRatePerStrategyMet);

            // this matrix is necessary for PopulationMonitor.holdCatch (reused in rule)
            MatrixND catchPerStrategyMet = siMatrix.matrixCatchPerStrategyMet(N, pop, date, catchRatePerStrategyMet);
            popMon.holdCatch(pop, catchPerStrategyMet);
            resManager.addResult(date, pop, catchPerStrategyMet);
        
            if (resManager.isEnabled(ResultName.MATRIX_CATCH_WEIGHT_PER_STRATEGY_MET)) {
                MatrixND catchWeightPerStrategyMet = siMatrix.matrixCatchWeightPerStrategyMet(pop, date, catchPerStrategyMet); 
                resManager.addResult(date, pop, catchWeightPerStrategyMet);
            }
        }   
        log.debug("abundance: " + abundance);
        
        // Keep new N
        popMon.setN(pop, abundance);

        log.debug("====================== end " + date + " - " + pop +" ===========================");
   
    }
    
    /**
     * @param date
     * @param resManager
     * @throws IsisFishException 
     * @throws TopiaException 
     */
    private void saveGravityModel(Date date, ResultManager resManager, GravityModel gravityModel) throws IsisFishException, TopiaException {
        if (resManager.isEnabled(ResultName.MATRIX_FISHING_TIME_PER_MONTH_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixFishingTimePerMonthPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_FUEL_COSTS_OF_TRAVEL_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixFuelCostsOfTravelPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_COSTS_OF_FISHING_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixCostsOfFishingPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_FUEL_COSTS_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixFuelCostsOfTravelPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_REPAIR_AND_MAINTENANCE_GEAR_COSTS_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixRepairAndMaintenanceGearCostsPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_OTHER_RUNNING_COSTS_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixOtherRunningCostsPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_SHARED_NOT_FIXED_COSTS_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixSharedNotFixedCostsPerVessel(date); 
            resManager.addResult(date, mat);
        }      
//        if (resManager.isEnabled(ResultName.MATRIX_SHARED_FIXED_COSTS_PER_VESSEL_PER_MET)) {
//            MatrixND mat = gravityModel.matrixSharedFixedCostsPerVesselPerMet(date); 
//            resManager.addResult(date, mat);
//        }      
        if (resManager.isEnabled(ResultName.MATRIX_GROSS_VALUE_OF_LANDINGS_PER_SPECIES_PER_STRATEGY_MET)) {
            MatrixND mat = gravityModel.matrixGrossValueOfLandingsPerSpeciesPerStrategyMet(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_GROSS_VALUE_OF_LANDINGS_PER_STRATEGY_MET)) {
            MatrixND mat = gravityModel.matrixGrossValueOfLandingsPerStrategyMet(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_GROSS_VALUE_OF_LANDINGS_PER_STRATEGY_MET_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixGrossValueOfLandingsPerStrategyMetPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_NET_VALUE_OF_LANDINGS_PER_STRATEGY_MET)) {
            MatrixND mat = gravityModel.matrixNetValueOfLandingsPerStrategyMet(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_NET_VALUE_OF_LANDINGS_PER_STRATEGY_MET_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixNetValueOfLandingsPerStrategyMetPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_NET_RENEVUE_TO_SHARE_PER_STRATEGY_MET_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixNetRenevueToSharePerStrategyMetPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_CREW_SHARE_PER_STRATEGY_MET_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixCrewSharePerStrategyMetPerVessel(date); 
            resManager.addResult(date, mat);
        }
        if (resManager.isEnabled(ResultName.MATRIX_OWNER_MARGIN_OVER_VARIABLE_COSTS_PER_STRATEGY_MET_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixOwnerMarginOverVariableCostsPerStrategyMetPerVessel(date);                                                                                                            
            resManager.addResult(date, mat);
        }
        if (resManager.isEnabled(ResultName.MATRIX_VESSEL_MARGIN_OVER_VARIABLE_COSTS_PER_STRATEGY_MET_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixVesselMarginOverVariableCostsPerStrategyMetPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_OWNER_MARGIN_OVER_VARIABLE_COSTS_PER_STRATEGY_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixOwnerMarginOverVariableCostsPerStrategyPerVessel(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_OWNER_MARGIN_OVER_VARIABLE_COSTS_PER_STRATEGY)) {
            MatrixND mat = gravityModel.matrixOwnerMarginOverVariableCostsPerStrategy(date); 
            resManager.addResult(date, mat);
        }      
        if (resManager.isEnabled(ResultName.MATRIX_VESSEL_MARGIN_OVER_VARIABLE_COSTS_PER_STRATEGY_PER_VESSEL)) {
            MatrixND mat = gravityModel.matrixVesselMarginOverVariableCostsPerStrategyPerVessel(date);                                                                                                          
            resManager.addResult(date, mat);        
        }
        if (resManager.isEnabled(ResultName.MATRIX_VESSEL_MARGIN_OVER_VARIABLE_COSTS_PER_STRATEGY)) {
            MatrixND mat = gravityModel.matrixVesselMarginOverVariableCostsPerStrategy(date); 
            resManager.addResult(date, mat);
        }
    }

}


