"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DividendDistribution = void 0;
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = require("lodash");
const internal_1 = require("../../../internal");
const queries_1 = require("../../../middleware/queries");
const types_1 = require("../../../types");
const utils_1 = require("../../../types/utils");
const constants_1 = require("../../../utils/constants");
const conversion_1 = require("../../../utils/conversion");
const internal_2 = require("../../../utils/internal");
const notExistsMessage = 'The Dividend Distribution no longer exists';
/**
 * Represents a Corporate Action via which a Security Token issuer wishes to distribute dividends
 *   between a subset of the Tokenholders (targets)
 */
class DividendDistribution extends internal_1.CorporateActionBase {
    /**
     * @hidden
     */
    constructor(args, context) {
        const { origin, currency, perShare, maxAmount, expiryDate, paymentDate } = args, corporateActionArgs = __rest(args, ["origin", "currency", "perShare", "maxAmount", "expiryDate", "paymentDate"]);
        super(Object.assign({}, corporateActionArgs), context);
        this.origin = origin;
        this.currency = currency;
        this.perShare = perShare;
        this.maxAmount = maxAmount;
        this.expiryDate = expiryDate;
        this.paymentDate = paymentDate;
        this.pay = internal_2.createProcedureMethod({
            getProcedureAndArgs: payDividendsArgs => [
                internal_1.payDividends,
                Object.assign(Object.assign({}, payDividendsArgs), { distribution: this }),
            ],
        }, context);
        this.claim = internal_2.createProcedureMethod({ getProcedureAndArgs: () => [internal_1.claimDividends, { distribution: this }], voidArgs: true }, context);
        this.modifyCheckpoint = internal_2.createProcedureMethod({
            getProcedureAndArgs: modifyCheckpointArgs => [
                internal_1.modifyDistributionCheckpoint,
                Object.assign({ distribution: this }, modifyCheckpointArgs),
            ],
        }, context);
        this.reclaimFunds = internal_2.createProcedureMethod({
            getProcedureAndArgs: () => [internal_1.reclaimDividendDistributionFunds, { distribution: this }],
            voidArgs: true,
        }, context);
    }
    /**
     * Retrieve the Checkpoint associated with this Dividend Distribution. If the Checkpoint is scheduled and has not been created yet,
     *   the corresponding CheckpointSchedule is returned instead
     */
    checkpoint() {
        const _super = Object.create(null, {
            checkpoint: { get: () => super.checkpoint }
        });
        return __awaiter(this, void 0, void 0, function* () {
            const exists = yield this.exists();
            if (!exists) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.DataUnavailable,
                    message: notExistsMessage,
                });
            }
            const checkpoint = yield _super.checkpoint.call(this);
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            return checkpoint;
        });
    }
    /**
     * Retrieve whether the Distribution exists
     */
    exists() {
        return __awaiter(this, void 0, void 0, function* () {
            const distribution = yield this.fetchDistribution();
            return distribution.isSome;
        });
    }
    /**
     * Retrieve details associated with this Dividend Distribution
     */
    details() {
        return __awaiter(this, void 0, void 0, function* () {
            const distribution = yield this.fetchDistribution();
            if (distribution.isNone) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.DataUnavailable,
                    message: notExistsMessage,
                });
            }
            const { reclaimed, remaining } = distribution.unwrap();
            return {
                remainingFunds: conversion_1.balanceToBigNumber(remaining),
                fundsReclaimed: conversion_1.boolToBoolean(reclaimed),
            };
        });
    }
    /**
     * Retrieve a comprehensive list of all Identities that are entitled to dividends in this Distribution (participants),
     *   the amount they are entitled to and whether they have been paid or not
     *
     * @note this request can take a lot of time with large amounts of Tokenholders
     * @note if the Distribution Checkpoint hasn't been created yet, the result will be an empty array.
     *   This is because the Distribution participants cannot be determined without a Checkpoint
     */
    getParticipants() {
        return __awaiter(this, void 0, void 0, function* () {
            const { targets: { identities: targetIdentities, treatment }, paymentDate, perShare, } = this;
            let balances = [];
            const checkpoint = yield this.checkpoint();
            if (checkpoint instanceof internal_1.CheckpointSchedule) {
                return [];
            }
            let allFetched = false;
            let start;
            while (!allFetched) {
                const { data, next } = yield checkpoint.allBalances({ size: constants_1.MAX_PAGE_SIZE, start });
                start = next || undefined;
                allFetched = !next;
                balances = [...balances, ...data];
            }
            const isExclusion = treatment === types_1.TargetTreatment.Exclude;
            const participants = [];
            const clonedTargets = [...targetIdentities];
            balances.forEach(({ identity: { did }, identity, balance }) => {
                const isTarget = !!lodash_1.remove(clonedTargets, ({ did: targetDid }) => did === targetDid).length;
                if (balance.gt(0) && internal_2.xor(isTarget, isExclusion)) {
                    participants.push({
                        identity,
                        amount: balance.multipliedBy(perShare),
                        paid: false,
                    });
                }
            });
            // participants can't be paid before the payment date
            if (paymentDate < new Date()) {
                return participants;
            }
            const paidStatuses = yield this.getParticipantStatuses(participants);
            return paidStatuses.map((paid, index) => (Object.assign(Object.assign({}, participants[index]), { paid })));
        });
    }
    /**
     * Retrieve an Identity that is entitled to dividends in this Distribution (participant),
     *   the amount it is entitled to and whether it has been paid or not
     *
     * @param args.identity - defaults to the current Identity
     *
     * @note if the Distribution Checkpoint hasn't been created yet, the result will be null.
     *   This is because the Distribution participant's corresponding payment cannot be determined without a Checkpoint
     */
    getParticipant(args) {
        return __awaiter(this, void 0, void 0, function* () {
            const { id: localId, token: { ticker }, targets: { identities: targetIdentities, treatment }, paymentDate, perShare, context, context: { polymeshApi: { query }, }, } = this;
            const checkpoint = yield this.checkpoint();
            if (checkpoint instanceof internal_1.CheckpointSchedule) {
                return null;
            }
            const [did, balance] = yield Promise.all([
                internal_2.getDid(args === null || args === void 0 ? void 0 : args.identity, context),
                checkpoint.balance(args),
            ]);
            const identity = new internal_1.Identity({ did }, context);
            const isTarget = !!targetIdentities.find(({ did: targetDid }) => did === targetDid);
            let participant;
            const isExclusion = treatment === types_1.TargetTreatment.Exclude;
            if (balance.gt(0) && internal_2.xor(isTarget, isExclusion)) {
                participant = {
                    identity,
                    amount: balance.multipliedBy(perShare),
                    paid: false,
                };
            }
            else {
                return null;
            }
            // participant can't be paid before the payment date
            if (paymentDate < new Date()) {
                return participant;
            }
            const rawDid = conversion_1.stringToIdentityId(did, context);
            const rawCaId = conversion_1.corporateActionIdentifierToCaId({ ticker, localId }, context);
            const holderPaid = yield query.capitalDistribution.holderPaid([rawCaId, rawDid]);
            const paid = conversion_1.boolToBoolean(holderPaid);
            return Object.assign(Object.assign({}, participant), { paid });
        });
    }
    /**
     * @hidden
     */
    fetchDistribution() {
        const { token: { ticker }, id, context, } = this;
        return context.polymeshApi.query.capitalDistribution.distributions(conversion_1.corporateActionIdentifierToCaId({ ticker, localId: id }, context));
    }
    /**
     * Retrieve the amount of taxes that have been withheld up to this point in this Distribution
     *
     * @note uses the middleware
     */
    getWithheldTax() {
        return __awaiter(this, void 0, void 0, function* () {
            const { id, token: { ticker }, context, } = this;
            const taxPromise = context.queryMiddleware(queries_1.getWithholdingTaxesOfCa({
                // eslint-disable-next-line @typescript-eslint/naming-convention
                CAId: { ticker, localId: id.toNumber() },
            }));
            const [exists, result] = yield Promise.all([this.exists(), taxPromise]);
            if (!exists) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.DataUnavailable,
                    message: notExistsMessage,
                });
            }
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const { taxes } = result.data.getWithholdingTaxesOfCA;
            return new bignumber_js_1.default(taxes);
        });
    }
    /**
     * Retrieve the payment history for this Distribution
     *
     * @note uses the middleware
     * @note supports pagination
     */
    getPaymentHistory(opts = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            const { id, token: { ticker }, context, context: { polymeshApi: { query: { system }, }, }, } = this;
            const { size, start } = opts;
            const paymentsPromise = context.queryMiddleware(queries_1.getHistoryOfPaymentEventsForCa({
                CAId: { ticker, localId: id.toNumber() },
                fromDate: null,
                toDate: null,
                count: size,
                skip: start,
            }));
            const [exists, result] = yield Promise.all([this.exists(), paymentsPromise]);
            if (!exists) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.DataUnavailable,
                    message: notExistsMessage,
                });
            }
            const { data: { getHistoryOfPaymentEventsForCA: getHistoryOfPaymentEventsForCaResult }, } = result;
            const { items, totalCount: count } = getHistoryOfPaymentEventsForCaResult;
            const data = [];
            const multiParams = [];
            /* eslint-disable @typescript-eslint/no-non-null-assertion */
            items.forEach(item => {
                const { blockId, datetime, eventDid: did, balance, tax } = item;
                multiParams.push(conversion_1.numberToU32(blockId, context));
                data.push({
                    blockNumber: new bignumber_js_1.default(blockId),
                    date: new Date(datetime),
                    target: new internal_1.Identity({ did }, context),
                    amount: new bignumber_js_1.default(balance),
                    withheldTax: new bignumber_js_1.default(tax),
                });
            });
            /* eslint-enable @typescript-eslint/no-non-null-assertion */
            const next = internal_2.calculateNextKey(count, size, start);
            let hashes = [];
            if (multiParams.length) {
                hashes = yield system.blockHash.multi(multiParams);
            }
            return {
                data: data.map((payment, index) => (Object.assign(Object.assign({}, payment), { blockHash: conversion_1.hashToString(hashes[index]) }))),
                next,
                count,
            };
        });
    }
    /**
     * @hidden
     */
    getParticipantStatuses(participants) {
        return __awaiter(this, void 0, void 0, function* () {
            const { token: { ticker }, id: localId, context: { polymeshApi: { query: { capitalDistribution }, }, }, context, } = this;
            /*
             * For optimization, we separate the participants into chunks that can fit into one multi call
             * and then sequentially perform bunches of said multi requests in parallel
             */
            const participantChunks = lodash_1.chunk(participants, constants_1.MAX_PAGE_SIZE);
            const parallelCallChunks = lodash_1.chunk(participantChunks, constants_1.MAX_CONCURRENT_REQUESTS);
            let paidStatuses = [];
            const caId = conversion_1.corporateActionIdentifierToCaId({ localId, ticker }, context);
            yield bluebird_1.default.each(parallelCallChunks, (callChunk) => __awaiter(this, void 0, void 0, function* () {
                const parallelMultiCalls = callChunk.map(participantChunk => {
                    const multiParams = participantChunk.map(({ identity: { did } }) => utils_1.tuple(caId, conversion_1.stringToIdentityId(did, context)));
                    return capitalDistribution.holderPaid.multi(multiParams);
                });
                const results = yield Promise.all(parallelMultiCalls);
                paidStatuses = [...paidStatuses, ...lodash_1.flatten(results).map(paid => conversion_1.boolToBoolean(paid))];
            }));
            return paidStatuses;
        });
    }
    /**
     * Return the Dividend Distribution's static data
     */
    toJson() {
        const { origin, currency, perShare, maxAmount, expiryDate, paymentDate } = this;
        const parentReadable = super.toJson();
        return Object.assign(Object.assign({}, internal_2.toHumanReadable({ origin, currency, perShare, maxAmount, expiryDate, paymentDate })), parentReadable);
    }
}
exports.DividendDistribution = DividendDistribution;
//# sourceMappingURL=index.js.map