/*
 * #%L
 * IsisFish data
 * %%
 * Copyright (C) 2006 - 2015 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.StorageException;
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 resultinfos.*;

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

    /**
     * When you create new simulator with specific SiMatrix you must
     * overwrite this method to return you SiMatrix version
     * @param context
     * @return
     * @throws TopiaException 
     */
    protected SiMatrix newSiMatrix(SimulationContext context) throws TopiaException {
        return new SiMatrix(context);
    }

    /**
     * When you create new simulator with specific GravityModel you must
     * overwrite this method to return you GravityModel version
     * @param context
     * @param siMatrix
     * @return
     * @throws TopiaException 
     * @throws StorageException 
     */
    protected GravityModel newGravityModel(SimulationContext context, SiMatrix siMatrix) throws TopiaException, StorageException {
        return new GravityModel(context, siMatrix);
    }

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

        SimulationParameter param = context.getSimulationStorage()
                .getParameter();
        SimulationControl control = context.getSimulationControl();
        int lastStep = param.getNumberOfMonths();
        TimeStep step = control.getStep();

        ResultManager resManager = context.getResultManager();
        TopiaContext db = context.getDB(); // for database init

        SiMatrix siMatrix = newSiMatrix(context);
        GravityModel gravityModel = newGravityModel(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(MatrixAbundance.NAME);
            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.validateDBChanges();

        //
        // 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(MatrixNoActivity.NAME)) {
                    MatrixND mat = metierMonitor.getOrCreateNoActivity(step,
                            MatrixNoActivity.NAME, 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(MatrixMetierZone.NAME)) {
                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(MatrixEffortPerStrategyMet.NAME)) {
                    MatrixND effortPerStrategyMet = siMatrix
                            .matrixEffortPerStrategyMet(step);
                    resManager.addResult(step, effortPerStrategyMet);
                }

                if (resManager.isEnabled(MatrixEffortNominalPerStrategyMet.NAME)) {
                    MatrixND effortNominalPerStrategyMet = siMatrix.matrixEffortNominalPerStrategyMet(step);
                    resManager.addResult(step, effortNominalPerStrategyMet);
                }
                
            }


            //
            // 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(
                                    MatrixDiscardsPerStrMetPerZonePop.NAME,
                                    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(MatrixDiscardsWeightPerStrMetPerZonePop.NAME)) {
                            MatrixND discardWeightPerStrategyMet = siMatrix
                                    .matrixDiscardWeightPerStrategyMetPerZonePop(
                                            pop, step, discard);
                            resManager.addResult(step, pop,
                                                 discardWeightPerStrategyMet);
                        }

                    }

                    //
                    // add landing result
                    //
                    if (resManager.isEnabled(MatrixLandingPerMet.NAME)) {
                        MatrixND landing = populationMonitor.getCatch(pop).copy();
                        landing.setName(MatrixLandingPerMet.NAME);
                        if (discard != null) {
                            landing = landing.minus(discard);
                        }
                        resManager.addResult(step, pop, landing);
                    }

                    if (resManager.isEnabled(MatrixLandingWeight.NAME)) {
                        MatrixND landing = populationMonitor.getCatch(pop).copy();
                        if (discard != null) {
                            landing = landing.minus(discard);
                        }
                        MatrixND landingweight = siMatrix.matrixToWeightMatrix(step, 2,
                                MatrixLandingWeight.NAME, landing);

                        resManager.addResult(step, pop, landingweight);
                    }

                    // F and Fbar
                    if (resManager.isEnabled(MatrixFishingMortalityPerGroup.NAME)) {
                        MatrixND fishingMortalityPerGroup = siMatrix.fishingMortalityPerGroup(step, pop, context.getResultManager());
                        resManager.addResult(step, pop, fishingMortalityPerGroup);

                        // MatrixTotalFishingMortality.NAME depends on MatrixFishingMortalityPerGroup.NAME
                        if (resManager.isEnabled(MatrixTotalFishingMortality.NAME)) {
                            MatrixND totalFishingMortality = siMatrix.totalFishingMortality(
                                    step, pop, fishingMortalityPerGroup);
                            resManager.addResult(step, pop, totalFishingMortality);
                        }
                    }
                }

                //
                // Add economics results
                // 
                if (!"false".equalsIgnoreCase(param.getTagValue().get(
                        "ecoResult"))) {
                    control.setText("Add economics results");
                    saveGravityModel(step, resManager, gravityModel);
                }

                if (resManager.isEnabled(MatrixPrice.NAME)) {
                    for (Population pop : siMatrix.getPopulations(step)) {
                        MatrixND matPrice = siMatrix.matrixPrice(step, pop);
                        resManager.addResult(step, pop, matPrice);
                    }
                }

            }

            //
            // Go next step
            //
            // do next before rollbackTransaction to permit export to take
            // current database value and export them.
            step = step.next();

            //
            // revert modification for next step
            //
            control.setText("Rollback rules changes");
            context.rollbackRuleChanges();
        }

    }

    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
        ResultManager resManager = context.getResultManager();

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

        MatrixND biomass = siMatrix.matrixBiomass(N, pop, step);
        if (resManager.isEnabled(MatrixBiomass.NAME)) {
            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, N, biomass, 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(MatrixAbundanceBeginMonth.NAME)) {
            MatrixND abondanceBM = siMatrix.matrixAbondanceBeginMonth(N, pop,
                                                                      step);
            resManager.addResult(step, pop, abondanceBM);
        }

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

        if (resManager.isEnabled(MatrixLbar.NAME)) {
            MatrixND Lbar = siMatrix.Lbar(step, pop, N);
            resManager.addResult(step, pop, Lbar);
        }

        if (resManager.isEnabled(MatrixLbarTotal.NAME)) {
            MatrixND Lbar = siMatrix.LbarTotal(step, pop, N);
            resManager.addResult(step, pop, Lbar);
        }

        if (resManager.isEnabled(MatrixRecruitment.NAME)) {
            MatrixND Recruitment = siMatrix.Recruitment(step, pop);
            resManager.addResult(step, pop, Recruitment);
        }

        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;

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

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

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

            siMatrix.computeMonthExtra(step, pop, N);

        } 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
     */
    protected void saveGravityModel(TimeStep step, ResultManager resManager,
                                  GravityModel gravityModel) throws IsisFishException, TopiaException {
        if (resManager
                .isEnabled(MatrixFishingTimePerMonthPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixFishingTimePerMonthPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixFuelCostsOfTravelPerVessel.NAME)) {
            MatrixND mat = gravityModel.matrixFuelCostsOfTravelPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager.isEnabled(MatrixCostsOfFishingPerVessel.NAME)) {
            MatrixND mat = gravityModel.matrixCostsOfFishingPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager.isEnabled(MatrixFuelCostsPerVessel.NAME)) {
            MatrixND mat = gravityModel.matrixFuelCostsOfTravelPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixRepairAndMaintenanceGearCostsPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixRepairAndMaintenanceGearCostsPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixOtherRunningCostsPerVessel.NAME)) {
            MatrixND mat = gravityModel.matrixOtherRunningCostsPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixSharedNotFixedCostsPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixSharedNotFixedCostsPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixGrossValueOfLandingsPerSpeciesPerStrategyMet.NAME)) {
            MatrixND mat = gravityModel
                    .matrixGrossValueOfLandingsPerSpeciesPerStrategyMet(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixGrossValueOfLandingsPerStrategyMet.NAME)) {
            MatrixND mat = gravityModel
                    .matrixGrossValueOfLandingsPerStrategyMet(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixGrossValueOfLandingsPerStrategyMetPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixGrossValueOfLandingsPerStrategyMetPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixNetValueOfLandingsPerStrategyMet.NAME)) {
            MatrixND mat = gravityModel
                    .matrixNetValueOfLandingsPerStrategyMet(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixNetValueOfLandingsPerStrategyMetPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixNetValueOfLandingsPerStrategyMetPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixNetRevenueToSharePerStrategyMetPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixNetRenevueToSharePerStrategyMetPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixCrewSharePerStrategyPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixCrewSharePerStrategyMetPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixOwnerMarginOverVariableCostsPerStrategyMetPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixOwnerMarginOverVariableCostsPerStrategyMetPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixVesselMarginOverVariableCostsPerStrategyMetPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixVesselMarginOverVariableCostsPerStrategyMetPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixOwnerMarginOverVariableCostsPerStrategyPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixOwnerMarginOverVariableCostsPerStrategyPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixOwnerMarginOverVariableCostsPerStrategy.NAME)) {
            MatrixND mat = gravityModel
                    .matrixOwnerMarginOverVariableCostsPerStrategy(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixVesselMarginOverVariableCostsPerStrategyPerVessel.NAME)) {
            MatrixND mat = gravityModel
                    .matrixVesselMarginOverVariableCostsPerStrategyPerVessel(step);
            resManager.addResult(step, mat);
        }
        if (resManager
                .isEnabled(MatrixVesselMarginOverVariableCostsPerStrategy.NAME)) {
            MatrixND mat = gravityModel
                    .matrixVesselMarginOverVariableCostsPerStrategy(step);
            resManager.addResult(step, mat);
        }
    }

}
