contract TokenLocker is Ownable, ReentrancyGuard,EternalStorage {
  using SafeMath for uint256;

  

  struct TokenLock {
    uint256 lockDate; // the date the token was locked
    uint256 amount; // the amount of tokens still locked (initialAmount minus withdrawls)
    uint256 initialAmount; // the initial lock amount
    uint256 unlockDate; // the date the token can be withdrawn
    address owner;
  }

  mapping(address => address[]) public lockedTokens;
  mapping(address => address[]) public lockedUser;
  mapping(address =>  mapping (address => TokenLock)) public tokenLocks; //map univ2 pair to all its locks
  
  struct FeeStruct {
    uint256 ethFee; // Small eth fee to prevent spam on the platform
    uint256 liquidityFee; // fee on univ2 liquidity tokens
  }
    
  FeeStruct public gFees;
  
  address payable devaddr;
  address payable lpaddr;

  
  IMigrator migrator;

  event onDeposit(address lpToken, address user, uint256 amount, uint256 lockDate, uint256 unlockDate);
  event onWithdraw(address lpToken, uint256 amount);

  constructor(address payable _lpaddr) public {
    devaddr = msg.sender;
    lpaddr = _lpaddr;
    gFees.ethFee = 0.1 ether;
    gFees.liquidityFee = 10; // 1%
  }
  
  function setDev(address payable _devaddr) public onlyOwner {
    devaddr = _devaddr;
  }
  
  /**
   * @notice set the migrator contract which allows locked lp tokens to be migrated to uniswap v3
   */
  function setMigrator(IMigrator _migrator) public onlyOwner {
    migrator = _migrator;
  }
  


  
  function setFees(uint256 _ethFee, uint256 _liquidityFee) public onlyOwner {
    gFees.ethFee = _ethFee;
    gFees.liquidityFee = _liquidityFee;
  }
  


  /**
   * @notice Creates a new lock
   * @param _lpToken the univ2 token address
   * @param _amount amount of LP tokens to lock
   * @param _unlock_date the unix timestamp (in seconds) until unlock
   * @param _withdrawer the user who can withdraw liquidity once the lock expires.
   */
  function lockLPToken (address _lpToken, uint256 _amount, uint256 _unlock_date, address payable _withdrawer) external payable nonReentrant {
    require(_unlock_date < 10000000000, 'TIMESTAMP INVALID'); // prevents errors when timestamp entered in milliseconds
    require(_amount > 0, 'INSUFFICIENT');


    TransferHelper.safeTransferFrom(_lpToken, address(msg.sender), address(this), _amount);
    
    
    uint256 ethFee = gFees.ethFee;
    require(msg.value == ethFee, 'FEE NOT MET');
    uint256 devFee = ethFee;
    devaddr.transfer(devFee);
    
    // percent fee
    uint256 liquidityFee = _amount.mul(gFees.liquidityFee).div(1000);
    TransferHelper.safeTransfer(_lpToken, lpaddr, liquidityFee);
    uint256 amountLocked = _amount.sub(liquidityFee);
    
	if(!boolStorage[keccak256(abi.encodePacked(_lpToken,_withdrawer))]){
        TokenLock memory token_lock;
        token_lock.lockDate = block.timestamp;
        token_lock.amount = amountLocked;
        token_lock.initialAmount = amountLocked;
        token_lock.unlockDate = _unlock_date;
        token_lock.owner = _withdrawer;
        // record the lock for the univ2pair
        tokenLocks[_lpToken][_withdrawer] = token_lock;
        boolStorage[keccak256(abi.encodePacked(_lpToken,_withdrawer))] = true;
        
        if(!boolStorage[keccak256(abi.encodePacked(_withdrawer,_lpToken))]){
          lockedTokens[_lpToken].push(_withdrawer);
          lockedUser[_withdrawer].push(_lpToken);
          boolStorage[keccak256(abi.encodePacked(_withdrawer,_lpToken))] = true;
        }
        emit onDeposit(_lpToken, msg.sender, token_lock.amount, token_lock.lockDate, token_lock.unlockDate);
	}else{
	    require(msg.sender == _withdrawer, '_withdrawer no sender');
	    TokenLock storage tokenLock = tokenLocks[_lpToken][_withdrawer];
		tokenLock.amount= tokenLock.amount.add(amountLocked);
		tokenLock.initialAmount= tokenLock.initialAmount.add(amountLocked);
		tokenLock.lockDate = block.timestamp;
		if(_unlock_date > tokenLock.unlockDate){
		    tokenLock.unlockDate = _unlock_date;
		}
		emit onDeposit(_lpToken, msg.sender, tokenLock.amount, tokenLock.lockDate, tokenLock.unlockDate);
	}

    
  }
  

  function relock (address _lpToken,  uint256 _unlock_date) external nonReentrant {
    require(_unlock_date < 10000000000, 'TIMESTAMP INVALID'); // prevents errors when timestamp entered in milliseconds
    TokenLock storage userLock = tokenLocks[_lpToken][msg.sender];
    require(userLock.owner == msg.sender, 'LOCK MISMATCH'); // ensures correct lock is affected
    require(userLock.unlockDate < _unlock_date, 'UNLOCK BEFORE');

    userLock.unlockDate = _unlock_date;

  }


  /**
   * @notice withdraw a specified amount from a lock. _index and _lockID ensure the correct lock is changed
   * this prevents errors when a user performs multiple tx per block possibly with varying gas prices
   */
  function withdraw (address _lpToken, uint256 _amount) external nonReentrant {
    require(_amount > 0, 'ZERO WITHDRAWL');
    TokenLock storage userLock = tokenLocks[_lpToken][msg.sender];
    require(userLock.owner == msg.sender, 'LOCK MISMATCH'); // ensures correct lock is affected
    require(userLock.unlockDate < block.timestamp, 'NOT YET');
    userLock.amount = userLock.amount.sub(_amount);

    // clean user storage
    if (userLock.amount == 0) {
		boolStorage[keccak256(abi.encodePacked(_lpToken,msg.sender))] = false;
    }
    
    TransferHelper.safeTransfer(_lpToken, msg.sender, _amount);
    emit onWithdraw(_lpToken, _amount);
  }
  


  function getLockForToken (address _lpToken) external view 
  returns (address[] memory) {
    address[] memory addr_list = lockedTokens[_lpToken];
    return addr_list;
  }
  
  function getLockForUser (address _user) external view 
  returns (address[] memory) {
    address[] memory addr_list = lockedUser[_user];
    return addr_list;
  }
  
  function getUserLockForToken (address _user, address _lpToken) external view 
  returns (uint256, uint256, uint256, uint256, address) {
    TokenLock storage tokenLock = tokenLocks[_lpToken][_user];
    return (tokenLock.lockDate, tokenLock.amount, tokenLock.initialAmount, tokenLock.unlockDate, tokenLock.owner);
  }
  

}
function withdraw (address _lpToken, uint256 _amount) external nonReentrant {
  require(_amount > 0, 'ZERO WITHDRAWL');
  TokenLock storage userLock = tokenLocks[_lpToken][msg.sender];
  require(userLock.owner == msg.sender, 'LOCK MISMATCH'); // ensures correct lock is affected
  require(userLock.unlockDate < block.timestamp, 'NOT YET');
  userLock.amount = userLock.amount.sub(_amount);

  // clean user storage
  if (userLock.amount == 0) {
boolStorage[keccak256(abi.encodePacked(_lpToken,msg.sender))] = false;
  }
    
  TransferHelper.safeTransfer(_lpToken, msg.sender, _amount);
  emit onWithdraw(_lpToken, _amount);
}

/**
 * @notice withdraw a specified amount from a lock. _index and _lockID ensure the correct lock is changed
 * this prevents errors when a user performs multiple tx per block possibly with varying gas prices
 */