import BigNumber from 'bignumber.js';
import { TREND_THRESHOLDS } from './constants';
import { Reward } from './entities';
import { iqr } from './iqr';

type BigNumberish = number | string | BigNumber;

const ethUnits = {
    gwei: 1000000000,
};

const { FALLING, MEDIAN_SLOPE, RAISING, SURGING } = TREND_THRESHOLDS;

export const multiply = (numberOne: BigNumberish, numberTwo: BigNumberish): BigNumber => new BigNumber(numberOne).times(numberTwo);

export const divide = (numberOne: BigNumberish, numberTwo: BigNumberish): BigNumber => {
    if (!(numberOne || numberTwo)) return new BigNumber(0);
    return new BigNumber(numberOne).dividedBy(numberTwo);
};

export const gweiToWei = (gweiAmount: BigNumberish) => {
    const weiAmount = multiply(gweiAmount, ethUnits.gwei).toFixed(0);
    return weiAmount;
};

export const weiToGwei = (weiAmount: BigNumberish) => {
    const gweiAmount = divide(weiAmount, ethUnits.gwei).toFixed();
    return gweiAmount;
};

export const weiToGweiNumber = (weiAmount: BigNumberish) => {
    const gweiAmount = divide(weiAmount, ethUnits.gwei).toNumber();
    return gweiAmount;
};

export const weiToString = (weiAmount: BigNumberish) => {
    return new BigNumber(weiAmount).toString();
};

export const samplingCurve = (sumWeight: number, sampleMin: number, sampleMax: number) => {
    if (sumWeight <= sampleMin) {
        return 0;
    }
    if (sumWeight >= sampleMax) {
        return 1;
    }
    return (1 - Math.cos(((sumWeight - sampleMin) * 2 * Math.PI) / (sampleMax - sampleMin))) / 2;
};

export const linearRegression = (y: number[]) => {
    const x = Array.from(Array(y.length + 1).keys());
    const n = y.length;
    let sumX = 0;
    let sumY = 0;
    let sumXY = 0;
    let sumXX = 0;

    for (let i = 0; i < y.length; i++) {
        const cY = Number(y[i]);
        const cX = Number(x[i]);
        sumX += cX;
        sumY += cY;
        sumXY += cX * cY;
        sumXX += cX * cX;
    }
    const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);

    return slope;
};

export const suggestBaseFee = (baseFee: number[], order: number[], timeFactor: number, sampleMin: number, sampleMax: number) => {
    if (timeFactor < 1e-6) {
        return baseFee[baseFee.length - 1];
    }
    const pendingWeight = (1 - Math.exp(-1 / timeFactor)) / (1 - Math.exp(-baseFee.length / timeFactor));
    let sumWeight = 0;
    let result = 0;
    let samplingCurveLast = 0;
    for (let or of order) {
        sumWeight += pendingWeight * Math.exp((or - baseFee.length + 1) / timeFactor);
        const samplingCurveValue = samplingCurve(sumWeight, sampleMin, sampleMax);
        result += (samplingCurveValue - samplingCurveLast) * baseFee[or];
        if (samplingCurveValue >= 1) {
            return result;
        }
        samplingCurveLast = samplingCurveValue;
    }
    return result;
};

export const getOutlierBlocksToRemove = (blocksRewards: Reward[], index: number) => {
    const blockRewardsInGwei = blocksRewards.map((reward) => weiToGweiNumber(reward[index]));
    const filtered = iqr(blockRewardsInGwei);
    const outliers = blockRewardsInGwei.reduce((result: number[], n: number, index: number) => {
        if (!filtered.includes(n)) {
            result.push(index);
        }
        return result;
    }, []);
    return outliers;
};

export const rewardsFilterOutliers = (blocksRewards: Reward[], outlierBlocks: number[], rewardIndex: number) =>
    blocksRewards.filter((_, index) => !outlierBlocks.includes(index)).map((reward) => weiToGweiNumber(reward[rewardIndex]));

const calculateGroupInfo = (baseFees: number[]) => {
    const sortedBaseFees = baseFees.sort((a, b) => a - b);
    const min = sortedBaseFees[0];
    const max = sortedBaseFees[sortedBaseFees.length - 1];
    const median = sortedBaseFees[Math.floor(sortedBaseFees.length / 2)];
    return { max, median, min };
};

const createSubsets = (numbers: number[], n: number) => {
    const subsets = [];
    for (let i = 0; i < numbers.length; i = i + n) {
        subsets.push(numbers.slice(i, i + n));
    }
    return subsets;
};

export const getData = (numbers: number[], n: number) => {
    const subsets = createSubsets(numbers, n);
    const subsetsInfo = subsets.map((subset) => calculateGroupInfo(subset));
    const { max: lastMax, min: lastMin, median: lastMedian } = subsetsInfo[subsetsInfo.length - 1];
    const medianData = subsetsInfo.map((data) => data.median);
    const medianSlope = linearRegression(medianData);

    return {
        max: lastMax,
        median: lastMedian,
        medianSlope,
        min: lastMin,
    };
};

export const calculateBaseFeeTrend = (baseFees: number[], currentBaseFee: string) => {
    let trend = 0;
    try {
        // taking 50 blocks
        const baseFees50Blocks = baseFees.slice(51);
        // divide it in groups of 5
        const n50 = {
            g5: getData(baseFees50Blocks, 5),
        };

        // taking 100 blocks
        const baseFees100Blocks = baseFees.slice(1);
        // divide it in groups of 25
        const n100 = {
            g25: getData(baseFees100Blocks, 25),
        };

        const maxByMedian = n100.g25.max / n100.g25.median;
        const minByMedian = n100.g25.min / n100.g25.median;

        if (maxByMedian > SURGING) {
            trend = 2;
        } else if (maxByMedian > RAISING && minByMedian > FALLING) {
            trend = 1;
        } else if (maxByMedian < RAISING && minByMedian > FALLING) {
            if (n50.g5.medianSlope < MEDIAN_SLOPE) {
                trend = -1;
            } else {
                trend = 0;
            }
        } else if (maxByMedian < RAISING && minByMedian < FALLING) {
            trend = -1;
        } else {
            // if none is on the threshold
            if (weiToGweiNumber(currentBaseFee) > n100.g25.median) {
                trend = 1;
            } else {
                trend = -1;
            }
        }
    } catch (e) {
        //
    }
    return trend;
};
