package gregtech.api.util;

public class GT_OverclockCalculator {

    /**
     * @mAmps - Amperage of the multiblock
     * @mEUt - Voltage of the multiblock
     * @mRecipeEUt - Voltage the recipe will run at
     * @mRecipeAmps - The amount of amps the recipe needs
     */
    private long mAmps = 1, mEUt = 0, mRecipeEUt = 0, mRecipeAmps = 1;
    /**
     * @mEUtDiscount - Discount for EUt at the beginning of calculating overclocks, like GT++ machines
     * @mSpeedBoost - Speeding/Slowing up/down the duration of a recipe at the beginning of calculating overclocks, like
     *              GT++ machines
     * @mHeatDiscountAmont - The value used for discount final eut per 900 heat
     */
    private float mEUtDiscount = 1, mSpeedBoost = 1, mHeatDiscountAmount = 0.95f;
    /**
     * @mEUtIncreasePerOC - How much the bits should be moved to the left when it is overclocking (Going up, 2 meaning
     *                    it is multiplied with 4x)
     * @mDurationDecreasePerOC - How much the bits should be moved to the right when its overclocking (Going down, 1
     *                         meaning it is halved)
     * @mDuration - Duration of the recipe
     * @mParallel - The parallel the multi has when trying to overclock
     * @mRecipeHeat - The min heat required for the recipe
     * @mMultiHeat - The heat the multi has when starting the recipe
     * @mHeatPerfectOC - How much the bits should be moved to the right for each 1800 above recipe heat (Used for
     *                 duration)
     */
    private int mEUtIncreasePerOC = 2, mDurationDecreasePerOC = 1, mDuration = 0, mParallel = 1, mRecipeHeat = 0,
        mMultiHeat = 0, mHeatPerfectOC = 2;
    /**
     * @mHeatOC - Whether to enable overclocking with heat like the EBF every 1800 heat difference
     * @mOneTickDiscount - Whether to give EUt Discount when the duration goes below one tick
     * @calculates - variable to check whether the overclocks have been calculated
     * @mHeatDiscount - Whether to enable heat discounts every 900 heat difference
     */
    private boolean mHeatOC, mOneTickDiscount, calculated, mHeatDiscount;

    private static final int HEAT_DISCOUNT_THRESHOLD = 900;
    private static final int HEAT_PERFECT_OVERCLOCK_THRESHOLD = 1800;

    /**
     * An Overclock helper for calculating overclocks in many different situations
     */
    public GT_OverclockCalculator() {}

    /**
     * @param aRecipeEUt Sets the Recipe's starting voltage
     */
    public GT_OverclockCalculator setRecipeEUt(long aRecipeEUt) {
        mRecipeEUt = aRecipeEUt;
        return this;
    }

    /**
     * @param aEUt Sets the EUt that the multiblock can use. This is the voltage of the multi
     */
    public GT_OverclockCalculator setEUt(long aEUt) {
        mEUt = aEUt;
        return this;
    }

    /**
     * @param aDuration Sets the duration of the recipe
     */
    public GT_OverclockCalculator setDuration(int aDuration) {
        mDuration = aDuration;
        return this;
    }

    /**
     * @param aAmps Sets the Amperage that the multi can support
     */
    public GT_OverclockCalculator setAmperage(long aAmps) {
        mAmps = aAmps;
        return this;
    }

    /**
     * @param aRecipeAmps Sets the Amperage of the recipe
     */
    public GT_OverclockCalculator setRecipeAmperage(long aRecipeAmps) {
        mRecipeAmps = aRecipeAmps;
        return this;
    }

    /**
     * Enables Perfect OC in calculation
     */
    public GT_OverclockCalculator enablePerfectOC() {
        mDurationDecreasePerOC = 2;
        return this;
    }

    /**
     * Enables calculating overclocking using EBF's perfectOC
     */
    public GT_OverclockCalculator enableHeatOC() {
        mHeatOC = true;
        return this;
    }

    /**
     * Enables adding a heat discount at the end of calculating an overclock, just like the EBF
     */
    public GT_OverclockCalculator enableHeatDiscount() {
        mHeatDiscount = true;
        return this;
    }

    /**
     * Sets the starting heat of the recipe
     */
    public GT_OverclockCalculator setRecipeHeat(int aRecipeHeat) {
        mRecipeHeat = aRecipeHeat;
        return this;
    }

    /**
     * Sets the heat of the coils on the multi
     */
    public GT_OverclockCalculator setMultiHeat(int aMultiHeat) {
        mMultiHeat = aMultiHeat;
        return this;
    }

    /**
     * Sets an EUtDiscount. 0.9 is 10% less energy. 1.1 is 10% more energy
     */
    public GT_OverclockCalculator setEUtDiscount(float aEUtDiscount) {
        mEUtDiscount = aEUtDiscount;
        return this;
    }

    /**
     * Sets a Speed Boost for the multiblock. 0.9 is 10% faster. 1.1 is 10% slower
     */
    public GT_OverclockCalculator setSpeedBoost(float aSpeedBoost) {
        mSpeedBoost = aSpeedBoost;
        return this;
    }

    /**
     * Sets the parallel that the multiblock uses
     */
    public GT_OverclockCalculator setParallel(int aParallel) {
        mParallel = aParallel;
        return this;
    }

    /**
     * Sets the heat discount during OC calculation if HeatOC is used. Default: 0.95 = 5% discount Used like a EU/t
     * Discount
     */
    public GT_OverclockCalculator setHeatDiscount(float aHeatDiscount) {
        mHeatDiscountAmount = aHeatDiscount;
        return this;
    }

    /**
     * Sets the Overclock that should be calculated when one. This uses BitShifting! Default is 2, which is a 4x
     * decrease
     */
    public GT_OverclockCalculator setHeatPerfectOC(int aHeatPerfectOC) {
        mHeatPerfectOC = aHeatPerfectOC;
        return this;
    }

    /**
     * Sets the amount that the EUt increases per overclock. This uses BitShifting! Default is 2, which is a 4x increase
     */
    public GT_OverclockCalculator setEUtIncreasePerOC(int aEUtIncreasePerOC) {
        mEUtIncreasePerOC = aEUtIncreasePerOC;
        return this;
    }

    /**
     * Sets the amount that the duration decreases per overclock. This uses BitShifting! Default is 1, which halves the
     * duration
     */
    public GT_OverclockCalculator setDurationDecreasePerOC(int aDurationDecreasePerOC) {
        mDurationDecreasePerOC = aDurationDecreasePerOC;
        return this;
    }

    /**
     * Enables One Tick Discount on EUt based on Duration Decrease Per Overclock. This functions the same as single
     * blocks.
     */
    public GT_OverclockCalculator enableOneTickDiscount() {
        mOneTickDiscount = true;
        return this;
    }

    /**
     * Call this when all values have been put it.
     */
    public GT_OverclockCalculator calculate() {
        calculateOverclock();
        calculated = true;
        return this;
    }

    private void calculateOverclock() {
        if (mRecipeEUt > mEUt || mRecipeHeat > mMultiHeat) {
            mRecipeEUt = Long.MAX_VALUE;
            mDuration = Integer.MAX_VALUE;
            return;
        }
        int heatDiscounts = (mMultiHeat - mRecipeHeat) / HEAT_DISCOUNT_THRESHOLD;
        mDuration = (int) Math.ceil(mDuration * mSpeedBoost);
        if (mHeatOC) {
            while (mRecipeHeat + HEAT_PERFECT_OVERCLOCK_THRESHOLD <= mMultiHeat
                && mRecipeEUt * mParallel * mRecipeAmps << 2 < mEUt * mAmps) {
                if (mDuration < 1) {
                    break;
                }
                mRecipeEUt <<= mEUtIncreasePerOC;
                mDuration >>= mHeatPerfectOC;
                mRecipeHeat += HEAT_PERFECT_OVERCLOCK_THRESHOLD;
            }
        }

        int tRecipeTier = GT_Utility.getTier(mRecipeEUt);
        if (tRecipeTier == 0) {
            int tTier = GT_Utility.getTier(mEUt);
            int tTierDifference = tTier - 1;
            long tNextConsumption = ((long) Math.ceil(mRecipeEUt * mParallel * mRecipeAmps * mEUtDiscount))
                << mEUtIncreasePerOC;
            while (tTierDifference > 0 && tNextConsumption < mEUt * mAmps) {
                mRecipeEUt <<= mEUtIncreasePerOC;
                mDuration >>= mDurationDecreasePerOC;
                tNextConsumption <<= mEUtIncreasePerOC;
                tTierDifference--;
            }
        } else {
            long tNextConsumption = ((long) Math.ceil(mRecipeEUt * mParallel * mRecipeAmps * mEUtDiscount))
                << mEUtIncreasePerOC;
            while (tNextConsumption < mEUt * mAmps) {
                if (mDuration <= 1) {
                    break;
                }
                mRecipeEUt <<= mEUtIncreasePerOC;
                mDuration >>= mDurationDecreasePerOC;
                tNextConsumption <<= mEUtIncreasePerOC;
            }
        }

        if (mDuration < 1) {
            mDuration = 1;
        }

        if (mHeatDiscount) {
            mRecipeEUt = (long) Math.ceil(mRecipeEUt * Math.pow(mHeatDiscountAmount, heatDiscounts));
        }

        if (mOneTickDiscount) {
            int voltageDifference = GT_Utility.getTier(mEUt) - GT_Utility.getTier(mRecipeEUt);
            mRecipeEUt >>= voltageDifference * mDurationDecreasePerOC;
            if (mRecipeEUt < 1) {
                mRecipeEUt = 1;
            }
        }

        mRecipeEUt = (long) Math.ceil(mRecipeEUt * mParallel * mRecipeAmps * mEUtDiscount);
    }

    /**
     * @return The consumption after overclock has been calculated
     */
    public long getConsumption() {
        if (!calculated) {
            calculate();
        }
        return mRecipeEUt;
    }

    /**
     * @return The duration of the recipe after overclock has been calculated
     */
    public int getDuration() {
        if (!calculated) {
            calculate();
        }
        return mDuration;
    }
}