contract SINStaking is Ownable {

    using SafeMath for uint256;
    using LowGasSafeMath for uint32;
    using SafeERC20 for IERC20;
    using SafeERC20 for IsSIN;

    IERC20 public immutable SIN;
    IsSIN public immutable sSIN;

    struct Epoch {
        uint number;
        uint distribute;
        uint32 length;
        uint32 endTime;
    }
    Epoch public epoch;

    IDistributor public distributor;
    
    uint public totalBonus;
    
    IWarmup public warmupContract;
    uint public warmupPeriod;
    enum LOCKUPS { NONE, MONTH1, MONTH3, MONTH6 }
    struct Lockup{
        uint gonsWarmup;
        uint gonsAccount;
        uint initialDeposit;
        uint256 multiplier;
        uint256 lockTimestamp;
    }

    mapping( address => Lockup) public lockupInfo;
    uint internal month = 2629746;

    event LogStake(address indexed recipient, uint256 amount);
    event LogClaim(address indexed recipient, uint256 amount);
    event LogForfeit(address indexed recipient, uint256 memoAmount, uint256 timeAmount);
    event LogDepositLock(address indexed user, bool locked);
    event LogUnstake(address indexed recipient, uint256 amount);
    event LogRebase(uint256 distribute);
    event LogSetContract(CONTRACTS contractType, address indexed _contract);
    event LogWarmupPeriod(uint period);
    
    constructor ( 
        address _SIN, 
        address _sSIN, 
        uint32 _epochLength,
        uint _firstEpochNumber,
        uint32 _firstEpochTime
    ) {
        require( _SIN != address(0) );
        SIN = IERC20(_SIN);
        require( _sSIN != address(0) );
        sSIN = IsSIN(_sSIN);
        
        epoch = Epoch({
            length: _epochLength,
            number: _firstEpochNumber,
            endTime: _firstEpochTime,
            distribute: 0
        });
    }

    struct Claim {
        uint deposit;
        uint gons;
        uint expiry;
        bool lock; // prevents malicious delays
    }
    mapping( address => Claim ) public warmupInfo;

    /**
        @notice stake SIN to enter warmup
        @param _amount uint
        @return bool
     */
    function stake( uint _amount, address _recipient, LOCKUPS _lockup ) external returns ( bool ) {
        rebase();
        
        SIN.safeTransferFrom( msg.sender, address(this), _amount );

        Claim memory info = warmupInfo[ _recipient ];
        require( !info.lock, "Deposits for account are locked" );

        warmupInfo[ _recipient ] = Claim ({
            deposit: info.deposit.add( _amount ),
            gons: info.gons.add( sSIN.gonsForBalance( _amount ) ),
            expiry: epoch.number.add( warmupPeriod ),
            lock: false
        });
        
        Lockup memory lock = lockupInfo[ _recipient ];
        require(lock.multiplier == 0 || _lockup == LOCKUPS.NONE, "Account is already locked");
        
        uint256 amountToTransfer = _amount;

        if( _lockup == LOCKUPS.MONTH1 ) { // 1 - 1.25 Multiplier

            lockupInfo[ _recipient ] = Lockup({
                gonsWarmup: IsSIN( sSIN ).gonsForBalance( _amount.mul(25).div(100) ),
                gonsAccount: IsSIN( sSIN ).gonsForBalance( _amount ),
                initialDeposit: _amount,
                multiplier: 125,
                lockTimestamp: block.timestamp + month
            });
            amountToTransfer = _amount.mul(125)/100;
        }
        if( _lockup == LOCKUPS.MONTH3 ) { // 2 - 1.5 Multiplier

            lockupInfo[ _recipient ] = Lockup({
                gonsWarmup: IsSIN( sSIN ).gonsForBalance( _amount.mul(50).div(100) ),
                gonsAccount: IsSIN( sSIN ).gonsForBalance( _amount ),
                initialDeposit: _amount,
                multiplier: 150,
                lockTimestamp: block.timestamp + month*3
            });

            amountToTransfer = _amount.mul(150).div(100);
        }
        if( _lockup == LOCKUPS.MONTH6 ) { // 3 - 2 Multiplier

            lockupInfo[ _recipient ] = Lockup({
                gonsWarmup: IsSIN( sSIN ).gonsForBalance( _amount ),
                gonsAccount: IsSIN( sSIN ).gonsForBalance( _amount ),
                initialDeposit: _amount,
                multiplier: 200,
                lockTimestamp: block.timestamp + month*6
            });
            amountToTransfer = _amount*2;
        }

        sSIN.safeTransfer( address(warmupContract), amountToTransfer );
        emit LogStake(_recipient, _amount);
        return true;
    }

    /**
        @notice retrieve sSIN from warmup
        @param _recipient address
     */
    function claim ( address _recipient ) external {
        Claim memory info = warmupInfo[ _recipient ];
        if ( epoch.number >= info.expiry && info.expiry != 0 ) {
            delete warmupInfo[ _recipient ];
            uint256 amount = sSIN.balanceForGons( info.gons );
            warmupContract.retrieve( _recipient,  amount);
            emit LogClaim(_recipient, amount);
        }
    }

    /**
        @notice forfeit sSIN in warmup and retrieve SIN
     */
    function forfeit() external {
        Claim memory info = warmupInfo[ msg.sender ];
        delete warmupInfo[ msg.sender ];
        uint memoBalance = sSIN.balanceForGons( info.gons );
        warmupContract.retrieve( address(this),  memoBalance);
        SIN.safeTransfer( msg.sender, info.deposit);
        emit LogForfeit(msg.sender, memoBalance, info.deposit);
    }

    /**
        @notice prevent new deposits to address (protection from malicious activity)
     */
    function toggleDepositLock() external {
        warmupInfo[ msg.sender ].lock = !warmupInfo[ msg.sender ].lock;
        emit LogDepositLock(msg.sender, warmupInfo[ msg.sender ].lock);
    }

    /**
        @notice redeem sSIN for SIN
        @param _amount uint
        @param _trigger bool
     */
    function unstake( uint _amount, bool _trigger ) external {
        if ( _trigger ) {
            rebase();
        }
        Lockup memory lock = lockupInfo[ msg.sender ];
        require(sSIN.balanceOf(msg.sender) - _amount >= sSIN.balanceForGons( lock.gonsAccount ), "Not enough sSIN for lockup");
        sSIN.safeTransferFrom( msg.sender, address(this), _amount );
        SIN.safeTransfer( msg.sender, _amount );
        emit LogUnstake(msg.sender, _amount);
    }

    function unstakeLocked(bool _trigger) external{
        if (_trigger){
            rebase();
        }
        Lockup memory lock = lockupInfo[ msg.sender ];
        if (lock.multiplier >0){
            sSIN.safeTransferFrom( msg.sender, address(this), sSIN.balanceForGons( lock.gonsAccount ));
            warmupContract.retrieve( address(this), sSIN.balanceForGons( lock.gonsWarmup ));

            if (lock.lockTimestamp <= block.timestamp){
                SIN.safeTransfer( msg.sender, sSIN.balanceForGons( lock.gonsWarmup + lock.gonsAccount ));
            }
            else{
                SIN.safeTransfer( msg.sender, lock.initialDeposit );
            }
        delete lockupInfo[ msg.sender ];

        }


    } 


    /**
        @notice returns the sSIN index, which tracks rebase growth
        @return uint
     */
    function index() external view returns ( uint ) {
        return sSIN.index();
    }

    /**
        @notice trigger rebase if epoch over
     */
    function rebase() public {
        if( epoch.endTime <= uint32(block.timestamp) ) {

            sSIN.rebase( epoch.distribute, epoch.number );

            epoch.endTime = epoch.endTime.add32( epoch.length );
            epoch.number++;
            
            if ( address(distributor) != address(0) ) {
                distributor.distribute();
            }

            uint balance = contractBalance();
            uint staked = sSIN.circulatingSupply();

            if( balance <= staked ) {
                epoch.distribute = 0;
            } else {
                epoch.distribute = balance.sub( staked );
            }
            emit LogRebase(epoch.distribute);
        }
    }

    /**
        @notice returns contract SIN holdings, including bonuses provided
        @return uint
     */
    function contractBalance() public view returns ( uint ) {
        return SIN.balanceOf( address(this) ).add( totalBonus );
    }

    enum CONTRACTS { DISTRIBUTOR, WARMUP }

    /**
        @notice sets the contract address for LP staking
        @param _contract address
     */
    function setContract( CONTRACTS _contract, address _address ) external onlyOwner {
        if( _contract == CONTRACTS.DISTRIBUTOR ) { // 0
            distributor = IDistributor(_address);
        } else if ( _contract == CONTRACTS.WARMUP ) { // 1
            require( address(warmupContract) == address( 0 ), "Warmup cannot be set more than once" );
            warmupContract = IWarmup(_address);
        }
        emit LogSetContract(_contract, _address);
    }
    
    /**
     * @notice set warmup period in epoch's numbers for new stakers
     * @param _warmupPeriod uint
     */
    function setWarmup( uint _warmupPeriod ) external onlyOwner {
        warmupPeriod = _warmupPeriod;
        emit LogWarmupPeriod(_warmupPeriod);
    }
}
function claim ( address _recipient ) external {
    Claim memory info = warmupInfo[ _recipient ];
    if ( epoch.number >= info.expiry && info.expiry != 0 ) {
        delete warmupInfo[ _recipient ];
        uint256 amount = sSIN.balanceForGons( info.gons );
        warmupContract.retrieve( _recipient,  amount);
        emit LogClaim(_recipient, amount);
    }
}

/**
 @notice retrieve sSIN from warmup
 @param _recipient address
 */