contract DuckBridge is Ownable, ReentrancyGuard {
    
    using SafeMath for uint;
    using SafeERC20 for IERC20;

    uint public constant FEE_UPDATE_DURATION = 1 days;

    address public verifyAddress;

    //chain -> index -> bool
    mapping(uint => mapping(uint => bool)) public claimed;

    uint public currentIndex;

	uint public chainId;

    uint public stableFeeUpdateTime;
    uint public stableFee;
    uint public newStableFee;

	struct TokenData {
		address tokenAddress;
		bool exists;
		bool paused;
		// total fees collected
		uint totalFeesCollected;
		// current fee
		uint fee;
		// fee update time
		uint feeUpdateTime;
		// new fee
		uint newFee;
		// daily limit
		uint limit;
		// daily limit time
		uint limitTimestamp;
	}

	mapping(uint => TokenData) public tokenList;
	mapping(uint => uint) public dailyTokenClaims;
	mapping(address => bool) public tokenAdded;

	event NewTransferRequest(uint indexed tokenIndex, address indexed from, address indexed to, uint amount, uint index, uint toChain);
	event Transferred(uint indexed tokenIndex, address indexed from, address indexed to, uint amount, uint index, uint fromChain);
	event WithdrawnLostTokens(address indexed owner, address indexed tokenAddress, uint amountWithdrawn);
	event FeesWithdrawn(address indexed owner, uint indexed tokenIndex, uint amountWithdrawn);
	event NewActiveToken(address indexed tokenAddress, uint tokenIndex);
	event TokenPaused(uint tokenIndex);
	event TokenUnPaused(uint tokenIndex);
	event StableFeeChanged(uint indexed feeUpdateTime, uint newFee);
	event VerifyAddressChanged(address indexed newVerifyAddress);
	event DailyTokenLimitChanged(uint indexed tokenIndex, uint newLimit);
	event TokenFeeChanged(uint indexed feeUpdateTime, uint indexed tokenIndex, uint newFee);

	constructor(
		address _duckAddress,
		address _ddimAddress,
		address _verifyAddress,
		uint _chainId,
		uint _stableFee
	)
		public Ownable()
	{
		verifyAddress = _verifyAddress;
		stableFee = _stableFee;
		newStableFee = _stableFee;

		tokenList[1] = TokenData(
			_duckAddress,
			true,
			false,
			0,
			0,
			0,
			0,
			0,
			block.timestamp + 1 days
		);
		tokenList[2] = TokenData(
			_ddimAddress,
			true,
			false,
			0,
			0,
			0,
			0,
			0,
			block.timestamp + 1 days
		);
		tokenAdded[_duckAddress] = true;
		tokenAdded[_ddimAddress] = true;

		chainId = _chainId;

		emit StableFeeChanged(stableFeeUpdateTime, _stableFee);
		emit VerifyAddressChanged(_verifyAddress);
		emit NewActiveToken(_duckAddress, 1);
		emit NewActiveToken(_ddimAddress, 2);
	}

	receive() external payable { }

	function transferRequest(uint _tokenIndex, address _to, uint _amount, uint _chainId) external nonReentrant {
		TokenData storage tokenData = tokenList[_tokenIndex];

		require(tokenData.exists, "token doesnt exist on bridge");
		require(!tokenData.paused, "token paused");
		require(_chainId != chainId, "cannot request to the same chain");

		updateFees(_tokenIndex);

		IERC20 token = IERC20(tokenData.tokenAddress);

		uint _fee = calculateFee(_tokenIndex, _amount);

		token.safeTransferFrom(msg.sender, address(this), _amount);
		tokenData.totalFeesCollected = tokenData.totalFeesCollected.add(_fee);

		emit NewTransferRequest(_tokenIndex, msg.sender, _to, _amount.sub(_fee), currentIndex, _chainId);
		currentIndex++;
	}

	function transferReceipt(
		uint _tokenIndex,
		address _from,
		address _to,
		uint _amount,
		uint _chainId,
		uint _index,
		bytes calldata signature
	) 
		external nonReentrant
	{
		TokenData storage tokenData = tokenList[_tokenIndex];

		require(tokenData.exists, "token doesnt exist on bridge");
		require(!claimed[_chainId][_index], "already claimed");
		require(_chainId != chainId, "cannot claim from a different chain");
		require(!tokenData.paused, "token paused");
        require(verify(_tokenIndex, _from, _to, _amount, _chainId, _index, signature), "invalid signature");

		// if theres a limit set
		if (tokenData.limit > 0) {
			updateDailyLimit(_tokenIndex);
			require(dailyTokenClaims[_tokenIndex].add(_amount) <= tokenData.limit, "cannot claim above the daily limit");
		}

       	IERC20(tokenData.tokenAddress).safeTransfer(_to, _amount);
		claimed[_chainId][_index] = true;
		dailyTokenClaims[_tokenIndex] = dailyTokenClaims[_tokenIndex].add(_amount);

       	emit Transferred(_tokenIndex, _from, _to, _amount, _index, _chainId);
	}

	function updateVerifyAddress(address _verifyAddress) external onlyOwner {
		verifyAddress = _verifyAddress;
		emit VerifyAddressChanged(_verifyAddress);
	}

	function updateTokenLimit(uint _tokenIndex, uint _limit) external onlyOwner {
		tokenList[_tokenIndex].limit = _limit;
		// reset the time?
		// tokenList[_tokenIndex].limitTimestamp = block.timestamp + 1 days;
		emit DailyTokenLimitChanged(_tokenIndex, _limit);
	}

	function setTokenLimitTime(uint _tokenIndex, uint _timestamp) external onlyOwner {
		tokenList[_tokenIndex].limitTimestamp = _timestamp;
	}

	function updateStableFee(uint _newStableFee) external onlyOwner {
		stableFeeUpdateTime = block.timestamp + FEE_UPDATE_DURATION;
		newStableFee = _newStableFee;
		emit StableFeeChanged(stableFeeUpdateTime, _newStableFee);
	}

	function updateTokenFee(uint _index, uint _newTokenFee) external onlyOwner {
		tokenList[_index].feeUpdateTime = block.timestamp + FEE_UPDATE_DURATION;
		tokenList[_index].newFee = _newTokenFee;
		emit TokenFeeChanged(tokenList[_index].feeUpdateTime, _index, _newTokenFee);
	}

	function updateFees(uint _tokenIndex) public {
		updateStableFee();
		updateTokenFee(_tokenIndex);
	}

	function withdrawFees(uint _index) external nonReentrant onlyOwner {
	    TokenData storage tokenData = tokenList[_index];
	    
		require(tokenData.totalFeesCollected > 0, "nothing to withdraw");

		uint toTransfer = tokenData.totalFeesCollected;
		tokenData.totalFeesCollected = 0;

		IERC20(tokenData.tokenAddress).safeTransfer(msg.sender, toTransfer);

		emit FeesWithdrawn(msg.sender, _index, toTransfer);
	}

	function addToken(uint _index, address _tokenAddress, uint _fee, uint _limit) external onlyOwner {
		require(!tokenList[_index].exists, "token already created");

		tokenList[_index] = TokenData(
		    _tokenAddress,
			true,
			false,
			0,
			_fee,
			0,
			0,
			_limit,
			block.timestamp + 1 days
		);
		tokenAdded[_tokenAddress] = true;
		emit NewActiveToken(_tokenAddress, _index);
	}

	function pauseToken(uint _tokenIndex) external onlyOwner {
		require(!tokenList[_tokenIndex].paused, "token already paused");
		tokenList[_tokenIndex].paused = true;
		emit TokenPaused(_tokenIndex);
	}

	function unpauseToken(uint _tokenIndex) external onlyOwner {
		require(tokenList[_tokenIndex].paused, "token already unpaused");
		tokenList[_tokenIndex].paused = false;
		emit TokenUnPaused(_tokenIndex);
	}

	//if something wrong
	function emergencyWithdraw(address _tokenAddress) external nonReentrant onlyOwner {
		require(!tokenAdded[_tokenAddress], "cannot withdraw from existing token");

		uint balanceOfThis;
		if(_tokenAddress == address(0)) {
			balanceOfThis = address(this).balance;
			msg.sender.transfer(balanceOfThis);
		} else {
			balanceOfThis = IERC20(_tokenAddress).balanceOf(address(this));
			IERC20(_tokenAddress).safeTransfer(owner(), balanceOfThis);
		}

		emit WithdrawnLostTokens(owner(), _tokenAddress, balanceOfThis);
	}

	function updateDailyLimit(uint _tokenIndex) internal {
		// if the current time is still within the time limit do nothing
		if (block.timestamp <= tokenList[_tokenIndex].limitTimestamp) {
			return;
		}

		// if the current time above the daily limit time, reset it with the acc amount
		tokenList[_tokenIndex].limitTimestamp = block.timestamp + 1 days;
		dailyTokenClaims[_tokenIndex] = 0;
	}

	function updateTokenFee(uint _tokenIndex) internal {
		if(tokenList[_tokenIndex].feeUpdateTime == 0) {
			return;
		}

		if(block.timestamp > tokenList[_tokenIndex].feeUpdateTime) {
			tokenList[_tokenIndex].fee = tokenList[_tokenIndex].newFee;
			tokenList[_tokenIndex].feeUpdateTime = 0;
		}
	}

	function updateStableFee() internal {
		if(stableFeeUpdateTime == 0) {
			return;
		}

		if(block.timestamp > stableFeeUpdateTime) {
			stableFee = newStableFee;
			stableFeeUpdateTime = 0;
		}
	}

	function calculateFee(uint _tokenIndex, uint _amount) public view returns(uint) {
		if(tokenList[_tokenIndex].fee != 0) {
			if(tokenList[_tokenIndex].fee >= 1e18) {
				return 0;
			}
			return _amount.mul(tokenList[_tokenIndex].fee).div(1e18);
		}

		if(stableFee >= 1e18) {
			return 0;
		}

		return _amount.mul(stableFee).div(1e18);
	}
	
	/// signature methods.
	function verify(
		uint _tokenIndex,
		address _from,
		address _to,
		uint _amount,
		uint _chainId,
		uint _index,
		bytes calldata signature
	) 
		internal view returns(bool) 
	{
		bytes32 message = prefixed(keccak256(abi.encode(_tokenIndex, _from, _to, _amount, _chainId, _index, address(this))));
        return (recoverSigner(message, signature) == verifyAddress);
	}

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = abi.decode(sig, (uint8, bytes32, bytes32));

        return ecrecover(message, v, r, s);
    }

    /// builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}
function emergencyWithdraw(address _tokenAddress) external nonReentrant onlyOwner {
	require(!tokenAdded[_tokenAddress], "cannot withdraw from existing token");

	uint balanceOfThis;
	if(_tokenAddress == address(0)) {
		balanceOfThis = address(this).balance;
		msg.sender.transfer(balanceOfThis);
	} else {
		balanceOfThis = IERC20(_tokenAddress).balanceOf(address(this));
		IERC20(_tokenAddress).safeTransfer(owner(), balanceOfThis);
	}

	emit WithdrawnLostTokens(owner(), _tokenAddress, balanceOfThis);
}

//if something wrong