pragma solidity ^0.6.0;

import "../DS/DSMath.sol";
import "../interfaces/TokenInterface.sol";
import "../interfaces/ExchangeInterfaceV2.sol";
import "./SaverExchangeHelper.sol";

contract Prices is DSMath {

    address public constant KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    enum ActionType { SELL, BUY }


    /// @notice Returns the best estimated price from 2 exchanges
    /// @param _amount Amount of source tokens you want to exchange
    /// @param _srcToken Address of the source token
    /// @param _destToken Address of the destination token
    /// @param _type Type of action SELL|BUY
    /// @param _wrappers Array of wrapper addresses to compare
    /// @return (address, uint) The address of the best exchange and the exchange price
    function getBestPrice(
        uint256 _amount,
        address _srcToken,
        address _destToken,
        ActionType _type,
        address[] memory _wrappers
    ) public returns (address, uint256) {

        uint256[] memory rates = new uint256[](_wrappers.length);
        for (uint i=0; i<_wrappers.length; i++) {
            rates[i] = getExpectedRate(_wrappers[i], _srcToken, _destToken, _amount, _type);
        }

        if (_type == ActionType.SELL) {
            return getBiggestRate(_wrappers, rates);
        } else {
            return getSmallestRate(_wrappers, rates);
        }
    }

    /// @notice Return the expected rate from the exchange wrapper
    /// @dev In case of Oasis/Uniswap handles the different precision tokens
    /// @param _wrapper Address of exchange wrapper
    /// @param _srcToken From token
    /// @param _destToken To token
    /// @param _amount Amount to be exchanged
    /// @param _type Type of action SELL|BUY
    function getExpectedRate(
        address _wrapper,
        address _srcToken,
        address _destToken,
        uint256 _amount,
        ActionType _type
    ) public returns (uint256) {
        bool success;
        bytes memory result;

        if (_type == ActionType.SELL) {
            (success, result) = _wrapper.call(abi.encodeWithSignature(
                "getSellRate(address,address,uint256)",
                _srcToken,
                _destToken,
                _amount
            ));

        } else {
            (success, result) = _wrapper.call(abi.encodeWithSignature(
                "getBuyRate(address,address,uint256)",
                _srcToken,
                _destToken,
                _amount
            ));
        }

        if (success) {
            return sliceUint(result, 0);
        }

        return 0;
    }

    /// @notice Finds the biggest rate between exchanges, needed for sell rate
    /// @param _wrappers Array of wrappers to compare
    /// @param _rates Array of rates to compare
    function getBiggestRate(
        address[] memory _wrappers,
        uint256[] memory _rates
    ) internal pure returns (address, uint) {
        uint256 maxIndex = 0;

        // starting from 0 in case there is only one rate in array
        for (uint256 i=0; i<_rates.length; i++) {
            if (_rates[i] > _rates[maxIndex]) {
                maxIndex = i;
            }
        }

        return (_wrappers[maxIndex], _rates[maxIndex]);
    }

    /// @notice Finds the smallest rate between exchanges, needed for buy rate
    /// @param _wrappers Array of wrappers to compare
    /// @param _rates Array of rates to compare
    function getSmallestRate(
        address[] memory _wrappers,
        uint256[] memory _rates
    ) internal pure returns (address, uint) {
        uint256 minIndex = 0;

        // starting from 0 in case there is only one rate in array
        for (uint256 i=0; i<_rates.length; i++) {
            if ((_rates[i] < _rates[minIndex] && _rates[i] > 0) || _rates[minIndex] == 0) {
                minIndex = i;
            }
        }

        return (_wrappers[minIndex], _rates[minIndex]);
    }

    function getDecimals(address _token) internal view returns (uint256) {
        if (_token == KYBER_ETH_ADDRESS) return 18;

        return ERC20(_token).decimals();
    }

    function sliceUint(bytes memory bs, uint256 start) internal pure returns (uint256) {
        require(bs.length >= start + 32, "slicing out of range");

        uint256 x;
        assembly {
            x := mload(add(bs, add(0x20, start)))
        }

        return x;
    }
}