import BN from "bn.js";

const { Zilliqa } = require('@zilliqa-js/zilliqa');

const KEY_LAST_REWARD_CYCLE = 'lastrewardcycle';
const KEY_DIRECT_DEPOSIT_DELEG = 'direct_deposit_deleg';
const KEY_BUFF_DEPOSIT_DELEG = 'buff_deposit_deleg';
const KEY_STAKE_SSN_PER_CYCLE = 'stake_ssn_per_cycle';
const KEY_LAST_WITHDRAW_CYCLE = 'last_withdraw_cycle_deleg'
const KEY_DELEG_PER_CYCLE = 'deleg_stake_per_cycle'

export class RewardCalculator {
    zilliqa: any;
    contract: any;
    last_reward_cycle_json: any;

    constructor(url: String, ssnlist: String) {
         this.zilliqa = new Zilliqa(url);
         this.contract = this.zilliqa.contracts.at(ssnlist);
    }

    // store the required maps first
    async compute_maps() {
        this.last_reward_cycle_json = await this.contract.getSubState(KEY_LAST_REWARD_CYCLE);
    }

    async get_rewards(ssnaddr: string, delegator: string) {
        const last_withdraw_cycle_map = await this.contract.getSubState(KEY_LAST_WITHDRAW_CYCLE, [delegator.toLowerCase()]);
        const reward_list = await this.get_reward_cycle_list(last_withdraw_cycle_map, this.last_reward_cycle_json, ssnaddr, delegator);
        const delegate_per_cycle = await this.combine_buff_direct(ssnaddr,delegator,reward_list);
        const need_list = await this.get_reward_need_cycle_list(last_withdraw_cycle_map, this.last_reward_cycle_json, ssnaddr, delegator);
        const rewards = await this.calculate_rewards(ssnaddr,delegate_per_cycle,need_list);
        return rewards;
    }


    // get reward cycle list
    async get_reward_cycle_list(last_withdraw_cycle_map: any, last_reward_cycle_json: any, ssnaddr: string, delegator: string) {
        // remove this
        // console.log(last_withdraw_cycle_map);
        
        if (last_withdraw_cycle_map !== null 
            && last_withdraw_cycle_map[KEY_LAST_WITHDRAW_CYCLE][delegator][ssnaddr] !== undefined) {
            const last_reward_cycle = Number.parseInt(last_reward_cycle_json[KEY_LAST_REWARD_CYCLE]);
            let result_list = [];
            let i =  1;
            for (i; i <= last_reward_cycle; i++) {
                result_list.push(i)
            }

            return result_list;

        } else {
            return [];
        }
    }

    // to get those cycles need to calculte rewards
    async get_reward_need_cycle_list(last_withdraw_cycle_map: any, last_reward_cycle_json: any, ssnaddr: string, delegator: string) {
         // get last_reward_cycle;
        const last_reward_cycle = last_reward_cycle_json[KEY_LAST_REWARD_CYCLE];

        // get last_withdraw_cycle_deleg
        if (last_withdraw_cycle_map === null) {
            return [];
        }
        const last_withdraw_cycle = last_withdraw_cycle_map[KEY_LAST_WITHDRAW_CYCLE][delegator][ssnaddr];

        // to filter those elements that meet
        // last_withdraw_cycle < elements <= last_reward_cycle
        const reward_cycle_list_reverse = await this.get_reward_cycle_list(last_withdraw_cycle_map, last_reward_cycle_json, ssnaddr, delegator);
        const cycle_need_to_calculate = reward_cycle_list_reverse.filter(c => c > Number.parseInt(last_withdraw_cycle) && c <= Number.parseInt(last_reward_cycle));
        return cycle_need_to_calculate;
    }

    // to combine buffered deposit map and direct deposit map
    // eventually get the actual delegate amount of every cycle for this particular ssn operator
    async combine_buff_direct(ssnaddr: string, delegator: string, reward_list: number[]) {
        const result_map = new Map <number, BN>();

        const direct_deposit_json = await this.contract.getSubState(KEY_DIRECT_DEPOSIT_DELEG, [delegator.toLowerCase(), ssnaddr]);
        const buffer_deposit_json = await this.contract.getSubState(KEY_BUFF_DEPOSIT_DELEG, [delegator.toLowerCase(), ssnaddr]);
        const deleg_stake_per_cycle_json = await this.contract.getSubState(KEY_DELEG_PER_CYCLE, [delegator.toLowerCase(), ssnaddr]);

        let direct_deposit_map: any = null;
        let buffer_deposit_map: any = null;
        let deleg_stake_per_cycle_map: any = null;

        if (direct_deposit_json !== null) {
            direct_deposit_map = direct_deposit_json[KEY_DIRECT_DEPOSIT_DELEG][delegator.toLowerCase()][ssnaddr];
        }

        if (buffer_deposit_json !== null) {
            buffer_deposit_map = buffer_deposit_json[KEY_BUFF_DEPOSIT_DELEG][delegator.toLowerCase()][ssnaddr];
        }

        if (deleg_stake_per_cycle_json !== null) {
            deleg_stake_per_cycle_map = deleg_stake_per_cycle_json[KEY_DELEG_PER_CYCLE][delegator.toLowerCase()][ssnaddr];
        }

        reward_list.forEach((cycle: number) => {
            // for every reward cycle, we need to get
            // 1. cycle - 1 in direct deposit
            // 2. cycle - 2 in buffered deposit
            // 3. accumulate last result to get total amount for this cycle
            const c1 = cycle - 1;
            const c2 = cycle - 2;
            let hist_amt = new BN(0);
            if (deleg_stake_per_cycle_map !== null 
                && deleg_stake_per_cycle_map[c1.toString()] !== undefined) {
                    hist_amt = new BN(deleg_stake_per_cycle_map[c1.toString()]);
            }

            let dir_amt = new BN(0);
            if (direct_deposit_map !== null 
                && direct_deposit_map[c1.toString()] !== undefined) {
                dir_amt = new BN(direct_deposit_map[c1.toString()]);
            }

            let buf_amt = new BN(0);
            if (buffer_deposit_map !== null 
                && buffer_deposit_map[c2.toString()] !== undefined) {
                    buf_amt = new BN(buffer_deposit_map[c2.toString()]);
            }

            let total_amt_tmp = dir_amt.add(buf_amt);
            total_amt_tmp = total_amt_tmp.add(hist_amt);
            const last_amt = result_map.get(c1);
            if (last_amt !== undefined) {
                const total_amt = total_amt_tmp.add(last_amt);
                result_map.set(cycle, total_amt);
            } else {
                result_map.set(cycle, total_amt_tmp);
            }
        });
        return result_map;
    }


    async calculate_rewards(ssnaddr: string, delegate_per_cycle: Map<number, BN>, need_list: number[]) {
        var result_rewards = new BN(0);

        const stake_ssn_per_cycle_map = await this.contract.getSubState(KEY_STAKE_SSN_PER_CYCLE, [ssnaddr]);

        if (stake_ssn_per_cycle_map === null) {
            return result_rewards;
        }
        need_list.forEach((cycle: number) => {
            const cycle_info = stake_ssn_per_cycle_map[KEY_STAKE_SSN_PER_CYCLE][ssnaddr][cycle];
            if (cycle_info === undefined) {
                // no rewards for this cycle, just skip
            } else {
                const total_rewards = new BN(cycle_info['arguments'][1]);
                const total_stake = new BN(cycle_info['arguments'][0]);

                let deleg_amt: any = delegate_per_cycle.get(cycle);

                if (deleg_amt !== undefined) {
                    const rewards_tmp = deleg_amt.mul(total_rewards);
                    const rewards = rewards_tmp.div(total_stake);
                    result_rewards = result_rewards.add(rewards);
                }
            }
        });

        return result_rewards;
    }
}