// SPDX-License-Identifier: AGPLv3
pragma solidity 0.7.6;
pragma experimental ABIEncoderV2;

import {
    ISuperfluidGovernance,
    ISuperfluid,
    ISuperfluidToken,
    ISuperApp,
    SuperAppDefinitions,
    ContextDefinitions
} from "../interfaces/superfluid/ISuperfluid.sol";
import { ISuperfluidToken } from "../interfaces/superfluid/ISuperfluidToken.sol";

import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol";


/**
 * @dev Helper library for building super agreement
 */
library AgreementLibrary {

    using SignedSafeMath for int256;
    using SafeCast for uint256;
    using SafeCast for int256;

    /**************************************************************************
     * Context helpers
     *************************************************************************/

    /**
     * @dev Authorize the msg.sender to access token agreement storage
     *
     * NOTE:
     * - msg.sender must be the expected host contract.
     * - it should revert on unauthorized access.
     */
    function authorizeTokenAccess(ISuperfluidToken token, bytes memory ctx)
        internal view
        returns (ISuperfluid.Context memory)
    {
        require(token.getHost() == msg.sender, "AgreementLibrary: unauthroized host");
        // ruleid: superfluid-ctx-injection
        return ISuperfluid(msg.sender).decodeCtx(ctx);
    }

    /**************************************************************************
     * Agreement callback helpers
     *************************************************************************/

    struct CallbackInputs {
        ISuperfluidToken token;
        address account;
        bytes32 agreementId;
        bytes agreementData;
        uint256 appAllowanceGranted;
        int256 appAllowanceUsed;
        uint256 noopBit;
    }

    function createCallbackInputs(
        ISuperfluidToken token,
        address account,
        bytes32 agreementId,
        bytes memory agreementData
    )
       internal pure
       returns (CallbackInputs memory inputs)
    {
        inputs.token = token;
        inputs.account = account;
        inputs.agreementId = agreementId;
        inputs.agreementData = agreementData;
    }

    function callAppBeforeCallback(
        CallbackInputs memory inputs,
        bytes memory ctx
    )
        internal
        returns(bytes memory cbdata)
    {
        bool isSuperApp;
        bool isJailed;
        uint256 noopMask;
        (isSuperApp, isJailed, noopMask) = ISuperfluid(msg.sender).getAppManifest(ISuperApp(inputs.account));
        if (isSuperApp && !isJailed) {
            bytes memory appCtx = _pushCallbackStack(ctx, inputs);
            if ((noopMask & inputs.noopBit) == 0) {
                bytes memory callData = abi.encodeWithSelector(
                    _selectorFromNoopBit(inputs.noopBit),
                    inputs.token,
                    address(this) /* agreementClass */,
                    inputs.agreementId,
                    inputs.agreementData,
                    new bytes(0) // placeholder ctx
                );
                cbdata = ISuperfluid(msg.sender).callAppBeforeCallback(
                    ISuperApp(inputs.account),
                    callData,
                    inputs.noopBit == SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP,
                    appCtx);
            }
            _popCallbackStack(ctx, 0);
        }
    }

    function callAppAfterCallback(
        CallbackInputs memory inputs,
        bytes memory cbdata,
        bytes memory ctx
    )
        internal
        returns (ISuperfluid.Context memory appContext, bytes memory newCtx)
    {
        bool isSuperApp;
        bool isJailed;
        uint256 noopMask;
        (isSuperApp, isJailed, noopMask) = ISuperfluid(msg.sender).getAppManifest(ISuperApp(inputs.account));

        if (isSuperApp && !isJailed) {
            newCtx = _pushCallbackStack(ctx, inputs);
            if ((noopMask & inputs.noopBit) == 0) {
                bytes memory callData = abi.encodeWithSelector(
                    _selectorFromNoopBit(inputs.noopBit),
                    inputs.token,
                    address(this) /* agreementClass */,
                    inputs.agreementId,
                    inputs.agreementData,
                    cbdata,
                    new bytes(0) // placeholder ctx
                );
                newCtx = ISuperfluid(msg.sender).callAppAfterCallback(
                    ISuperApp(inputs.account),
                    callData,
                    inputs.noopBit == SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP,
                    newCtx);

                appContext = ISuperfluid(msg.sender).decodeCtx(newCtx);

                // adjust allowance used to the range [appAllowanceWanted..appAllowanceGranted]
                appContext.appAllowanceUsed = max(0, min(
                    inputs.appAllowanceGranted.toInt256(),
                    max(appContext.appAllowanceWanted.toInt256(), appContext.appAllowanceUsed)));

            }
            newCtx = _popCallbackStack(ctx, appContext.appAllowanceUsed);
        }
    }

    function _selectorFromNoopBit(uint256 noopBit)
        private pure
        returns (bytes4 selector)
    {
        if (noopBit == SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP) {
            return ISuperApp.beforeAgreementCreated.selector;
        } else if (noopBit == SuperAppDefinitions.BEFORE_AGREEMENT_UPDATED_NOOP) {
            return ISuperApp.beforeAgreementUpdated.selector;
        } else if (noopBit == SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP) {
            return ISuperApp.beforeAgreementTerminated.selector;
        } else if (noopBit == SuperAppDefinitions.AFTER_AGREEMENT_CREATED_NOOP) {
            return ISuperApp.afterAgreementCreated.selector;
        } else if (noopBit == SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP) {
            return ISuperApp.afterAgreementUpdated.selector;
        } else /* if (noopBit == SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP) */ {
            return ISuperApp.afterAgreementTerminated.selector;
        }
    }

    function _pushCallbackStack(
        bytes memory ctx,
        CallbackInputs memory inputs
    )
        private
        returns (bytes memory appCtx)
    {
        // app allowance params stack PUSH
        // pass app allowance and current allowance used to the app,
        appCtx = ISuperfluid(msg.sender).appCallbackPush(
            ctx,
            ISuperApp(inputs.account),
            inputs.appAllowanceGranted,
            inputs.appAllowanceUsed,
            inputs.token);
    }

    function _popCallbackStack(
        bytes memory ctx,
        int256 appAllowanceUsedDelta
    )
        private
        returns (bytes memory newCtx)
    {
        // app allowance params stack POP
        return ISuperfluid(msg.sender).appCallbackPop(ctx, appAllowanceUsedDelta);
    }

    /**************************************************************************
     * Misc
     *************************************************************************/

    function max(int256 a, int256 b) internal pure returns (int256) { return a > b ? a : b; }

    function min(int256 a, int256 b) internal pure returns (int256) { return a > b ? b : a; }
}