/*

  Copyright 2019 ZeroEx Intl.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

*/

pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;

import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";


contract NativeOrderSampler {
    using LibSafeMath for uint256;
    using LibBytes for bytes;

    /// @dev The Exchange ERC20Proxy ID.
    bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0;
    /// @dev Gas limit for calls to `getOrderFillableTakerAmount()`.
    uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k

    function getTokenDecimals(
        address makerTokenAddress,
        address takerTokenAddress
    )
        public
        view
        returns (uint256, uint256)
    {
        uint256 fromTokenDecimals = LibERC20Token.decimals(makerTokenAddress);
        uint256 toTokenDecimals = LibERC20Token.decimals(takerTokenAddress);
        return (fromTokenDecimals, toTokenDecimals);
    }

    /// @dev Queries the fillable taker asset amounts of native orders.
    ///      Effectively ignores orders that have empty signatures or
    ///      maker/taker asset amounts (returning 0).
    /// @param orders Native orders to query.
    /// @param orderSignatures Signatures for each respective order in `orders`.
    /// @param exchange The V3 exchange.
    /// @return orderFillableTakerAssetAmounts How much taker asset can be filled
    ///         by each order in `orders`.
    function getOrderFillableTakerAssetAmounts(
        LibOrder.Order[] memory orders,
        bytes[] memory orderSignatures,
        IExchange exchange
    )
        public
        view
        returns (uint256[] memory orderFillableTakerAssetAmounts)
    {
        orderFillableTakerAssetAmounts = new uint256[](orders.length);
        for (uint256 i = 0; i != orders.length; i++) {
            // solhint-disable indent
            (bool didSucceed, bytes memory resultData) =
                address(this)
                    .staticcall
                    .gas(DEFAULT_CALL_GAS)
                    (abi.encodeWithSelector(
                       this.getOrderFillableTakerAmount.selector,
                       orders[i],
                       orderSignatures[i],
                       exchange
                    ));
            // solhint-enable indent
            orderFillableTakerAssetAmounts[i] = didSucceed
                ? abi.decode(resultData, (uint256))
                : 0;
        }
    }

    /// @dev Queries the fillable taker asset amounts of native orders.
    ///      Effectively ignores orders that have empty signatures or
    /// @param orders Native orders to query.
    /// @param orderSignatures Signatures for each respective order in `orders`.
    /// @param exchange The V3 exchange.
    /// @return orderFillableMakerAssetAmounts How much maker asset can be filled
    ///         by each order in `orders`.
    function getOrderFillableMakerAssetAmounts(
        LibOrder.Order[] memory orders,
        bytes[] memory orderSignatures,
        IExchange exchange
    )
        public
        view
        returns (uint256[] memory orderFillableMakerAssetAmounts)
    {
        orderFillableMakerAssetAmounts = getOrderFillableTakerAssetAmounts(
            orders,
            orderSignatures,
            exchange
        );
        // `orderFillableMakerAssetAmounts` now holds taker asset amounts, so
        // convert them to maker asset amounts.
        for (uint256 i = 0; i < orders.length; ++i) {
            if (orderFillableMakerAssetAmounts[i] != 0) {
                orderFillableMakerAssetAmounts[i] = LibMath.getPartialAmountCeil(
                    orderFillableMakerAssetAmounts[i],
                    orders[i].takerAssetAmount,
                    orders[i].makerAssetAmount
                );
            }
        }
    }

    /// @dev Get the fillable taker amount of an order, taking into account
    ///      order state, maker fees, and maker balances.
    function getOrderFillableTakerAmount(
        LibOrder.Order memory order,
        bytes memory signature,
        IExchange exchange
    )
        public
        view
        returns (uint256 fillableTakerAmount)
    {
        if (signature.length == 0 ||
            order.makerAssetAmount == 0 ||
            order.takerAssetAmount == 0)
        {
            return 0;
        }

        LibOrder.OrderInfo memory orderInfo = exchange.getOrderInfo(order);
        if (orderInfo.orderStatus != LibOrder.OrderStatus.FILLABLE) {
            return 0;
        }
        if (!exchange.isValidHashSignature(orderInfo.orderHash, order.makerAddress, signature)) {
            return 0;
        }
        address spender = exchange.getAssetProxy(ERC20_ASSET_PROXY_ID);
        IERC20Token makerToken = _getTokenFromERC20AssetData(order.makerAssetData);
        if (makerToken == IERC20Token(0)) {
            return 0;
        }
        IERC20Token makerFeeToken = order.makerFee > 0
            ? _getTokenFromERC20AssetData(order.makerFeeAssetData)
            : IERC20Token(0);
        uint256 remainingTakerAmount = order.takerAssetAmount
            .safeSub(orderInfo.orderTakerAssetFilledAmount);
        fillableTakerAmount = remainingTakerAmount;
        // The total fillable maker amount is the remaining fillable maker amount
        // PLUS maker fees, if maker fees are denominated in the maker token.
        uint256 totalFillableMakerAmount = LibMath.safeGetPartialAmountFloor(
            remainingTakerAmount,
            order.takerAssetAmount,
            makerFeeToken == makerToken
                ? order.makerAssetAmount.safeAdd(order.makerFee)
                : order.makerAssetAmount
        );
        // The spendable amount of maker tokens (by the maker) is the lesser of
        // the maker's balance and the allowance they've granted to the ERC20Proxy.
        uint256 spendableMakerAmount = LibSafeMath.min256(
            makerToken.balanceOf(order.makerAddress),
            makerToken.allowance(order.makerAddress, spender)
        );
        // Scale the fillable taker amount by the ratio of the maker's
        // spendable maker amount over the total fillable maker amount.
        if (spendableMakerAmount < totalFillableMakerAmount) {
            fillableTakerAmount = LibMath.getPartialAmountCeil(
                spendableMakerAmount,
                totalFillableMakerAmount,
                remainingTakerAmount
            );
        }
        // If the maker fee is denominated in another token, constrain
        // the fillable taker amount by how much the maker can pay of that token.
        if (makerFeeToken != makerToken && makerFeeToken != IERC20Token(0)) {
            uint256 spendableExtraMakerFeeAmount = LibSafeMath.min256(
                makerFeeToken.balanceOf(order.makerAddress),
                makerFeeToken.allowance(order.makerAddress, spender)
            );
            if (spendableExtraMakerFeeAmount < order.makerFee) {
                fillableTakerAmount = LibSafeMath.min256(
                    fillableTakerAmount,
                    LibMath.getPartialAmountCeil(
                        spendableExtraMakerFeeAmount,
                        order.makerFee,
                        remainingTakerAmount
                    )
                );
            }
        }
    }

    function _getTokenFromERC20AssetData(bytes memory assetData)
        private
        pure
        returns (IERC20Token token)
    {
        if (assetData.length == 0) {
            return IERC20Token(address(0));
        }
        if (assetData.length != 36 ||
            assetData.readBytes4(0) != ERC20_ASSET_PROXY_ID)
        {
            return IERC20Token(address(0));
        }
        return IERC20Token(assetData.readAddress(16));
    }
}
