/*
 * #%L
 * IsisFish data
 * %%
 * Copyright (C) 2006 - 2014 Ifremer, Code Lutin, Cedric Pineau, Benjamin Poussin, Chatellier Eric
 * %%
 * 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>.
 * #L%
 */
package simulators;

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.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.Month;
import fr.ifremer.isisfish.types.TimeStep;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuiton.math.matrix.MatrixFactory;
import org.nuiton.math.matrix.MatrixND;
import org.nuiton.topia.TopiaContext;
import org.nuiton.topia.TopiaException;
import scripts.GravityModel;
import scripts.ResultName;
import scripts.SiMatrix;

import java.util.List;

import static org.nuiton.i18n.I18n.t;
import static org.nuiton.i18n.I18n.n;

/**
 * DefaultSimulator.java
 *
 * Created: 21 aout 2006 10:57:46
 *
 * @author poussin
 * @version $Revision: 1.19 $
 *
 * Last update: $Date: 2007-11-06 18:16:14 $
 * by : $Author: bpoussin $
 */
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 lastStep = lastYear * Month.NUMBER_OF_MONTH;
        TimeStep step = control.getStep();

        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();

        List<Population> allpops = siMatrix.getPopulations(step);
        populationMonitor.init(allpops);

        for (Population pop : allpops) {
            MatrixND N = param.getNumberOf(pop);
            N.setName(ResultName.MATRIX_ABUNDANCE);
            populationMonitor.setN(pop, N);
        }

        // reload context parameters with context.getDB() context
        param.reloadContextParameters();
        
        //
        // 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 (step.getStep() < lastStep) {
            
            //
            // if user stop simulation before last year
            //
            if (control.isStopSimulationRequest()) {
                break;
            }

            control.setStep(step);
            control.setProgress(step.getStep());
            control.setText(t("begin step " + step));

            //
            // Reload parameters for current step
            //
            control.setText(t("Reloading parameters"));
            // reload context parameters with db.rollbackTransaction() context
            param.reloadContextParameters();
            rules = param.getRules();

            // raz des metiers interdits et des licences
            metierMonitor.clear();

            if (step.getMonth().equals(Month.JANUARY)) {
                populationMonitor.clearCatch();
            }

            // only if there are one or more strategy
            if (siMatrix.getStrategies(step).size() > 0) {
                if (resManager.isEnabled(ResultName.MATRIX_NO_ACTIVITY)) {
                    MatrixND mat = metierMonitor.getOrCreateNoActivity(step,
                            ResultName.MATRIX_NO_ACTIVITY, siMatrix
                                    .getStrategies(step), siMatrix
                                    .getMetiers(step));
                    resManager.addResult(step, mat);
                }
            }

            //
            // Rule condition evaluation
            //
            control.setText("Evaluate rules conditions (" + rules.size()
                    + " rules)");
            for (Rule rule : rules) {
                for (Metier metier : siMatrix.getMetiers(step)) {
                    boolean active = false;
                    try {
                        active = rule.condition(context, step, metier);
                    } catch (Exception eee) {
                        if (log.isWarnEnabled()) {
                            log.warn("Can't evaluate rule condition for: "
                                    + rule, eee);
                        }
                    }
                    ruleMonitor.setEvaluationCondition(step, rule, metier,
                            active);
                    if (active) {
                        log.info("Activate rule: "
                                + rule.getClass().getSimpleName());
                        resManager.addActiveRule(step, rule);
                    }
                }
            }

            //
            // Rule pre action
            //
            control.setText("Do pre action Rules");
            for (Rule rule : rules) {
                for (Metier metier : siMatrix.getMetiers(step)) {
                    boolean condition = ruleMonitor.getEvalutionCondition(step,
                            rule, metier);
                    if (condition) {
                        rule.preAction(context, step, metier);
                    }
                }
            }

            //
            // Keep modification's information done in rule
            //
            if (resManager.isEnabled(ResultName.MATRIX_METIER_ZONE)) {
                MatrixND metierZone = siMatrix.getMetierZone(step);
                resManager.addResult(step, metierZone);
            }

            //
            // Simulate one step for all pop
            // 
            control.setText("Simulate one month");
            for (Population pop : siMatrix.getPopulations(step)) {
                computeMonth(context, siMatrix, step, pop);
            }
                
            //
            // Add some result not population dependante
            //


            // only if there are one or more strategy
            if (siMatrix.getStrategies(step).size() > 0) {

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

                if (resManager.isEnabled(ResultName.MATRIX_EFFORT_NOMINAL_PER_STRATEGY_MET)) {
                    MatrixND effortNominalPerStrategyMet = siMatrix.matrixEffortNominalPerStrategyMet(step); 
                    resManager.addResult(step, effortNominalPerStrategyMet);
                }
                
                //
                // Add economics results
                //                        
                if (!"false".equalsIgnoreCase(param.getTagValue().get(
                        "ecoResult"))) {
                    control.setText("Add economics results");
                    saveGravityModel(step, resManager, gravityModel);
                }
            }

            //
            // Add economics results
            //                        
            if (resManager.isEnabled(ResultName.MATRIX_PRICE)) {
                for (Population pop : siMatrix.getPopulations(step)) {
                    MatrixND matPrice = siMatrix.matrixPrice(step, pop);
                    resManager.addResult(step, pop, matPrice);
                }
            }

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

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

                control.setText("Compute discard and landing");
                for (Population pop : siMatrix.getPopulations(step)) {
                    //
                    // discard computation
                    //
                    MatrixND discard = populationMonitor.getDiscard(step, pop);
                    if (discard != null || step.getStep() == 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_PER_ZONE_POP,
                                    new List[] { siMatrix.getStrategies(step),
                                            siMatrix.getMetiers(step),
                                            pop.getPopulationGroup(),
                                            pop.getPopulationZone() },
                                    new String[] { n("Strategies"),
                                            n("Metiers"), n("Groups"),
                                            n("Zones") });
                        }
                        resManager.addResult(step, pop, discard);

                        if (resManager
                                .isEnabled(ResultName.MATRIX_DISCARDS_WEIGHT_PER_STR_MET_PER_ZONE_POP)) {
                            MatrixND discardWeightPerStrategyMet = siMatrix
                                    .matrixDiscardWeightPerStrategyMetPerZonePop(
                                            pop, step, discard);
                            resManager.addResult(step, pop,
                                    discardWeightPerStrategyMet);
                        }

                    }

                    //
                    // add landing result
                    //
                    if (resManager.isEnabled(ResultName.MATRIX_LANDING_PER_MET)) {
                        MatrixND landing = populationMonitor.getCatch(pop).copy();
                        landing.setName(ResultName.MATRIX_LANDING_PER_MET);
                        if (discard != null) {
                            landing = landing.minus(discard);
                        }
                        resManager.addResult(step, 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
            //
            step = step.next();
        }

    }

    protected boolean isEffortByCell(SimulationContext context) {
        boolean result = "true".equalsIgnoreCase(context.getSimulationStorage()
                .getParameter().getTagValue().get("effortByCell"));
        return result;
    }

    protected void computeMonth(SimulationContext context, SiMatrix siMatrix,
            TimeStep step, 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 " + step + " - " + pop
                    + " =========================== "
                    + System.currentTimeMillis());
            log.info("N: " + N);
        }

        // add N and biomass result now, before computation
        // N is reassigned during computation        
        resManager.addResult(step, pop, N);

        if (resManager.isEnabled(ResultName.MATRIX_BIOMASS)) {
            MatrixND biomass = siMatrix.matrixBiomass(N, pop, step);
            resManager.addResult(step, pop, biomass);
        }

        Month month = step.getMonth();
        PopulationSeasonInfo info = pop.getPopulationSeasonInfo(month);

        // group change
        MatrixND CA;
        if (step.getStep() == 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);

        // reproduction
        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(step, pop, R);

        // recrutement
        MatrixND recruitment = popMon.getRecruitment(step, pop);
        log.debug("recruitment: " + recruitment);

        // mortalite de la reproduction
        popMon.applyReproductionMortality(pop);

        N = N.add(recruitment);
        log.debug("N after recru: " + N);

        if (resManager.isEnabled(ResultName.MATRIX_ABUNDANCE_BEGIN_MONTH)) {
            MatrixND abondanceBM = siMatrix.matrixAbondanceBeginMonth(N, pop,
                    step);
            resManager.addResult(step, pop, abondanceBM);
        }

        if (resManager.isEnabled(ResultName.MATRIX_BIOMASS_BEGIN_MONTH)) {
            MatrixND biomassBM = siMatrix.matrixBiomassBeginMonth(N, pop, step);
            resManager.addResult(step, pop, biomassBM);
        }

        MatrixND abundance;
        // compute fishing matrix only if there are one or more strategy
        if (siMatrix.getStrategies(step).size() > 0) {
            // compute some Matrix and add result

            // this matrix is necessary for PopulationMonitor.holdCatch (reused in rule)
            MatrixND catchPerStrategyMetPerZonePop;

            if (isEffortByCell(context)) {
                abundance = siMatrix.matrixAbundance(N, pop, step);
                catchPerStrategyMetPerZonePop = siMatrix
                        .matrixCatchPerStrategyMetPerZonePop(N, pop, step);
            } else { // en zone
                MatrixND matrixFishingMortality = siMatrix
                        .matrixFishingMortality(step, pop);
                resManager.addResult(step, pop, matrixFishingMortality);

                abundance = siMatrix.matrixAbundance(N, pop, step,
                        matrixFishingMortality);

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

                catchPerStrategyMetPerZonePop = siMatrix
                        .matrixCatchPerStrategyMetPerZone(N, pop, step,
                                catchRatePerStrategyMet);

            }

            popMon.holdCatch(pop, catchPerStrategyMetPerZonePop);
            resManager.addResult(step, pop, catchPerStrategyMetPerZonePop);

            if (resManager
                    .isEnabled(ResultName.MATRIX_CATCH_WEIGHT_PER_STRATEGY_MET_PER_ZONE_POP)) {
                MatrixND catchWeightPerStrategyMet = siMatrix
                        .matrixCatchWeightPerStrategyMetPerZonePop(step, pop,
                                catchPerStrategyMetPerZonePop);
                resManager.addResult(step, pop, catchWeightPerStrategyMet);
                  }

            if (isEffortByCell(context)) {
                MatrixND catchPerStrategyMetPerZoneMet = siMatrix
                        .matrixCatchPerStrategyMetPerZoneMet(N, pop, step);
                resManager.addResult(step, pop, catchPerStrategyMetPerZoneMet);

                if (resManager
                        .isEnabled(ResultName.MATRIX_CATCH_WEIGHT_PER_STRATEGY_MET_PER_ZONE_MET)) {
                    MatrixND catchWeightPerStrategyMet = siMatrix
                            .matrixCatchWeightPerStrategyMetPerZoneMet(step,
                                    pop, catchPerStrategyMetPerZoneMet);
                    resManager.addResult(step, pop, catchWeightPerStrategyMet);
                }
            }

            if (resManager.isEnabled(ResultName.MATRIX_FISHING_MORTALITY_PER_GROUP) ||
                    resManager.isEnabled(ResultName.MATRIX_TOTAL_FISHING_MORTALITY)) {
                MatrixND fishingMortalityPerGroup = siMatrix.fishingMortalityPerGroup(step,
                        pop, context.getResultManager());

                if (resManager.isEnabled(ResultName.MATRIX_FISHING_MORTALITY_PER_GROUP)) {
                    resManager.addResult(step, pop, fishingMortalityPerGroup);
                }
            
                if (resManager.isEnabled(ResultName.MATRIX_TOTAL_FISHING_MORTALITY)) {
                    MatrixND totalFishingMortality = siMatrix.totalFishingMortality(step, pop, fishingMortalityPerGroup);
                    resManager.addResult(step, pop, totalFishingMortality);
                }
            }

        } else { // no strategies
            // compute only if fishing mortality =0 to apply Natural Mortality
            abundance = siMatrix.matrixAbundanceSsF(N, pop, step);
        }
        log.debug("abundance: " + abundance);

        // Keep new N
        popMon.setN(pop, abundance);

        log.debug("====================== end " + step + " - " + pop
                + " ===========================");

    }

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

}
