#region <<< Using Directives >>>
using System;

using Volpe.Cafe.Generic;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.ModelSettings;

using RC = Volpe.Cafe.RegulatoryClass;
#endregion

namespace Volpe.Cafe.Model
{
    static class CreditTradingCO2
    {

        //--------------------------------------------------
        // Setup transfer categories sources
        //--------------------------------------------------
        static RCAltValue<RC[]> TransferCategorySource = new RCAltValue<RC[]>
        {
            [RC.AllPassengerCar] = new RC[] { RC.LightTruck },
            [RC.LightTruck] = new RC[] { RC.AllPassengerCar },
            [RC.HDPUV] = Array.Empty<RC>()
        };

        /// <summary>
        /// Examines credits for transfers and carry forward.
        /// Determines whether credits can be and need to be transferred into a compliance category defined by
        /// the specified model year and manufacturer.
        /// If the destination compliance category is at a deficit, a credit carry forward action, followed by
        /// a credit transfer action is performed.
        /// </summary>
        public static void ExamineCredits(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr, ICompliance complianceModel)
        {
            CT_CarryForward  (scen, iteration, year, mfr, complianceModel, false, 0); // transfer credits from carry-forward years
            CT_FleetTransfers(scen, iteration, year, mfr, complianceModel, false, 0); // transfer credits between reg-classes
        }

        //----- Credit carry-forward -----
        /// <summary>
        /// Performs a credit carry forward operation into a compliance category defined by the specified model
        /// year and manufacturer for all regulatory classes.
        /// A carry forward operation will only be performed if credit trading settings allow and the destination
        /// compliance category is at a deficit.
        /// </summary>
        public static void CT_CarryForward(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr,
            ICompliance complianceModel, bool expiringOnly, int expiringYrIndex)
        {
            foreach (RC regClass in RegClasses.AltList) {
                CT_CarryForward(scen, iteration, year, mfr, regClass, complianceModel, expiringOnly, expiringYrIndex);
            }
        }
        /// <summary>
        /// Performs a credit carry forward operation into a compliance category defined by the specified model
        /// year, manufacturer, and regulatory class.
        /// A carry forward operation will only be performed if credit trading settings allow and the destination
        /// compliance category is at a deficit.
        /// </summary>
        public static void CT_CarryForward(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr, RC rc,
            ICompliance complianceModel, bool expiringOnly, int expiringYrIndex)
        {
            ModelingSettings settings = complianceModel.Settings;
            RuntimeSettings runtimeSettings = settings.RuntimeSettings;
            CreditTradingValues ct = settings.Parameters.CreditTradingValues;
            if (!runtimeSettings.AllowCreditTrading || !ct.AllowCarryForward) {
                return;
            }

            double credits = mfr.GetNetCO2Credits(rc);
            if (credits >= 0) { 
                return;
            }

            // scan each year to see if credits can be carried forward
            int expiringYear = expiringYrIndex + ModelYear.MinYear;
            int minComplianceYear = runtimeSettings.AnalysisStartYear;
            int minBankedCredYear = mfr.BaselineCreditBank.MinYear;
            int minYear = Math.Min(minComplianceYear, minBankedCredYear);
            //
            for (int pYear = minYear; pYear < year.Year; pYear++) {
                // if only expiring credits should be used, skip all other years
                if (expiringOnly && (pYear > expiringYear - ct.CarryForwardYears)) {
                    continue;
                }

                // if credits already expired, skip year
                if (pYear + ct.CarryForwardYears < year.Year) {
                    continue;
                }

                IManufacturerRevision pMfrRev;
                double eCredits;
                bool useBank = false;
                if (pYear >= minComplianceYear)
                {   // checking one of analysis years -- check for credits generated during compliance
                    pMfrRev = mfr.GetRevision(pYear);
                    // **note: when computing available credits from previous years, CO2CreditsIn
                    //         should not be considered, since there is no way of tracking where
                    //         they came from or their expiration
                    var pStats = pMfrRev.ComplianceStats;
                    eCredits = Math.Max(0, pStats.CO2Credits[rc] - pStats.CO2CreditsOut[rc]);
                }
                else
                {   // checking a year prior to start of modeling -- check for banked credits
                    pMfrRev = mfr.RevisionInfo;
                    eCredits = mfr.CO2Bank[pYear][rc];
                    useBank = true;
                }

                // continue to the next year if did not earn any credits in the "i-th" year
                if (eCredits <= 0) {
                    continue;
                }

                // perform a credit transfer
                DoTransfer(complianceModel, scen, iteration, mfr,
                           eCredits, -credits, rc, pYear, pMfrRev, useBank, rc, year, mfr.RevisionInfo);

                // update credits; if compliance achieved, return
                credits = mfr.GetNetCO2Credits(rc);
                if (credits >= 0) {
                    return;
                }
            } // next i (carry forward credit year)
        }

        //----- Credit carry-forward -----
        /// <summary>
        /// Performs a credit transfer operation into a compliance category defined by the specified model
        /// year and manufacturer for all regulatory classes.
        /// A transfer operation will only be performed if credit trading settings allow and the destination
        /// compliance category is at a deficit.
        /// </summary>
        public static void CT_FleetTransfers(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr,
            ICompliance complianceModel, bool expiringOnly, int expiringYrIndex)
        {
            foreach (RC regClass in RegClasses.AltList) {
                CT_FleetTransfers(scen, iteration, year, mfr, regClass, complianceModel, expiringOnly, expiringYrIndex);
            }
        }
        /// <summary>
        /// Performs a credit transfer operation into a compliance category defined by the specified model
        /// year, manufacturer, and regulatory class.
        /// A transfer operation will only be performed if credit trading settings allow and the destination
        /// compliance category is at a deficit.
        /// </summary>
        public static void CT_FleetTransfers(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr, RC rc,
            ICompliance complianceModel, bool expiringOnly, int expiringYrIndex, bool allowCarryForward = true)
        {
            ModelingSettings settings = complianceModel.Settings;
            RuntimeSettings runtimeSettings = settings.RuntimeSettings;
            CreditTradingValues ct = settings.Parameters.CreditTradingValues;
            if (!runtimeSettings.AllowCreditTrading || !ct.AllowFleetTransfers) {
                return;
            }

            double credits = mfr.GetNetCO2Credits(rc);
            if (credits >= 0) {
                return;
            }

            // obtain maximum credits that can be transferred into this reg-class, taking the cap into account
            double rCredits = -credits;

            //------------------
            // Transfer Credits
            //
            RC[] eRCArr = TransferCategorySource[rc];
            if (eRCArr.Length == 0) {
                return;
            }

            // first, transfer credits from other reg-class, from the previous years (if settings allow)
            // but, skip the eRC if during the analysis year eRC has no sales or has a negative balance of credits
            if (ct.AllowCarryForward && allowCarryForward) {
                foreach (RC eRC in eRCArr) {
                    if (mfr.ComplianceStats.VehicleStats[eRC].Sales <= 0 || mfr.GetNetCO2Credits(eRC) < 0) {
                        continue;
                    }

                    // scan each year
                    int expiringYear = expiringYrIndex + ModelYear.MinYear;
                    int minComplianceYear = runtimeSettings.AnalysisStartYear;
                    int minBankedCredYear = mfr.BaselineCreditBank.MinYear;
                    int minYear = Math.Min(minComplianceYear, minBankedCredYear);
                    //
                    for (int pYear = minYear; pYear < year.Year; pYear++) {
                        // if only expiring credits should be used, skip all other years
                        if (expiringOnly && (pYear > expiringYear - ct.CarryForwardYears)) {
                            continue;
                        }

                        // if credits already expired, skip year
                        if (pYear + ct.CarryForwardYears < year.Year) {
                            continue;
                        }

                        IManufacturerRevision pMfrRev;
                        double eCredits;
                        bool useBank = false;
                        if (pYear >= minComplianceYear)
                        {   // checking one of analysis years -- check for credits generated during compliance
                            pMfrRev = mfr.GetRevision(pYear);
                            var pStats = pMfrRev.ComplianceStats;
                            eCredits = Math.Max(0, pStats.CO2Credits[eRC] - pStats.CO2CreditsOut[eRC]); // see note in "CT_CarryForward(...)"
                        }
                        else
                        {   // checking a year prior to start of modeling -- check for banked credits
                            pMfrRev = mfr.RevisionInfo;
                            eCredits = mfr.CO2Bank[pYear][eRC];
                            useBank = true;
                        }

                        // continue to the next year if did not earn any credits in the "i-th" year
                        if (eCredits <= 0) {
                            continue;
                        }

                        // determine amount of credits that can be transferred
                        RequestCredits(eCredits, rCredits, out double outECredits, out double outRCredits);

                        // carry forward credits within the reg-class
                        var mfrRev = mfr.RevisionInfo;
                        DoTransfer(complianceModel, scen, iteration, mfr, outECredits, double.PositiveInfinity,
                                   eRC, pYear, pMfrRev, useBank, eRC, year, mfrRev);
                        // transfer credits between reg-classes
                        eCredits = mfr.GetNetCO2Credits(eRC);
                        DoTransfer(complianceModel, scen, iteration, mfr, eCredits, outRCredits,
                                   eRC, year, mfrRev, false, rc, year, mfrRev);

                        // update credits; if compliance achieved, return
                        credits = mfr.GetNetCO2Credits(rc);
                        if (credits >= 0) {
                            return;
                        }
                        rCredits = -credits; // update maximum credits that can be transferred
                    }
                } // next eRC
            } // end-if (allow_carry-fwd)

            // second, exhaust credits from other reg-classes for the current year
            // do not transfer if only expiring credits should be used
            if (expiringOnly) {
                return;
            }

            // examine credits ...
            foreach (RC eRC in eRCArr) {
                double eCredits = mfr.GetNetCO2Credits(eRC);
                if (eCredits <= 0) {
                    continue;
                }

                // perform a credit transfer
                var mfrRev = mfr.RevisionInfo;
                DoTransfer(complianceModel, scen, iteration, mfr,
                           eCredits, rCredits, eRC, year, mfrRev, false, rc, year, mfrRev);

                // update credits; if compliance achieved, return
                credits = mfr.GetNetCO2Credits(rc);
                if (credits >= 0) {
                    return;
                }
                rCredits = -credits; // update maximum credits that can be transferred
            }
        }

        //----- Helper methods for actual credit transfer -----
        static void DoTransfer(ICompliance complianceModel, Scenario scen, int iteration, IManufacturer mfr,
            double eCredits, double rCredits,
            RC eRC, int eYear, IManufacturerRevision eMfr, bool eBank, // params from the "earned" compliance category
            RC uRC, int uYear, IManufacturerRevision uMfr)             // params from the "used" compliance category
        {
            //
            // eCredits:  available credits from another compliance category (such reg-class and/or model year);
            //            if this value is infinity, maximum "out" earned credits will be returned
            // rCredits:  requested credits that can be transferred into the current compliance category (reg-class)
            //            if this value is infinity, maximum "out" required credits will be returned
            //
            // eCredits & rCredits may not both be infinity
            //
            // eBank:     if set to true, using banked credits generated prior to start of analysis
            //
            RequestCredits(eCredits, rCredits, out double outECredits, out double outRCredits);

            // adjust credits in / credits out
            if (eBank) {
                mfr.CO2Bank[eYear][eRC] -= outECredits;
            }
            else {
                eMfr.ComplianceStats.CO2CreditsOut[eRC] += outECredits;
            }
            uMfr.ComplianceStats.CO2CreditsIn[uRC] += outRCredits;

            //>>>>> debug
            var log = complianceModel.LogWriter.GetCreditTradingLog(scen.Index);
            log?.WriteValues(
                scen.Index, iteration, mfr.Name, ComplianceProgram.CO2,
                eCredits, rCredits, eRC, eYear, outECredits, uRC, uYear, outRCredits);
            //<<<<< debug
        }

        static void RequestCredits(double eCredits, double rCredits, out double outECredits, out double outRCredits)
        {
            //
            // This function computes the maximum amount of credits that can be transferred, given the available
            // eCredits and the requested rCredits.  The resultant out params (outECredits and outRCredits) will
            // not exceed the maximum of eCredits and rCredits.
            //
            // eCredits:  available credits from another compliance category (such reg-class and/or model year);
            //            if this value is infinity, maximum "out" earned credits will be returned
            // rCredits:  requested credits that can be transferred into the current compliance category (reg-class)
            //            if this value is infinity, maximum "out" required credits will be returned
            //
            // eCredits & rCredits may not both be infinity
            //

            if (double.IsInfinity(rCredits) && double.IsInfinity(eCredits))
            {
                outECredits = outRCredits = 0;
                return;
            }

            // determine max out-credits that can be obtained from available eCredits
            if (!double.IsInfinity(eCredits))
            {
                outRCredits = eCredits;
                if (double.IsInfinity(rCredits) || outRCredits <= rCredits)
                {   // if outRCredits <= rCredits, that means that avail eCredits is not too much
                    outECredits = outRCredits;
                    return;
                }
            }

            // this step is reached if avail eCredits produced too much transferable credits ... in this case, determine
            // amount of eCredits necessary using requested rCredits
            outECredits = rCredits;
            outRCredits = outECredits;
        }

    }
}
