contract TokenWhitelist is ENSResolvable, Controllable, Transferrable {
    using strings for *;
    using SafeMath for uint256;
    using BytesUtils for bytes;

    event UpdatedTokenRate(address _sender, address _token, uint _rate);

    event UpdatedTokenLoadable(address _sender, address _token, bool _loadable);
    event UpdatedTokenRedeemable(address _sender, address _token, bool _redeemable);

    event AddedToken(address _sender, address _token, string _symbol, uint _magnitude, bool _loadable, bool _redeemable);
    event RemovedToken(address _sender, address _token);

    event AddedMethodId(bytes4 _methodId);
    event RemovedMethodId(bytes4 _methodId);
    event AddedExclusiveMethod(address _token, bytes4 _methodId);
    event RemovedExclusiveMethod(address _token, bytes4 _methodId);

    event Claimed(address _to, address _asset, uint _amount);

    /// @dev these are the methods whitelisted by default in executeTransaction() for protected tokens
    bytes4 private constant _APPROVE = 0x095ea7b3; // keccak256(approve(address,uint256)) => 0x095ea7b3
    bytes4 private constant _BURN = 0x42966c68; // keccak256(burn(uint256)) => 0x42966c68
    bytes4 private constant _TRANSFER= 0xa9059cbb; // keccak256(transfer(address,uint256)) => 0xa9059cbb
    bytes4 private constant _TRANSFER_FROM = 0x23b872dd; // keccak256(transferFrom(address,address,uint256)) => 0x23b872dd

    struct Token {
        string symbol;    // Token symbol
        uint magnitude;   // 10^decimals
        uint rate;        // Token exchange rate in wei
        bool available;   // Flags if the token is available or not
        bool loadable;    // Flags if token is loadable to the TokenCard
        bool redeemable;    // Flags if token is redeemable in the TKN Holder contract
        uint lastUpdate;  // Time of the last rate update
    }

    mapping(address => Token) private _tokenInfoMap;

    // @notice specifies whitelisted methodIds for protected tokens in wallet's excuteTranaction() e.g. keccak256(transfer(address,uint256)) => 0xa9059cbb
    mapping(bytes4 => bool) private _methodIdWhitelist;

    address[] private _tokenAddressArray;

    /// @notice keeping track of how many redeemable tokens are in the tokenWhitelist
    uint private _redeemableCounter;

    /// @notice Address of the stablecoin.
    address private _stablecoin;

    /// @notice is registered ENS node identifying the oracle contract.
    bytes32 private _oracleNode;

    /// @notice Constructor initializes ENSResolvable, and Controllable.
    /// @param _ens_ is the ENS registry address.
    /// @param _oracleNode_ is the ENS node of the Oracle.
    /// @param _controllerNode_ is our Controllers node.
    /// @param _stablecoinAddress_ is the address of the stablecoint used by the wallet for the card load limit.
    constructor(address _ens_, bytes32 _oracleNode_, bytes32 _controllerNode_, address _stablecoinAddress_) ENSResolvable(_ens_) Controllable(_controllerNode_) public {
        _oracleNode = _oracleNode_;
        _stablecoin = _stablecoinAddress_;
        //a priori ERC20 whitelisted methods
        _methodIdWhitelist[_APPROVE] = true;
        _methodIdWhitelist[_BURN] = true;
        _methodIdWhitelist[_TRANSFER] = true;
        _methodIdWhitelist[_TRANSFER_FROM] = true;
    }

    modifier onlyAdminOrOracle() {
        address oracleAddress = _ensResolve(_oracleNode);
        require (_isAdmin(msg.sender) || msg.sender == oracleAddress, "either oracle or admin");
        _;
    }

    /// @notice Add ERC20 tokens to the list of whitelisted tokens.
    /// @param _tokens ERC20 token contract addresses.
    /// @param _symbols ERC20 token names.
    /// @param _magnitude 10 to the power of number of decimal places used by each ERC20 token.
    /// @param _loadable is a bool that states whether or not a token is loadable to the TokenCard.
    /// @param _redeemable is a bool that states whether or not a token is redeemable in the TKN Holder Contract.
    /// @param _lastUpdate is a unit representing an ISO datetime e.g. 20180913153211.
    function addTokens(address[] calldata _tokens, bytes32[] calldata _symbols, uint[] calldata _magnitude, bool[] calldata _loadable, bool[] calldata _redeemable, uint _lastUpdate) external onlyAdmin {
        // Require that all parameters have the same length.
        require(_tokens.length == _symbols.length && _tokens.length == _magnitude.length && _tokens.length == _loadable.length && _tokens.length == _loadable.length, "parameter lengths do not match");
        // Add each token to the list of supported tokens.
        for (uint i = 0; i < _tokens.length; i++) {
            // Require that the token isn't already available.
            require(!_tokenInfoMap[_tokens[i]].available, "token already available");
            // Store the intermediate values.
            string memory symbol = _symbols[i].toSliceB32().toString();
            // Add the token to the token list.
            _tokenInfoMap[_tokens[i]] = Token({
                symbol : symbol,
                magnitude : _magnitude[i],
                rate : 0,
                available : true,
                loadable : _loadable[i],
                redeemable: _redeemable[i],
                lastUpdate : _lastUpdate
                });
            // Add the token address to the address list.
            _tokenAddressArray.push(_tokens[i]);
            //if the token is redeemable increase the redeemableCounter
            if (_redeemable[i]){
                _redeemableCounter = _redeemableCounter.add(1);
            }
            // Emit token addition event.
            emit AddedToken(msg.sender, _tokens[i], symbol, _magnitude[i], _loadable[i], _redeemable[i]);
        }
    }

    /// @notice Remove ERC20 tokens from the whitelist of tokens.
    /// @param _tokens ERC20 token contract addresses.
    function removeTokens(address[] calldata _tokens) external onlyAdmin {
        // Delete each token object from the list of supported tokens based on the addresses provided.
        for (uint i = 0; i < _tokens.length; i++) {
            // Store the token address.
            address token = _tokens[i];
            //token must be available, reverts on duplicates as well
            require(_tokenInfoMap[token].available, "token is not available");
            //if the token is redeemable decrease the redeemableCounter
            if (_tokenInfoMap[token].redeemable){
                _redeemableCounter = _redeemableCounter.sub(1);
            }
            // Delete the token object.
            delete _tokenInfoMap[token];
            // Remove the token address from the address list.
            for (uint j = 0; j < _tokenAddressArray.length.sub(1); j++) {
                if (_tokenAddressArray[j] == token) {
                    _tokenAddressArray[j] = _tokenAddressArray[_tokenAddressArray.length.sub(1)];
                    break;
                }
            }
            _tokenAddressArray.length--;
            // Emit token removal event.
            emit RemovedToken(msg.sender, token);
        }
    }

    /// @notice based on the method it returns the recipient address and amount/value, ERC20 specific.
    /// @param _data is the transaction payload.
    function getERC20RecipientAndAmount(address _token, bytes calldata _data) external view returns (address, uint) {
        // Require that there exist enough bytes for encoding at least a method signature + data in the transaction payload:
        // 4 (signature)  + 32(address or uint256)
        require(_data.length >= 4 + 32, "not enough method-encoding bytes");
        // Get the method signature
        bytes4 signature = _data._bytesToBytes4(0);
        // Check if method Id is supported
        require(isERC20MethodSupported(_token, signature), "unsupported method");
        // returns the recipient's address and amount is the value to be transferred
        if (signature == _BURN) {
            // 4 (signature) + 32(uint256)
            return (_token, _data._bytesToUint256(4));
        } else if (signature == _TRANSFER_FROM) {
            // 4 (signature) + 32(address) + 32(address) + 32(uint256)
            require(_data.length >= 4 + 32 + 32 + 32, "not enough data for transferFrom");
            return ( _data._bytesToAddress(4 + 32 + 12), _data._bytesToUint256(4 + 32 + 32));
        } else { //transfer or approve
            // 4 (signature) + 32(address) + 32(uint)
            require(_data.length >= 4 + 32 + 32, "not enough data for transfer/appprove");
            return (_data._bytesToAddress(4 + 12), _data._bytesToUint256(4 + 32));
        }
    }

    /// @notice Toggles whether or not a token is loadable or not.
    function setTokenLoadable(address _token, bool _loadable) external onlyAdmin {
        // Require that the token exists.
        require(_tokenInfoMap[_token].available, "token is not available");

        // this sets the loadable flag to the value passed in
        _tokenInfoMap[_token].loadable = _loadable;

        emit UpdatedTokenLoadable(msg.sender, _token, _loadable);
    }

    /// @notice Toggles whether or not a token is redeemable or not.
    function setTokenRedeemable(address _token, bool _redeemable) external onlyAdmin {
        // Require that the token exists.
        require(_tokenInfoMap[_token].available, "token is not available");

        // this sets the redeemable flag to the value passed in
        _tokenInfoMap[_token].redeemable = _redeemable;

        emit UpdatedTokenRedeemable(msg.sender, _token, _redeemable);
    }

    /// @notice Update ERC20 token exchange rate.
    /// @param _token ERC20 token contract address.
    /// @param _rate ERC20 token exchange rate in wei.
    /// @param _updateDate date for the token updates. This will be compared to when oracle updates are received.
    function updateTokenRate(address _token, uint _rate, uint _updateDate) external onlyAdminOrOracle {
        // Require that the token exists.
        require(_tokenInfoMap[_token].available, "token is not available");
        // Update the token's rate.
        _tokenInfoMap[_token].rate = _rate;
        // Update the token's last update timestamp.
        _tokenInfoMap[_token].lastUpdate = _updateDate;
        // Emit the rate update event.
        emit UpdatedTokenRate(msg.sender, _token, _rate);
    }

    //// @notice Withdraw tokens from the smart contract to the specified account.
    function claim(address payable _to, address _asset, uint _amount) external onlyAdmin {
        _safeTransfer(_to, _asset, _amount);
        emit Claimed(_to, _asset, _amount);
    }

    /// @notice This returns all of the fields for a given token.
    /// @param _a is the address of a given token.
    /// @return string of the token's symbol.
    /// @return uint of the token's magnitude.
    /// @return uint of the token's exchange rate to ETH.
    /// @return bool whether the token is available.
    /// @return bool whether the token is loadable to the TokenCard.
    /// @return bool whether the token is redeemable to the TKN Holder Contract.
    /// @return uint of the lastUpdated time of the token's exchange rate.
    function getTokenInfo(address _a) external view returns (string memory, uint256, uint256, bool, bool, bool, uint256) {
        Token storage tokenInfo = _tokenInfoMap[_a];
        return (tokenInfo.symbol, tokenInfo.magnitude, tokenInfo.rate, tokenInfo.available, tokenInfo.loadable, tokenInfo.redeemable, tokenInfo.lastUpdate);
    }

    /// @notice This returns all of the fields for our StableCoin.
    /// @return string of the token's symbol.
    /// @return uint of the token's magnitude.
    /// @return uint of the token's exchange rate to ETH.
    /// @return bool whether the token is available.
    /// @return bool whether the token is loadable to the TokenCard.
    /// @return bool whether the token is redeemable to the TKN Holder Contract.
    /// @return uint of the lastUpdated time of the token's exchange rate.
    function getStablecoinInfo() external view returns (string memory, uint256, uint256, bool, bool, bool, uint256) {
        Token storage stablecoinInfo = _tokenInfoMap[_stablecoin];
        return (stablecoinInfo.symbol, stablecoinInfo.magnitude, stablecoinInfo.rate, stablecoinInfo.available, stablecoinInfo.loadable, stablecoinInfo.redeemable, stablecoinInfo.lastUpdate);
    }

    /// @notice This returns an array of all whitelisted token addresses.
    /// @return address[] of whitelisted tokens.
    function tokenAddressArray() external view returns (address[] memory) {
        return _tokenAddressArray;
    }

    /// @notice This returns an array of all redeemable token addresses.
    /// @return address[] of redeemable tokens.
    function redeemableTokens() external view returns (address[] memory) {
        address[] memory redeemableAddresses = new address[](_redeemableCounter);
        uint redeemableIndex = 0;
        for (uint i = 0; i < _tokenAddressArray.length; i++) {
            address token = _tokenAddressArray[i];
            if (_tokenInfoMap[token].redeemable){
                redeemableAddresses[redeemableIndex] = token;
                redeemableIndex += 1;
            }
        }
        return redeemableAddresses;
    }


    /// @notice This returns true if a method Id is supported for the specific token.
    /// @return true if _methodId is supported in general or just for the specific token.
    function isERC20MethodSupported(address _token, bytes4 _methodId) public view returns (bool) {
        require(_tokenInfoMap[_token].available, "non-existing token");
        return (_methodIdWhitelist[_methodId]);
    }

    /// @notice This returns true if the method is supported for all protected tokens.
    /// @return true if _methodId is in the method whitelist.
    function isERC20MethodWhitelisted(bytes4 _methodId) external view returns (bool) {
        return (_methodIdWhitelist[_methodId]);
    }

    /// @notice This returns the number of redeemable tokens.
    /// @return current # of redeemables.
    function redeemableCounter() external view returns (uint) {
        return _redeemableCounter;
    }

    /// @notice This returns the address of our stablecoin of choice.
    /// @return the address of the stablecoin contract.
    function stablecoin() external view returns (address) {
        return _stablecoin;
    }

    /// @notice this returns the node hash of our Oracle.
    /// @return the oracle node registered in ENS.
    function oracleNode() external view returns (bytes32) {
        return _oracleNode;
    }
}
/// @title TokenWhitelist stores a list of tokens used by the Consumer Contract Wallet, the Oracle, the TKN Holder and the TKN Licence Contract
function claim(address payable _to, address _asset, uint _amount) external onlyAdmin {
        _safeTransfer(_to, _asset, _amount);
        emit Claimed(_to, _asset, _amount);
    }
//// @notice Withdraw tokens from the smart contract to the specified account.