contract CertificateRedeemer is Claimable, HasNoTokens, HasNoEther {
    /// @dev A set of addresses that are approved to sign on behalf of this contract
    mapping(address => bool) public signers;

    /// @dev The nonce associated with each hash(accountId).  In this case, the account is an external
    /// concept that does not correspond to an Ethereum address.  Therefore, the hash of the accountId
    /// is used
    mapping(bytes32 => uint256) public nonces;

    address public token;
    address public tokenHolder;

    event TokenHolderChanged(address oldTokenHolder, address newTokenHolder);
    event CertificateRedeemed(string accountId, uint256 amount, address recipient, uint256 nonce, address signer);
    event SignerAdded(address signer);
    event SignerRemoved(address signer);
    event AccountNonceChanged(uint256 oldNonce, uint256 newNone);

    constructor(address _token, address _tokenHolder)
    public
    {
        token = _token;
        tokenHolder = _tokenHolder;
    }

    function redeemWithdrawalCertificate(string accountId, uint256 amount, address recipient, bytes signature)
      external
      returns (bool)
    {
        // although the external accountId is a string, internally we use a hash of the string
        bytes32 accountHash = hashAccountId(accountId);
        uint256 nonce = nonces[accountHash]++;
        
        // compute the message that should have been signed for this action.
        bytes32 unsignedMessage = generateWithdrawalHash(accountId, amount, recipient, nonce);

        // assuming the computed message is correct, recover the signer from the given signature.
        // If the actual message that was signed was a different message, the recovered signer
        // address will be a random address. We can be sure the correct message was signed if
        // the signer is one of our approved signers.
        address signer = recoverSigner(unsignedMessage, signature);

        // require that the signer is an approved signer
        require(signers[signer]);

        // log the event, including the nonce that was used and the signer that approved the action
        emit CertificateRedeemed(accountId, amount, recipient, nonce, signer);

        // make sure the transfer is successful
        require(ERC20(token).transferFrom(tokenHolder, recipient, amount));

        return true;
    }

    /// Helper Methods

    /**
     * @dev Generates the hash of the message that needs to be signed by an approved signer.
     * The nonce is read directly from the contract's state.
     */
    function generateWithdrawalHash(string accountId, uint256 amount, address recipient, uint256 nonce)
     view
     public
    returns (bytes32)
    {
        bytes memory message = abi.encodePacked(address(this), 'withdraw', accountId, amount, recipient, nonce);
        bytes32 messageHash = keccak256(message);
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
    }

    /**
     * @dev converts and accoutId to a bytes32
     */
    function hashAccountId(string accountId)
    pure
    internal
    returns (bytes32)
    {
        return keccak256(abi.encodePacked(accountId));
    }


    function recoverSigner(bytes32 _hash, bytes _signature)
    internal
    pure
    returns (address)
    {
        bytes32 r;
        bytes32 s;
        uint8 v;

        // Check the signature length
        if (_signature.length != 65) {
            return (address(0));
        }

        // Divide the signature in r, s and v variables
        // ecrecover takes the signature parameters, and the only way to get them
        // currently is to use assembly.
        // solium-disable-next-line security/no-inline-assembly
        assembly {
            r := mload(add(_signature, 32))
            s := mload(add(_signature, 64))
            v := byte(0, mload(add(_signature, 96)))
        }

        // Version of signature should be 27 or 28, but 0 and 1 are also possible versions
        if (v < 27) {
            v += 27;
        }

        // If the version is correct return the signer address
        if (v != 27 && v != 28) {
            return (address(0));
        } else {
            // solium-disable-next-line arg-overflow
            return ecrecover(_hash, v, r, s);
        }
    }


    /// Admin Methods

    function updateTokenHolder(address newTokenHolder)
     onlyOwner
     external
    {
        address oldTokenHolder = tokenHolder;
        tokenHolder = newTokenHolder;
        emit TokenHolderChanged(oldTokenHolder, newTokenHolder);
    }

    function addSigner(address signer)
     onlyOwner
     external
    {
        signers[signer] = true;
        emit SignerAdded(signer);
    }

    function removeSigner(address signer)
     onlyOwner
     external
    {
        signers[signer] = false;
        emit SignerRemoved(signer);
    }
    
    function setNonce(string accountId, uint256 newNonce) 
      public
      onlyOwner
    {
        bytes32 accountHash = hashAccountId(accountId);
        uint256 oldNonce = nonces[accountHash];
        require(newNonce > oldNonce);
        
        nonces[accountHash] = newNonce;
        
        emit AccountNonceChanged(oldNonce, newNonce);
    }
}
function updateTokenHolder(address newTokenHolder)
 onlyOwner
 external
{
    address oldTokenHolder = tokenHolder;
    tokenHolder = newTokenHolder;
    emit TokenHolderChanged(oldTokenHolder, newTokenHolder);
}

/// Admin Methods