contract PSYieldContract is YieldRoles {
    using SafeERC20 for IERC20;
    IERC20 token;
    
    // Timeframe in which it it possible to deposit tokens
    uint256 public endDepositTime;
    
    // Max tokens to be deposited
    uint256 internal maxTokens;
    
    // Originating wallet for yield payments
    address internal yieldWallet;
    
    // Yield rates in 1e18 granularity
    uint256 public threeMonthPercentage;
    uint256 public sixMonthPercentage;
    uint256 public twelveMonthPercentage;

    // Main struct for lockup
    struct LockBoxStruct {
        address beneficiary;
        uint256 balance;
        uint256 tokenAmount;
        uint256 releaseTime;
        bool tokensDeposited;
    }
    
    mapping (address => uint256) public lockBoxIndex;

    LockBoxStruct[] public lockBoxStructs; // This could be a mapping by address, but these numbered lockBoxes support possibility of multiple tranches per address

    event LogLockupDeposit(address sender, address beneficiary, uint amount, uint releaseTime);   
    event LogLockupWithdrawal(address receiver, uint amount);

    constructor(address tokenContract, uint256 _endDepositTime, address _yieldWallet, uint256 _maxTokens) public {
        token = IERC20(tokenContract);
        endDepositTime = _endDepositTime;
        
        yieldWallet = _yieldWallet;
        maxTokens = _maxTokens;
    }
    
    function getLockBoxBeneficiary(uint256 lockBoxNumber) public view returns(address) {
        return lockBoxStructs[lockBoxNumber].beneficiary;
    }

    // Deposit for 3, 6 or 12 months
    function createLockBoxForWallet3m(address beneficiary, uint256 amount) onlyOwner external {
        createLockBoxForWallet(beneficiary, amount, 90 days);
    }
    
    function createLockBoxForWallet6m(address beneficiary, uint256 amount) onlyOwner external {
        createLockBoxForWallet(beneficiary, amount, 180 days);
    }
    
    function createLockBoxForWallet12m(address beneficiary, uint256 amount) onlyOwner external {
        createLockBoxForWallet(beneficiary, amount, 360 days);
    }
    
    function createLockBoxForWallet(address beneficiary, uint256 amount, uint256 duration) internal {
        require(lockBoxIndex[beneficiary] == 0, "LockBox for user already exists!");
        require(amount < maxTokens, "Token deposit too high, limit breached.");
        maxTokens -= amount;

        // Define and get amount of yield
        uint256 yieldAmount;
        if (duration == 90 days) {
            yieldAmount = (threeMonthPercentage * amount) / 1e20;
        } else if (duration == 180 days) {
            yieldAmount = (sixMonthPercentage * amount) / 1e20;
        } else if (duration == 360 days) {
            yieldAmount = (twelveMonthPercentage * amount) / 1e20;
        } else {
            revert("Error: duration not allowed!");
        }
        require(token.transferFrom(yieldWallet, address(this), yieldAmount));
        
        // Store lockbox number + 1 (to check if it is not 0 for existence)
        lockBoxIndex[beneficiary] = lockBoxStructs.length + 1;
        
        // Build lockbox
        LockBoxStruct memory l;
        l.beneficiary = beneficiary;
        l.balance = yieldAmount;
        l.tokenAmount = amount;
        l.releaseTime = now + duration;
        l.tokensDeposited = false;
        lockBoxStructs.push(l);
    }

    function deposit(uint256 amount) public {
        require(now < endDepositTime, "Deposit time has ended.");
        require(lockBoxIndex[msg.sender] > 0, "No lockbox for wallet found.");
        
        // Get wallet's lockbox (-1 because we stored +1 above)
        uint256 lockBoxNumber = lockBoxIndex[msg.sender] - 1;
        LockBoxStruct storage l = lockBoxStructs[lockBoxNumber];
        
        require(l.tokensDeposited == false, "Tokens already deposited!");
        
        // Wallet must submit exactly the specified amount
        require(amount == l.tokenAmount, "Wrong amount of tokens to deposit.");
        
        // Get lockable tokens from user
        require(token.transferFrom(msg.sender, address(this), amount), "Tokens cannot be transferred.");
        
        // Update lockbox
        l.balance += amount;
        l.tokenAmount = 0;
        l.tokensDeposited = true;
        lockBoxStructs.push(l);
        emit LogLockupDeposit(msg.sender, l.beneficiary, l.balance, l.releaseTime);
    }
    
    // Beneficiaries can update the receiver wallet
    function updateBeneficiary(uint256 lockBoxNumber, address newBeneficiary) public {
        LockBoxStruct storage l = lockBoxStructs[lockBoxNumber];
        require(msg.sender == l.beneficiary);
        l.beneficiary = newBeneficiary;
    }

    function withdraw(uint256 lockBoxNumber) public {
        LockBoxStruct storage l = lockBoxStructs[lockBoxNumber];
        require(l.releaseTime <= now);
        require(l.tokensDeposited == true, "No tokens were deposited, inactive lockbox!");
        uint256 amount = l.balance;
        l.balance = 0;
        emit LogLockupWithdrawal(l.beneficiary, amount);
        require(token.transfer(l.beneficiary, amount));
    }

    // Helper function to release everything    
    function triggerWithdrawAll() public {
        for (uint256 i = 0; i < lockBoxStructs.length; ++i) {
            if (lockBoxStructs[i].releaseTime <= now && lockBoxStructs[i].balance > 0) {
                withdraw(i);
            }
        }
    }
    
    // Retrieve amount from unused lockBoxes
    function withdrawUnusedYield(uint256 lockBoxNumber) public onlyOwner {
        // Check for deposit time end
        require(now > endDepositTime, "Deposit still possible.");
        
        LockBoxStruct storage l = lockBoxStructs[lockBoxNumber];
        
        // Check for non-deposition of tokens
        require(l.tokensDeposited == false, "Tokens were deposited, active lockbox!");
        uint256 amount = l.balance;
        l.balance = 0;
        require(token.transfer(yieldWallet, amount), "Tokens cannot be transferred.");
    }
    
    // Admin update functions
    function updateEndDepositTime (uint256 newEndTime) public onlyOwner {
        endDepositTime = newEndTime;
    }
    
    function updateYieldWallet(address newWallet) public onlyOwner {
        yieldWallet = newWallet;
    }
    
    function updateYields(uint256 threeMonths, uint256 sixMonths, uint256 twelveMonths) public onlyOwner {
        threeMonthPercentage = threeMonths;
        sixMonthPercentage = sixMonths;
        twelveMonthPercentage = twelveMonths;
    }
    
    function updateMaxTokens(uint256 newMaxTokens) public onlyOwner {
        maxTokens = newMaxTokens;
    }
    
    // Retrieve other tokens that were sent to this contract
    function recoverERC20(address tokenAddress, uint256 tokenAmount, address receiver) public onlyOwner {
        // Not possible for DIA tokens to prevent accidential early release
        require(tokenAddress != 0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419);
        IERC20(tokenAddress).transfer(receiver, tokenAmount);
    }
    
    // Do not accept ETH payments
    function() external {
        revert();
    }
}
/**
 * @dev A token holder contract that will allow a beneficiary to extract the
 * tokens after a given release time.
 *
 * Useful for simple vesting schedules like "advisors get all of their tokens
 * after 1 year".
 */
function updateEndDepositTime (uint256 newEndTime) public onlyOwner {
    endDepositTime = newEndTime;
}

// Admin update functions