contract Holder is Ownable,Initializable {

    using SafeMath for uint256;
    using EnumerableSet for EnumerableSet.AddressSet;
    using EnumerableMap for EnumerableMap.UintToAddressMap;

    struct Order {
        address tokenAddr;
        uint256 lockAmount;
        uint256 startTime;
        uint256 endTime;
        uint claim;
    }


    EnumerableSet.AddressSet private _tokens;

    //address==>Order[]
    mapping(address => Order[]) userOrders;

    //token=>lockAmount
    mapping(address => uint256) tokenLockAmounts;

    address payable public feeAddress;
    
    uint256 constant public minLockTime = 60;
    uint256 constant public maxLockTime = 365 * 3 days;
    
    uint256 constant public baseRate = 10000;
    uint256 public feeRate = 0;
    uint256 constant public maxFeeRate = 200;
    
    
    

    event Lock(address indexed user, address indexed tokenAddr, uint256 indexed value, uint256 time);
    event LockETH(address indexed user,uint256 indexed value,uint256 indexed time);
    event UnLock(address indexed user, address indexed tokenAddr, uint256 indexed value);
    event UnLockETH(address indexed user,uint256 indexed value);

    // constructor() public {
       
    // }
    
      // --- Init ---
    function initialize(
       address payable _feeAddress,
       uint256 _feeRate
    ) public initializer onlyOwner  {
        require(_feeRate <= maxFeeRate,"invalid value");
        feeAddress = _feeAddress;
        feeRate = _feeRate;
    }


    function calculationFee(uint256 _amount) public view returns (uint256){
        return _amount.mul(feeRate).div(baseRate);
    }

    function lock(address _tokenAddr, uint256 _amount, uint256 _seconds) payable external returns (bool){
        if(_tokenAddr == address(0)){
            return lockEth(_tokenAddr,_seconds);
        }else{
            return lockERC20(_tokenAddr,_amount,_seconds);
        }
       
    }


    function lockEth(address _tokenAddr,uint256 _seconds) internal  returns(bool) {
        uint256 _amount = msg.value;
        require(_amount > 0, "amount must be greater than 0");
        require(_seconds >= minLockTime && _seconds <= maxLockTime, "invalid time");
        uint256 fee = calculationFee(_amount);
        feeAddress.transfer(fee);
        uint256 userLockAmount = _amount.sub(fee);
        // address _tokenAddr = address(0x0);

        Order memory order = Order(_tokenAddr, userLockAmount, now, now.add(_seconds), 0);
        userOrders[msg.sender].push(order);
        tokenLockAmounts[_tokenAddr] = tokenLockAmounts[_tokenAddr].add(userLockAmount);
        if (!EnumerableSet.contains(_tokens, _tokenAddr)) {
            EnumerableSet.add(_tokens, _tokenAddr);
        }
        emit LockETH(msg.sender, _amount, _seconds);
        return true;
        
    }
    
    function lockERC20(address _tokenAddr,uint256 _amount,uint256 _seconds) internal returns(bool) {
        require(_tokenAddr != address(0x0),"invalid token address");
        require(_amount > 0, "amount must be greater than 0");
        require(_seconds >= minLockTime && _seconds <= maxLockTime, "invalid time");
        uint256 fee = calculationFee(_amount);
       
        IERC20(_tokenAddr).transferFrom(msg.sender, address(this), _amount);
        IERC20(_tokenAddr).transfer(feeAddress, fee);
    

        uint256 userLockAmount = _amount.sub(fee);

        Order memory order = Order(_tokenAddr, userLockAmount, now, now.add(_seconds), 0);
        userOrders[msg.sender].push(order);
        tokenLockAmounts[_tokenAddr] = tokenLockAmounts[_tokenAddr].add(userLockAmount);
        if (!EnumerableSet.contains(_tokens, _tokenAddr)) {
            EnumerableSet.add(_tokens, _tokenAddr);
        }
        emit Lock(msg.sender, _tokenAddr, _amount, _seconds);
        return true;
        
    }
    
    
    function unlock(uint _index) external returns (bool){
        require(userOrders[msg.sender].length - 1 >= _index, "invalid index");
        Order storage order = userOrders[msg.sender][_index];
        require(order.claim == 0, "claimed");
        require(now >= order.endTime, "unlock time not reached");
        order.claim = 1;

        uint256 withdrawAmount = getLockAmount(order.tokenAddr,order.lockAmount);
        if(order.tokenAddr == address(0)){
            msg.sender.transfer(withdrawAmount);
            emit UnLockETH(msg.sender,order.lockAmount);
        }else{
            IERC20(order.tokenAddr).transfer(msg.sender, withdrawAmount);
            emit UnLock(msg.sender, order.tokenAddr, order.lockAmount);
        }
        tokenLockAmounts[order.tokenAddr] = tokenLockAmounts[order.tokenAddr].sub(order.lockAmount);
        
        return true;
    }

    function getLockAmount(address tokenAddr,uint lockAmount) internal  view returns(uint256){
        uint256 balance =  0;
        if(tokenAddr == address(0)){
            balance = address(this).balance;
        }else{
            balance = IERC20(tokenAddr).balanceOf(address(this));
        }
       
        uint256 totalAmount = tokenLockAmounts[tokenAddr];
        uint256 withdrawAmount = balance.mul(1e16).div(totalAmount).mul(lockAmount).div(1e16);
        return withdrawAmount;
    }


    function getOrders() public view returns (Order[] memory order){
        return userOrders[msg.sender];
    }

    function getUserOrderLength() public view returns (uint){
        return userOrders[msg.sender].length;
    }

    function getOrder(uint _index) public view returns (Order memory order){
        return userOrders[msg.sender][_index];
    }

    function getPercentOrders() public view returns (Order[] memory orders){
        uint256 orderLength = userOrders[msg.sender].length;
        
         orders = new Order[](orderLength);
  
        for(uint i = 0; i < orderLength; i++) {
            orders[i]  = getOrderView(i);
            // orders[i] = order;
        }
        return orders;

    }

    function getPercentOrder(uint _index) public view returns (Order memory order){
         uint256 orderLength = userOrders[msg.sender].length;
         return orderLength > _index? getOrderView(_index) : userOrders[msg.sender][_index];
    }


    function getOrderView(uint _index) internal view returns(Order memory order){
        Order memory  userOrder = userOrders[msg.sender][_index];
        uint256 withdrawAmount = getLockAmount(userOrder.tokenAddr,userOrder.lockAmount);
        order =  Order(userOrder.tokenAddr, withdrawAmount, userOrder.startTime, userOrder.endTime, userOrder.claim);
        return order;
    }


    function canUnlock(uint _index) public view returns (bool){
        return now >= userOrders[msg.sender][_index].endTime ? true : false;
    }


    function getTokens() public view returns (address[] memory tokens){
        tokens = new address[](getTokensLength());
        for (uint i = 0; i < getTokensLength(); i++) {
            tokens[i] = EnumerableSet.at(_tokens, i);
        }
        return tokens;
    }

    function getTokensLength() public view returns (uint){
        return EnumerableSet.length(_tokens);
    }


    function getToken(uint256 _index) public view returns (address){
        require(_index <= getTokensLength() - 1, "index out of bounds");
        return EnumerableSet.at(_tokens, _index);
    }

    function getTokenLockAmounts(address _tokenAddr) public view returns(uint256){
        return tokenLockAmounts[_tokenAddr];
    }

    function containsToken(address _tokenAddr) public view returns (bool) {
        return EnumerableSet.contains(_tokens, _tokenAddr);
    }

    


    function setFeeRate(uint256 _rate) external onlyOwner returns (bool){
        require(_rate <= maxFeeRate,"invalid feeRate");
        feeRate = _rate;
        return true;
    }

    

    function setFeeAddress(address payable _feeAddress) public onlyOwner returns(bool){
        feeAddress = _feeAddress;
        return true;
    }

}
function initialize(
   address payable _feeAddress,
   uint256 _feeRate
) public initializer onlyOwner  {
    require(_feeRate <= maxFeeRate,"invalid value");
    feeAddress = _feeAddress;
    feeRate = _feeRate;
}

// constructor() public {
// }
// --- Init ---