library Activity {
    using RunTime for RunTime.Context;
    using Order for Order.Info;
    using Activity for *;

    struct ActivityArg {
        Activity.Type activityType;
        bytes encodedFields;
    }

    enum Type {Trade, Loan, Liquidation}
    enum TradeMovementType {None, DepositAll, WithdrawAll}

    struct Trade {
        uint orderIndex;
        uint marketIdS;
        uint marketIdB;
        uint fillAmountS;
        TradeMovementType movementType;
    }

    struct Loan {
        // TODO
        uint orderIndex;
    }

    struct Liquidation {
        uint orderIndex;
        uint marketIdS;
        uint marketIdB;
        uint fillAmountB;
        address profitReceiver;
        address liquidOwner;
        uint liquidPositionId;
    }

    function depositBalanceHash() internal pure returns (bytes32) {
        return bytes32(0x1234567812345678123456781234567812345678123456781234567812345678);
    }

    function registerActivity(RunTime.Context memory ctx, Activity.Type activityType, bytes memory encodedFields) internal view {
        if (activityType == Type.Trade) encodedFields._decodeTradeActivity()._generateTradeActions(ctx);
        else if (activityType == Type.Loan) encodedFields._decodeLoanActivity()._generateLoanActions(ctx);
        else if (activityType == Type.Liquidation) encodedFields._decodeLiquidationActivity()._generateLiquidationActions(ctx);
    }

    // ---------------------------------

    function _decodeTradeActivity(bytes memory encoded) internal pure returns (Activity.Trade memory trade) {
        return abi.decode(encoded, (Activity.Trade));
    }

    function _decodeLoanActivity(bytes memory encoded) internal pure returns (Activity.Loan memory loan) {
        revert("NOT_IMPLEMENTED: Activity.Type.Loan is not yet supported");
    }

    function _decodeLiquidationActivity(bytes memory encoded) internal pure returns (Activity.Liquidation memory liquidation) {
        return abi.decode(encoded, (Activity.Liquidation));
    }

    // ---------------------------------

    function _generateTradeActions(Activity.Trade memory trade, RunTime.Context memory ctx) internal view {
        Order.Info memory order = ctx.orders[trade.orderIndex];
        Order.TradeInfo memory tradeInfo = order.tradeInfo(ctx);

        uint positionIndex = ctx.registerPosition(tradeInfo.trader, tradeInfo.positionId);

        // Requirements for deposit of trader's collateral
        if (trade.movementType == Activity.TradeMovementType.DepositAll) {
            address depositToken = ctx.marketTokenAddress[tradeInfo.depositMarketId];
            ctx.requireTokenTransfer(tradeInfo.signer, ctx.self, depositToken, tradeInfo.depositAmount, tradeInfo.isUsingDepositContract);
            ctx.addBeforeAction(DydxActionBuilder.Deposit(positionIndex, tradeInfo.depositMarketId, tradeInfo.depositAmount, ctx.self));
            ctx.setDepositFlag(order.orderHash);
        }

        // Actions for borrowing tokens to exchange and depositing what was received
        ctx.addBeforeAction(DydxActionBuilder.Withdraw(positionIndex, trade.marketIdS, trade.fillAmountS, ctx.self));
        ctx.addAfterAction(DydxActionBuilder.DepositAll({
            positionIndex : positionIndex,
            marketId : trade.marketIdB,
            burnMarketId : trade.marketIdS,
            controller : ctx.self,
            orderHash : order.orderHash
            }));

        // Actions to withdraw the position's balances to the trader
        if (trade.movementType == Activity.TradeMovementType.WithdrawAll) {
            ctx.addAfterAction(DydxActionBuilder.WithdrawAll(positionIndex, trade.marketIdS, tradeInfo.trader));
            ctx.addAfterAction(DydxActionBuilder.WithdrawAll(positionIndex, trade.marketIdB, tradeInfo.trader));
        }

        // Action for setting the expiration timestamp of the position
        if (trade.movementType == Activity.TradeMovementType.DepositAll && tradeInfo.expirationDays > 0) {
            uint expirationTime = block.timestamp + (tradeInfo.expirationDays * 60 * 60 * 24);
            ctx.addAfterAction(DydxActionBuilder.SetExpiry(positionIndex, ctx.dydxExpiryContractAddress, trade.marketIdS, expirationTime));
        }
    }

    function _generateLoanActions(Activity.Loan memory trade, RunTime.Context memory ctx) internal pure {/* TODO */}

    function _generateLiquidationActions(Activity.Liquidation memory liquidation, RunTime.Context memory ctx) internal view {
        // For liquidations, `primaryMarketId` is the owed market. `secondaryMarketId` is the market whose positive
        // balance will be earned (plus the 5% spread). We buy into `marketIdB` using the collateral earned from
        // `marketIdS`. In doing so, we will take on the debt owed by the liquid account and then sell off any positive
        // balance (which is in secondaryMarketId) into the owed market (primaryMarketId), to clear out any debt.
        // Whatever is left from this process is profit for the liquidator.
        Order.Info memory order = ctx.orders[liquidation.orderIndex];

        uint solidPositionTradeIndex = ctx.registerPosition(order.signer, 0);
        uint solidPositionProfitIndex = ctx.registerPosition(liquidation.profitReceiver, 0);
        uint liquidPositionIndex = ctx.registerPosition(liquidation.liquidOwner, liquidation.liquidPositionId);

        DydxActions.ActionArgs memory liquidateAction;
        liquidateAction.actionType = DydxActions.ActionType.Liquidate;
        liquidateAction.accountId = solidPositionTradeIndex;
        liquidateAction.otherAccountId = liquidPositionIndex;
        liquidateAction.primaryMarketId = liquidation.marketIdB;
        liquidateAction.secondaryMarketId = liquidation.marketIdS;
        if (liquidation.fillAmountB == uint(- 1)) {
            liquidateAction.amount = DydxTypes.AssetAmount({
                sign : true,
                denomination : DydxTypes.AssetDenomination.Wei,
                ref : DydxTypes.AssetReference.Target,
                value : 0
                });
        } else {
            liquidateAction.amount = DydxTypes.AssetAmount({
                sign : true,
                denomination : DydxTypes.AssetDenomination.Wei,
                ref : DydxTypes.AssetReference.Delta,
                value : liquidation.fillAmountB
                });
        }
        ctx.addBeforeAction(liquidateAction);

        // Sell off the full positive balance (`marketIdS`) that was received from the liquidation.
        ctx.addBeforeAction(DydxActionBuilder.WithdrawAll(solidPositionTradeIndex, liquidation.marketIdS, ctx.self));

        // Deposit all received tokens from the sell (of marketIdS into marketIdB) into the account. This should
        // make our negative balance positive. The new positive balance is now profit for the solid account.
        ctx.addAfterAction(DydxActionBuilder.DepositAll({
            positionIndex : solidPositionTradeIndex,
            marketId : liquidation.marketIdB,
            burnMarketId : liquidation.marketIdS,
            controller : ctx.self,
            orderHash : order.orderHash
            }));

        // Move the funds from trade index to the profit-taking one. This way, we only move positive balances into this
        // account.
        DydxActions.ActionArgs memory transferAction;
        transferAction.actionType = DydxActions.ActionType.Transfer;
        transferAction.accountId = solidPositionTradeIndex;
        transferAction.otherAccountId = solidPositionProfitIndex;
        transferAction.primaryMarketId = liquidation.marketIdB;
        transferAction.amount = DydxTypes.AssetAmount({
            sign : true,
            denomination : DydxTypes.AssetDenomination.Wei,
            ref : DydxTypes.AssetReference.Target,
            value : 0
            });
        ctx.addAfterAction(transferAction);
    }
}
function _generateTradeActions(Activity.Trade memory trade, RunTime.Context memory ctx) internal view {
    Order.Info memory order = ctx.orders[trade.orderIndex];
    Order.TradeInfo memory tradeInfo = order.tradeInfo(ctx);

    uint positionIndex = ctx.registerPosition(tradeInfo.trader, tradeInfo.positionId);

    // Requirements for deposit of trader's collateral
    if (trade.movementType == Activity.TradeMovementType.DepositAll) {
        address depositToken = ctx.marketTokenAddress[tradeInfo.depositMarketId];
        ctx.requireTokenTransfer(tradeInfo.signer, ctx.self, depositToken, tradeInfo.depositAmount, tradeInfo.isUsingDepositContract);
        ctx.addBeforeAction(DydxActionBuilder.Deposit(positionIndex, tradeInfo.depositMarketId, tradeInfo.depositAmount, ctx.self));
        ctx.setDepositFlag(order.orderHash);
    }

    // Actions for borrowing tokens to exchange and depositing what was received
    ctx.addBeforeAction(DydxActionBuilder.Withdraw(positionIndex, trade.marketIdS, trade.fillAmountS, ctx.self));
    ctx.addAfterAction(DydxActionBuilder.DepositAll({
        positionIndex : positionIndex,
        marketId : trade.marketIdB,
        burnMarketId : trade.marketIdS,
        controller : ctx.self,
        orderHash : order.orderHash
        }));

    // Actions to withdraw the position's balances to the trader
    if (trade.movementType == Activity.TradeMovementType.WithdrawAll) {
        ctx.addAfterAction(DydxActionBuilder.WithdrawAll(positionIndex, trade.marketIdS, tradeInfo.trader));
        ctx.addAfterAction(DydxActionBuilder.WithdrawAll(positionIndex, trade.marketIdB, tradeInfo.trader));
    }

    // Action for setting the expiration timestamp of the position
    if (trade.movementType == Activity.TradeMovementType.DepositAll && tradeInfo.expirationDays > 0) {
        uint expirationTime = block.timestamp + (tradeInfo.expirationDays * 60 * 60 * 24);
        ctx.addAfterAction(DydxActionBuilder.SetExpiry(positionIndex, ctx.dydxExpiryContractAddress, trade.marketIdS, expirationTime));
    }
}

// ---------------------------------