import OrderBookEntry from "../../../apiwrapper/orderbook/OrderBookEntry.js";
import AssetInstrumentCalculator from "../../util/AssetInstrumentCalculator.js";
import UnitHelper from "../../util/UnitHelper.js";
import AggregatedDataPoint from "./AggregatedDataPoint.js";

export default class AssetDataAggregator {
    static #SLOT_1_SEC = 1000;
    static #SLOT_1_MIN = 60000;

    /**
     * @type {AggregatedDataPoint}
     */
    #currentDataPoint = null;

    /**
     * @type {AggregatedDataPoint[]}
     */
    #perSecondDataPoints = [];

    /**
     * @type {AggregatedDataPoint[]}
     */
    #perMinuteDataPoints = [];

    #lastOneMinUpdate = 0;

    #updateCurrentDataPoint(){
        const timestamp = Date.now();

        // not initialized yet
        if(this.#currentDataPoint == null){
            this.#currentDataPoint = new AggregatedDataPoint(timestamp, AssetDataAggregator.#SLOT_1_SEC);
            this.#lastOneMinUpdate = timestamp;
            return;
        }

        this.#updateOneSecSlot(timestamp);
        this.#updateOneMinSlot(timestamp);
    }

    #updateOneMinSlot(timestamp){
        const elapsedTime = timestamp - this.#lastOneMinUpdate;

        // timeslot not over or not enough data to create
        if(elapsedTime < AssetDataAggregator.#SLOT_1_MIN || this.#perSecondDataPoints.length < 60){
            return;
        }

        const tmpSlot = new AggregatedDataPoint(timestamp - AssetDataAggregator.#SLOT_1_MIN, AssetDataAggregator.#SLOT_1_MIN);
        
        for(let i = this.#perSecondDataPoints.length - 60; i < this.#perSecondDataPoints.length; i++){
            const dp = this.#perSecondDataPoints[i];
            tmpSlot.pushAsk(dp.bestAsk);
            tmpSlot.pushBid(dp.bestBid);
            tmpSlot.addTrades(dp.tradeCount, dp.tradeVolume);
        }
        tmpSlot.end(timestamp + AssetDataAggregator.#SLOT_1_MIN);
        if(this.#perMinuteDataPoints.length > 0){
            tmpSlot.sma5min = AssetInstrumentCalculator.calculateSMA(this.#perMinuteDataPoints.slice(-1 * Math.min(5, this.#perMinuteDataPoints.length)));
            tmpSlot.ema5min = AssetInstrumentCalculator.calculateEMA(this.#perMinuteDataPoints.slice(-1 * Math.min(5, this.#perMinuteDataPoints.length)));
            tmpSlot.sma15min = AssetInstrumentCalculator.calculateSMA(this.#perMinuteDataPoints.slice(-1 * Math.min(15, this.#perMinuteDataPoints.length)));
            tmpSlot.ema15min = AssetInstrumentCalculator.calculateEMA(this.#perMinuteDataPoints.slice(-1 * Math.min(15, this.#perMinuteDataPoints.length)));
        } else {
            tmpSlot.sma5min = tmpSlot.ema5min = tmpSlot.sma15min = tmpSlot.ema15min = tmpSlot.avgPrice;
        }
        this.#lastOneMinUpdate = timestamp;

        this.#pushSlotAndFillUp(this.#perMinuteDataPoints, tmpSlot, AssetDataAggregator.#SLOT_1_MIN, timestamp);

        while(this.#perSecondDataPoints.length > 10080){
            this.#perSecondDataPoints.shift();
        }
    }

    #updateOneSecSlot(timestamp){
        const elapsedTime = timestamp - this.#currentDataPoint.startTime;

        // timeslot not over
        if(elapsedTime < AssetDataAggregator.#SLOT_1_SEC || (!this.#currentDataPoint.hasValues && this.#perSecondDataPoints.length < 1)){
            return;
        }

        let tmpSlot = this.#currentDataPoint;
        if(!tmpSlot.hasValues){
            tmpSlot = this.#perSecondDataPoints[this.#perSecondDataPoints.length - 1].clone();
            tmpSlot.startTime = timestamp - AssetDataAggregator.#SLOT_1_SEC;
            tmpSlot.endTime = timestamp;
            tmpSlot.tradeCount = 0;
            tmpSlot.tradeVolume = 0;
        }
        this.#currentDataPoint = new AggregatedDataPoint(timestamp, AssetDataAggregator.#SLOT_1_SEC);
        tmpSlot.end(timestamp);

        this.#pushSlotAndFillUp(this.#perSecondDataPoints, tmpSlot, AssetDataAggregator.#SLOT_1_SEC, timestamp);

        while(this.#perSecondDataPoints.length > 60){
            this.#perSecondDataPoints.shift();
        }
    }

    /**
     * @param {AggregatedDataPoint[]} targetArr 
     * @param {AggregatedDataPoint} point 
     * @param {number} duration 
     * @param {number} timestamp
     */
    #pushSlotAndFillUp(targetArr, point, duration, timestamp){
        const elapsedTime = timestamp - point.startTime;
        const missedSteps = Math.floor(elapsedTime / duration) - 1;

        targetArr.push(point);

        for(let i = 0; i < missedSteps; i++){
            const newPoint = point.clone();
            newPoint.startTime = point.startTime + duration * (i + 1);
            newPoint.endTime = newPoint.startTime + duration;
            newPoint.tradeCount = 0;
            newPoint.tradeVolume = 0;
            targetArr.push(newPoint);
        }
    }

    /**
     * Returns an array of aggregated data points.
     * If the duration is smaler then 1 min, datapoints per second will be returned, else datapoints per minute.
     * If the starttime is not it defaults to now() - duration;
     * If starttime is older then 1m, even with a duration less then 1m, only 1m data points will be returned.
     * 
     * If no data for given start timestamp and duration exists, an empty array will be returned! 
     * 
     * @param {string} duration chrono unit (eg. 1000 or 1s or 15m)
     * @param {number} start start timestamp
     * @param {boolean} returnUnfinished if true, unfinished timeslots will be returned (minutes only!)
     * @returns {AggregatedDataPoint[]}
     */
    getData(duration, start = -1, returnUnfinished = false){
        let result = [];
        const durationInMs = UnitHelper.formatedDurationToMs(duration);

        // if seconds data required
        if(durationInMs <= 60000 && start == -1){
            const dpCount = Math.round(durationInMs / AssetDataAggregator.#SLOT_1_SEC);
            if(dpCount <= this.#perSecondDataPoints.length){
                result = this.#perSecondDataPoints.slice(-1 * dpCount);
            }
        } else {
            const dpCount = Math.round(durationInMs / AssetDataAggregator.#SLOT_1_MIN);
            const pointOffset = start == -1 ? 0 : Math.round(start / AssetDataAggregator.#SLOT_1_MIN);
            if(pointOffset + dpCount <= this.#perMinuteDataPoints.length || returnUnfinished){
                const start = pointOffset + dpCount <= this.#perMinuteDataPoints.length ? -1 * (pointOffset + dpCount) : -1 * this.#perMinuteDataPoints.length;
                result = this.#perMinuteDataPoints.slice(start, pointOffset > 0 ? -1 * pointOffset : this.#perMinuteDataPoints.length);
            }
        }

        return result;
    }

    /**
     * @param {number} time 
     * @param {number} price 
     * @param {number} quantity 
     */
    pushTrade(time, price, quantity) {
        this.#updateCurrentDataPoint();
        this.#currentDataPoint.pushTrade(time, price, quantity);
    }

    /**
     * @param {number} time 
     * @param {number} low 
     * @param {number} high 
     * @param {number} open 
     * @param {number} close 
     */
    pushKLine(time, low, high, open, close){
        // ingore actual data and just update the point, as data is taken only from orderbook
        this.#updateCurrentDataPoint();
    }

    /**
     * @param {OrderBookEntry} ask 
     */
    pushBestAsk(ask){
        this.#updateCurrentDataPoint();
        this.#currentDataPoint.pushAsk(ask);
    }

    /**
     * @param {OrderBookEntry} bid 
     */
    pushBestBid(bid){
        this.#updateCurrentDataPoint();
        this.#currentDataPoint.pushBid(bid);
    }
}