#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 CreditTrading
    {

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

        //--------------------------------------------------
        // Methods for carry-fwd/transfers
        //--------------------------------------------------
       
        //
        // Examines credits for trading stages 1 and 2 (carry forward and fleet transfers).  If necessary,
        // credits will be transferred from one compliance category to another.
        //
        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
        }

        public static void CT_CarryForward(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr,
            ICompliance complianceModel, bool expiringOnly, int expiringYrIndex)
        {
            foreach (RC regClass in RegClasses.List) {
                CT_CarryForward(scen, iteration, year, mfr, regClass, complianceModel, expiringOnly, expiringYrIndex);
            }
        }
        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
                    || (scen[rc, year].StandardSettingYear && rc != RC.HDPUV)) {
                return; 
            }

            double credits = mfr.GetNetCredits(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, TCreditsIn should not be considered,
                    // since there is no way of tracking where they came from or their expiration
                    var stats = pMfrRev.ComplianceStats;
                    eCredits = Math.Max(0, stats.Credits[rc] - stats.CreditsOut[rc]);
                }
                else
                {   // checking a year prior to start of modeling -- check for banked credits
                    pMfrRev = mfr.RevisionInfo;
                    eCredits = mfr.CAFEBank[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, false, ct,
                           rc, pYear, pMfrRev, useBank, rc, year, mfr.RevisionInfo);

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

        public static void CT_FleetTransfers(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr,
            ICompliance complianceModel, bool expiringOnly, int expiringYrIndex)
        {
            foreach (RC regClass in RegClasses.List) {
                CT_FleetTransfers(scen, iteration, year, mfr, regClass, complianceModel, expiringOnly, expiringYrIndex);
            }
        }
        public static void CT_FleetTransfers(
            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.AllowFleetTransfers
                    || (scen[rc, year].StandardSettingYear && rc != RC.HDPUV)) {
                return;
            }

            // the amount of credits transferred into the reg-class has already reached or exceeded the cap -or-
            // the fleet has positive credits (thus, in compliance) for this reg-class
            double credits = mfr.GetNetCredits(rc);
            double cap = ComputeTransferCap(mfr, rc, year, ct);
            if (cap <= 0 || credits >= 0) {
                return;
            }

            // for passenger-car reg-class, also need to determine whether achieved CAFE is less than min STND
            // we're not allowed to use credits (other than carry-forward) to offset min-STND shortfall
            if (rc == RC.DomesticCar) {
                var stats = mfr.ComplianceStats;
                if (stats.CAFERating[rc] < stats.PStandard[rc]) {
                    return;
                }
            }

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

            //------------------
            // Transfer Credits
            //
            RC[] eRCArr = CT_FleetTransfers_GetNextRC(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) {
                foreach (RC eRC in eRCArr) {
                    if (mfr.ComplianceStats.VehicleStats[eRC].Sales <= 0 || mfr.GetNetCredits(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.Credits[eRC] - pStats.CreditsOut[eRC]); // see note in "CT_CarryForward(...)"
                        }
                        else
                        {   // checking a year prior to start of modeling -- check for banked credits
                            pMfrRev = mfr.RevisionInfo;
                            eCredits = mfr.CAFEBank[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
                        var mfrRev = mfr.RevisionInfo;
                        RequestCredits(eCredits, rCredits, true, ct, eRC, year, mfrRev, rc, year, mfrRev,
                                       out double outECredits, out double outRCredits);

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

                        // update credits and cap; if compliance achieved or cap reached, return
                        credits = mfr.GetNetCredits(rc);
                        cap = ComputeTransferCap(mfr, rc, year, ct);
                        if (cap <= 0 || credits >= 0) {
                            return;
                        }
                        rCredits = Math.Min(cap, -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.GetNetCredits(eRC);
                if (eCredits <= 0) {
                    continue;
                }

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

                // update credits and cap; if compliance achieved or cap reached, return
                credits = mfr.GetNetCredits(rc);
                cap = ComputeTransferCap(mfr, rc, year, ct);
                if (cap <= 0 || credits >= 0) {
                    return;
                }
                rCredits = Math.Min(cap, -credits); // update maximum credits that can be transferred
            }
        }

        static RC[] CT_FleetTransfers_GetNextRC(RC uRC)
        {
            return TransferCategorySource[uRC];
        }

        static double ComputeTransferCap(IManufacturer mfr, RC rc, ModelYear year, CreditTradingValues ct)
        {
            double cap = ct.GetTransferCaps(rc, year.Year);
            var stats = mfr.ComplianceStats;
            return Math.Floor(cap * stats.VehicleStats[rc].Sales) - stats.CreditsInCapped[rc];
        }

        static void DoTransfer(ICompliance complianceModel, Scenario scen, int iteration, IManufacturer mfr,
            double eCredits, double rCredits, bool applyAdjFactor, CreditTradingValues ct,
            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, applyAdjFactor, ct, eRC, eYear, eMfr, uRC, uYear, uMfr,
                out double outECredits, out double outRCredits);

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

            // also adjust "capped" credits in, if different reg-classes
            // (diff rc's occur when transferring between fleets)
            if (eRC != uRC) {
                uMfr.ComplianceStats.CreditsInCapped[uRC] += outRCredits;
            }

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

        static void RequestCredits(double eCredits, double rCredits, bool applyAdjFactor, CreditTradingValues ct,
            RC eRC, int eYear, IManufacturerRevision eMfr, // params from the "earned" compliance category
            RC uRC, int uYear, IManufacturerRevision uMfr, // params from the "used" compliance category
            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
            //

            // compute vmt levels first (if the adjustment factor is not 1:1)
            double eVMT = 0, uVMT = 0;
            if (applyAdjFactor)
            {
                eVMT = ct.GetAdjustmentVMT(eRC, eYear);
                uVMT = ct.GetAdjustmentVMT(uRC, uYear);
            }

            // compute credits
            double eCAFE = (eMfr == null) ? 0 : eMfr.ComplianceStats.CAFERating[eRC];
            double eSTND = (eMfr == null) ? 0 : eMfr.ComplianceStats.Standard  [eRC];
            double uCAFE = (uMfr == null) ? 0 : uMfr.ComplianceStats.CAFERating[uRC];
            double uSTND = (uMfr == null) ? 0 : uMfr.ComplianceStats.Standard  [uRC];
            //
            RequestCredits(eCredits, rCredits, applyAdjFactor, eVMT, eCAFE, eSTND, uVMT, uCAFE, uSTND, out outECredits, out outRCredits);
        }
        static void RequestCredits(double eCredits, double rCredits, bool applyAdjFactor,
            double eVMT, double eCAFE, double eSTND,    // params from the "earned" compliance category
            double uVMT, double uCAFE, double uSTND,    // params from the "used" compliance category
            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; }

            // obtain the adjustment factor
            double adjFactor = (applyAdjFactor) ? GetCreditAdjFactor(eVMT, eCAFE, eSTND, uVMT, uCAFE, uSTND) : 1;

            // determine max tCredits that can be obtained from available eCredits
            if (!double.IsInfinity(eCredits))
            {
                outRCredits = Math.Floor(eCredits / adjFactor);
                if (double.IsInfinity(rCredits) || outRCredits <= rCredits)
                {   // if outRCredits <= rCredits, that means that avail eCredits is not too much
                    outECredits = Math.Ceiling(outRCredits * adjFactor);
                    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 = Math.Ceiling(rCredits    * adjFactor);
            outRCredits = Math.Floor  (outECredits / adjFactor);
        }

        /// <summary>
        /// Returns the credit adjustment factor between two fleets specified by their VMT, CAFE, and Standard.
        /// </summary>
        /// <param name="eVMT">The lifetime vehicle miles traveled for the manufacturer, model year, and compliance category in which the credit was earned.</param>
        /// <param name="eCAFE">The achieved CAFE value for the manufacturer, compliance category, and model year in which the credit was earned.</param>
        /// <param name="eSTND">The required CAFE standard for the manufacturer, compliance category, and model year in which the credit was earned.</param>
        /// <param name="uVMT">The lifetime vehicle miles traveled for the manufacturer, model year, and compliance category in which the credit is used for compliance.</param>
        /// <param name="uCAFE">The achieved CAFE value for the manufacturer, compliance category, and model year in which the credit is used for compliance.</param>
        /// <param name="uSTND">The required CAFE standard for the manufacturer, compliance category, and model year in which the credit is used for compliance.</param>
        /// <returns>The credit adjustment factor between two fleets specified by their VMT, CAFE, and Standard.</returns>
        static double GetCreditAdjFactor(double eVMT, double eCAFE, double eSTND, double uVMT, double uCAFE, double uSTND)
        {
            return Math.Round((uVMT * eCAFE * eSTND) / (eVMT * uCAFE * uSTND), 4);
        }

    }
}
