contract Bridge is Ownable{

    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    struct TransferInfo {
        uint256 id;
        address sender;
        address receiver;
        uint256 sourceChainID;
        uint256 targetChainID;
        uint256 amount;
    }
    
    IERC20  public immutable token;
    uint256 public immutable chainID;

    uint256 public chainCounts;
    uint256 public transferID;

    uint256 public totalFee;
    
    mapping(uint256 => uint256) public transferFee;
    mapping(uint256 => TransferInfo) public transferRecords;
    mapping(uint256 => mapping(uint256 => bool)) public isTransferDone;

    
    // --- events ---
    
    event TokenSet(address token);
    event ChainIDSet(uint256 id);
    event ChainCountsUpdate(uint256 originCounts, uint256 newCounts);
    event TransferFeeUpdate(uint256 targetChainID, uint256 originFee, uint256 newFee);
    event WithdrawFee(uint256 amount, address receiver);
    event TransferTo(
        uint256 indexed id, 
        address indexed sender, 
        address indexed receiver, 
        uint256 sourceChainID, 
        uint256 targetChainID, 
        uint256 amount
    );
    event TransferDone(
        uint256 indexed id, 
        address indexed sender, 
        address indexed receiver, 
        uint256 sourceChainID, 
        uint256 targetChainID, 
        uint256 amount
    );


    // --- constructor ---

    constructor(address _tokenAddress, uint256 _chainID) public {
        
        token = IERC20(_tokenAddress);
        chainID = _chainID;

        transferID = 1;
        chainCounts = 2;

        emit TokenSet(_tokenAddress);
        emit ChainIDSet(_chainID);
        emit ChainCountsUpdate(0, chainCounts);
    }


    // --- view function ---

    function totalReserve() public view returns (uint256){
        return token.balanceOf(address(this)).sub(totalFee);
    }


    // --- mutable function ---

    function setChainCount(uint256 _chainCounts) external onlyOwner {
        uint256 originCounts = chainCounts;
        
        require(_chainCounts > originCounts, "invalid update");
        chainCounts = _chainCounts;

        emit ChainCountsUpdate(originCounts, chainCounts);
    }
    
    function setTransferFee(uint256 _targetChainID, uint256 _transferFee) external onlyOwner {
        uint256 originFee = transferFee[_targetChainID];
        transferFee[_targetChainID] = _transferFee;

        emit TransferFeeUpdate(_targetChainID, originFee, _transferFee);
    }

    function withdrawFee(address _receiver) external onlyOwner {
        
        uint256 amount = totalFee;
        totalFee = 0;

        token.safeTransfer(_receiver, amount);

        emit WithdrawFee(amount, _receiver);
    }

    function transferTo(
        uint256 _targetChainID,
        uint256 _amount,
        address _receiver
    ) external {
        require(_targetChainID != chainID, "can not transfer to source chain.");
        require(_targetChainID <= chainCounts, "invalid chain ID.");
        
        uint256 fee = transferFee[_targetChainID];
        require(_amount > fee, "transfer amount is less than transfer fee.");
        require(_receiver != address(0), "invalid receiver address");

        token.safeTransferFrom(msg.sender, address(this), _amount);
        
        totalFee = totalFee.add(fee);
        uint256 actualAmount = _amount.sub(fee);
        uint256 id = transferID;
        transferID = transferID.add(1);
        
        transferRecords[id] = TransferInfo(
            id,
            msg.sender,
            _receiver,
            chainID,
            _targetChainID,
            actualAmount
        );

        emit TransferTo(id, msg.sender, _receiver, chainID, _targetChainID, actualAmount);
    }

    function batchTransferDone(TransferInfo[] memory _transfers) external onlyOwner {
        for (uint8 i = 0; i < _transfers.length; i++) {
            TransferInfo memory transferInfo = _transfers[i];
            if (
                transferInfo.targetChainID == chainID && 
                !isTransferDone[transferInfo.sourceChainID][transferInfo.id]
            ) {
                isTransferDone[transferInfo.sourceChainID][transferInfo.id] = true;
                
                require(transferInfo.amount <= totalReserve(), "transfer amount is more than total reserve.");
                token.safeTransfer(transferInfo.receiver, transferInfo.amount);

                emit TransferDone(
                    transferInfo.id, 
                    transferInfo.sender, 
                    transferInfo.receiver, 
                    transferInfo.sourceChainID, 
                    transferInfo.targetChainID, 
                    transferInfo.amount
                );
            }
        }
    }
}
function setChainCount(uint256 _chainCounts) external onlyOwner {
    uint256 originCounts = chainCounts;
        
    require(_chainCounts > originCounts, "invalid update");
    chainCounts = _chainCounts;

    emit ChainCountsUpdate(originCounts, chainCounts);
}

// --- mutable function ---