contract NFTeGG_Farm_3_Final is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    event StakeERC20(address indexed user, address indexed token, uint256 value);
    event UnstakeERC20(address indexed user, address indexed token, uint256 value);
    event StakeERC721(address indexed user, address indexed token, uint256 tokenid);
    event UnstakeERC721(address indexed user, address indexed token, uint256 tokenid);
    event StakeT1(address indexed user, address indexed token, uint256 indexed tokenid);
    event UnstakeT1(address indexed user, address indexed token, uint256 indexed tokenid);
    event Payout(address indexed user, uint256 value);

    uint256 public BLOCKTIME_PENALTY_THRESHOLD = 3600 * 72; // minimum time (in seconds) for staking to receive payout without penalty
    address public T3_ADDRESS = address(0xD821E91DB8aED33641ac5D122553cf10de49c8eF); // address of T3 contract (ERC20) used for distributing rewards
    
    uint256 public repay = 0; 
    uint256 public counter = 0;

    mapping (address => uint256) public accumulatedPayouts; // mapping from address to current accumulated payouts; only updated on new stakes on top of an already staked erc20 token and unstakes

    struct StakingDataERC20 {
        uint256 tokenStaked;
        uint setupBlock;
        uint setupTime;
    }

    struct StakeFactor {
        uint128 multi;
        uint128 divi;
    }

    address[] public supportedTokensERC20;
    mapping (address => StakeFactor) public erc20StakeFactors; // maps ERC20 token address to its stake factors
    mapping (address => mapping (address => StakingDataERC20)) public erc20Stakes; // mapping (<user address> => mapping (<token address> => <stake data>))

    struct StakingDataERC721 {
        uint setupBlock;
        uint setupTime;
    }

    address[] public supportedTokensERC721;
    mapping (address => StakeFactor) public erc721StakeFactors; // maps ERC721 token address to its stake factors
    mapping (address => mapping (uint256 => address)) public stakedERC721Holders; // maps from token address and tokenid to the user address who staked the tokenid
    mapping (address => mapping (address => mapping (uint256 => StakingDataERC721))) public erc721Stakes; // maps token address and staker address onto a mapping from tokenid to the struct specifying when the stake was made
    mapping (address => mapping (address => uint256[])) public stakedERC721ByUser; // maps token address and user address to an array of staked tokenids
    mapping (address => mapping (uint256 => uint256)) public erc721ArrayIndex; // maps token address and a tokenid to the index in the stakedERC721ByUser array of the user that holds it

    struct StakeFactorT1 {
        uint64 baseMulti; // base: stake payout per power and block for staked T1
        uint64 baseDivi;
        uint64 boolMulti; // bool: multiplier if the bool is true
        uint64 boolDivi;
    }

    address[] public supportedTokensT1;
    mapping (address => StakeFactorT1) public t1StakeFactors; // maps ERC721 token address to its stake factors
    mapping (address => mapping (uint256 => address)) public stakedT1Holders; // maps from token address and tokenid to the user address who staked the tokenid
    mapping (address => mapping (address => mapping (uint256 => StakingDataERC721))) public t1Stakes; // maps token address and staker address onto a mapping from tokenid to the struct specifying when the stake was made
    mapping (address => mapping (address => uint256[])) public stakedT1ByUser; // maps token address and user address to an array of staked tokenids
    mapping (address => mapping (uint256 => uint256)) public t1ArrayIndex; // maps token address and a tokenid to the index in the stakedT1ByUser array of the user that holds it

    // ctor
    constructor () public {
    }

    // views
    function calcCurrentPayoutERC20(address user, address token) public view returns (uint256) {
        uint stakeBlock = erc20Stakes[user][token].setupBlock;
        require(stakeBlock != 0, "User has no stake");

        if ((block.timestamp - erc20Stakes[user][token].setupTime) >= BLOCKTIME_PENALTY_THRESHOLD) { // normal payout if minimum time was reached
            return (block.number - stakeBlock) * erc20Stakes[user][token].tokenStaked * erc20StakeFactors[token].multi / erc20StakeFactors[token].divi;
        } else { // 90% payout if minimum time was not reached
            return (block.number - stakeBlock) * erc20Stakes[user][token].tokenStaked * erc20StakeFactors[token].multi / erc20StakeFactors[token].divi * 9 / 10;
        }
    }

    function calcCurrentPayoutERC721(address token, address user, uint256 tokenid) public view returns (uint256) {
        uint stakeBlock = erc721Stakes[token][user][tokenid].setupBlock;
        require(stakeBlock != 0, "User did not stake that token");

        if ((block.timestamp - erc721Stakes[token][user][tokenid].setupTime) >= BLOCKTIME_PENALTY_THRESHOLD) { // normal payout if minimum time was reached
            return (block.number - stakeBlock) * erc721StakeFactors[token].multi / erc721StakeFactors[token].divi;
        } else { // 90% payout if minimum time was not reached
            return (block.number - stakeBlock) * erc721StakeFactors[token].multi / erc721StakeFactors[token].divi * 9 / 10;
        }
    }

    function calcCurrentPayoutT1(address token, address user, uint256 tokenid) public view returns (uint256) {
        uint stakeBlock = t1Stakes[token][user][tokenid].setupBlock;
        require(stakeBlock != 0, "User did not stake that token");

        uint256 base = (block.number - stakeBlock) * T1(token).power(tokenid) * t1StakeFactors[token].baseMulti / t1StakeFactors[token].baseDivi;

        if (T1(token).someBool(tokenid)) {
            base = base * t1StakeFactors[token].boolMulti / t1StakeFactors[token].boolDivi;
        }

        if ((block.timestamp - t1Stakes[token][user][tokenid].setupTime) < BLOCKTIME_PENALTY_THRESHOLD) { // 90% payout if minimum time was not reached
            base = base * 9 / 10;
        }

        return base;
    }

    function tokenIdsFromUserT1(address token, address user) external view returns (uint256[] memory) {
        require(t1StakeFactors[token].baseDivi != 0, "Token not supported");
        return stakedT1ByUser[token][user];
    }

    function tokenIdsFromUserERC721(address token, address user) external view returns (uint256[] memory) {
        require(erc721StakeFactors[token].divi != 0, "Token not supported");
        return stakedERC721ByUser[token][user];
    }

    // user stuff
    function processAccumulatedPayout() external {
        address user = msg.sender;
        uint256 payout = accumulatedPayouts[user];
        require(payout > 0, "No payout pending");
        uint256 reserve = IERC20(T3_ADDRESS).balanceOf(address(this));
        require(reserve > 0, "Unable to process any more payouts");

        if (reserve >= payout) {
            delete accumulatedPayouts[user];
            IERC20(T3_ADDRESS).safeTransfer(user, payout);
            Payout(user, payout);
        } else {
            accumulatedPayouts[user] = payout - reserve;
            IERC20(T3_ADDRESS).safeTransfer(user, reserve);
            Payout(user, reserve);
        }
    }

    function stakeERC20(address token, uint256 value) external {
        require(erc20StakeFactors[token].multi != 0, "ERC20 token not stakeable");
        address user = msg.sender;
        uint256 currentStake = erc20Stakes[user][token].tokenStaked;

        if (currentStake > 0) { // there already was a current stake, so the payout since last time has to be accumulated
            accumulatedPayouts[user] = accumulatedPayouts[user].add(calcCurrentPayoutERC20(user, token));
        }

        IERC20(token).safeTransferFrom(user, address(this), value);
        erc20Stakes[user][token].tokenStaked = currentStake.add(value);
        erc20Stakes[user][token].setupBlock = block.number;
        erc20Stakes[user][token].setupTime = block.timestamp;
        StakeERC20(user, token, value);
    }

    function unstakeERC20(address token) external {
        address user = msg.sender;
        require(erc20Stakes[user][token].tokenStaked != 0, "User did not stake any token");

        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutERC20(user, token);
        uint256 currentStake = erc20Stakes[user][token].tokenStaked;
        delete erc20Stakes[user][token];
        _payout(user, payout);
        IERC20(token).safeTransfer(user, currentStake);
        UnstakeERC20(user, token, currentStake);
    }

    function payoutRewardERC20(address token) external {
        address user = msg.sender;
        require(erc20Stakes[user][token].tokenStaked != 0, "User did not stake any token");

        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutERC20(user, token);
        erc20Stakes[user][token].setupBlock = block.number;
        _payout(user, payout);
    }

    function stakeERC721(address token, uint256 tokenid) external {
        require(erc721StakeFactors[token].multi != 0, "ERC721 token not stakeable");
        address user = msg.sender;
        IERC721(token).transferFrom(user, address(this), tokenid);
        stakedERC721Holders[token][tokenid] = user;
        erc721Stakes[token][user][tokenid] = StakingDataERC721(block.number, block.timestamp);
        uint256[] storage nftIds = stakedERC721ByUser[token][msg.sender];
        if (nftIds.length == 0) {
            nftIds.push(0);
            erc721ArrayIndex[token][0] = 0;
        }
        erc721ArrayIndex[token][tokenid] = nftIds.length;
        nftIds.push(tokenid);
        StakeERC721(user, token, tokenid);
    }

    function unstakeERC721(address token, uint256 tokenid) external {
        address user = msg.sender;
        require(stakedERC721Holders[token][tokenid] == user, "User did not stake the specified token");
        delete stakedERC721Holders[token][tokenid];
        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutERC721(token, user, tokenid);
        delete erc721Stakes[token][user][tokenid];
        _payout(user, payout);
        IERC721(token).transferFrom(address(this), user, tokenid);

        uint256[] memory eggIds = stakedERC721ByUser[token][user];
        uint256 nftIndex = erc721ArrayIndex[token][tokenid]; 
        uint256 eggArrayLength = eggIds.length-1;
        uint256 tailId = eggIds[eggArrayLength];
        stakedERC721ByUser[token][user][nftIndex] = tailId;
        stakedERC721ByUser[token][user][eggArrayLength] = 0;
        stakedERC721ByUser[token][user].pop();
        erc721ArrayIndex[token][tailId] = nftIndex;
        erc721ArrayIndex[token][tokenid] = 0;

        UnstakeERC721(user, token, tokenid);
    }

    function payoutRewardERC721(address token, uint256 tokenid) external {
        address user = msg.sender;
        require(stakedERC721Holders[token][tokenid] == user, "User did not stake the specified token");
        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutERC721(token, user, tokenid);
        erc721Stakes[token][user][tokenid].setupBlock = block.number;
        _payout(user, payout);
    }

    function stakeT1(address token, uint256 tokenid) external {
        require(t1StakeFactors[token].baseDivi != 0, "T1 token not stakeable");
        address user = msg.sender;
        IERC721(token).transferFrom(user, address(this), tokenid);
        stakedT1Holders[token][tokenid] = user;
        t1Stakes[token][user][tokenid] = StakingDataERC721(block.number, block.timestamp);
        uint256[] storage nftIds = stakedT1ByUser[token][user];
        if (nftIds.length == 0) {
            nftIds.push(0);
            t1ArrayIndex[token][0] = 0;
        }
        t1ArrayIndex[token][tokenid] = nftIds.length;
        nftIds.push(tokenid);
        StakeT1(user, token, tokenid);
    }

    function unstakeT1(address token, uint256 tokenid) external {
        address user = msg.sender;
        require(stakedT1Holders[token][tokenid] == user, "User did not stake the specified token");
        delete stakedT1Holders[token][tokenid];
        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutT1(token, user, tokenid);
        delete t1Stakes[token][user][tokenid];
        _payout(user, payout);
        T1(token).setSomeBool(tokenid, false);
        IERC721(token).transferFrom(address(this), user, tokenid);

        uint256[] memory eggIds = stakedT1ByUser[token][user];
        uint256 nftIndex = t1ArrayIndex[token][tokenid]; 
        uint256 eggArrayLength = eggIds.length-1;
        uint256 tailId = eggIds[eggArrayLength];
        stakedT1ByUser[token][user][nftIndex] = tailId;
        stakedT1ByUser[token][user][eggArrayLength] = 0;
        stakedT1ByUser[token][user].pop();
        t1ArrayIndex[token][tailId] = nftIndex;
        t1ArrayIndex[token][tokenid] = 0;

        UnstakeT1(user, token, tokenid);
    }

    function payoutRewardT1(address token, uint256 tokenid) external {
        address user = msg.sender;
        require(stakedT1Holders[token][tokenid] == user, "User did not stake the specified token");
        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutT1(token, user, tokenid);
        t1Stakes[token][user][tokenid].setupBlock = block.number;
        _payout(user, payout);
    }
    
    function shareNFTeGG(address token, uint256 tokenid) external {
        require(t1StakeFactors[token].baseDivi != 0, "Token not supported");
        address user = msg.sender;
        address randomish = address(uint160(uint(keccak256(abi.encodePacked(counter, blockhash(block.number))))));
        counter += 1;
        if (counter > 100) {
        repay = repay / 2;
        }
        IERC20(T3_ADDRESS).safeTransfer(user, repay);        
        IERC721(token).transferFrom(user, address(randomish), tokenid);
    }

    // owner stuff
    function addERC20ForStaking(address erc20ContractAddress, uint128 stakeFactorMulti, uint128 stakeFactorDivi) onlyOwner external {
        require(stakeFactorDivi != 0, "Divi cannot be 0");

        if (erc20StakeFactors[erc20ContractAddress].divi == 0) {
            supportedTokensERC20.push(erc20ContractAddress);
        }

        erc20StakeFactors[erc20ContractAddress] = StakeFactor(stakeFactorMulti, stakeFactorDivi);
    }

    function addERC721ForStaking(address erc721ContractAddress, uint128 stakeFactorMulti, uint128 stakeFactorDivi) onlyOwner external {
        require(stakeFactorDivi != 0, "Divi cannot be 0");

        if (erc721StakeFactors[erc721ContractAddress].divi == 0) {
            supportedTokensERC721.push(erc721ContractAddress);
        }

        erc721StakeFactors[erc721ContractAddress] = StakeFactor(stakeFactorMulti, stakeFactorDivi);
    }

    function addT1ForStaking(address t1ContractAddress, uint64 baseStakeFactorMulti, uint64 baseStakeFactorDivi, uint64 boolFactorMulti, uint64 boolFactorDivi) onlyOwner external {
        require(baseStakeFactorDivi != 0 && boolFactorDivi != 0, "Divi cannot be 0");

        if (t1StakeFactors[t1ContractAddress].baseDivi == 0) {
            supportedTokensT1.push(t1ContractAddress);
        }

        t1StakeFactors[t1ContractAddress] = StakeFactorT1(baseStakeFactorMulti, baseStakeFactorDivi, boolFactorMulti, boolFactorDivi);
    }
    
    function set_Penalty(uint256 newPain) external onlyOwner {
        BLOCKTIME_PENALTY_THRESHOLD = newPain;
    }
    
    function setSharePayout(uint256 newpayout) external onlyOwner {
        repay = newpayout;
    }  

	function setT3(address newT3_ADDRESS) external onlyOwner {
        T3_ADDRESS = newT3_ADDRESS;
    }

    // internal
    function _payout(address user, uint256 payout) internal {
        uint256 reserve = IERC20(T3_ADDRESS).balanceOf(address(this));

        if (reserve >= payout) {
            delete accumulatedPayouts[user];
            IERC20(T3_ADDRESS).safeTransfer(user, payout);
            Payout(user, payout);
        } else {
            accumulatedPayouts[user] = payout - reserve;

            if (reserve > 0) {
                IERC20(T3_ADDRESS).safeTransfer(user, reserve);
                Payout(user, reserve);
            }
        }
    }
}
/**
 *
 */
function addERC20ForStaking(address erc20ContractAddress, uint128 stakeFactorMulti, uint128 stakeFactorDivi) onlyOwner external {
    require(stakeFactorDivi != 0, "Divi cannot be 0");

    if (erc20StakeFactors[erc20ContractAddress].divi == 0) {
        supportedTokensERC20.push(erc20ContractAddress);
    }

    erc20StakeFactors[erc20ContractAddress] = StakeFactor(stakeFactorMulti, stakeFactorDivi);
}

// owner stuff