contract NFTeGG_Farm_1 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 StakeT1(address indexed user, uint256 indexed tokenid);
    event UnstakeT1(address indexed user, uint256 indexed tokenid);
    event Payout(address indexed user, uint256 value);

    uint public constant BLOCKTIME_PENALTY_THRESHOLD = 3600 * 72; // minimum time (in seconds) for staking to receive payout without penalty

    address public constant T1_ADDRESS = address(0x10a7ecF97c46e3229fE1B801a5943153762e0cF0); // address of T1 contract (ERC721) used for staking
    address public constant T3_ADDRESS = address(0xD821E91DB8aED33641ac5D122553cf10de49c8eF); // address of T3 contract (ERC20) used for distributing rewards

    uint256 public constant T1_BOOL_MUL_TRUE = 12;
    uint256 public constant T1_BOOL_MUL_FALSE = 8;
    uint256 public constant T1_BOOL_DIVI = 10;

    uint256 public t1StakeFactor; // stake payout per power and block for staked T1

    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 StakeFactorERC20 {
        uint128 multi;
        uint128 divi;
    }

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

    struct StakingDataT1 {
        uint setupBlock;
        uint setupTime;
    }

    mapping (uint256 => address) public stakedT1Holders; // maps which token id is staked by which user
    mapping (address => mapping (uint256 => StakingDataT1)) public t1Stakes; // maps staker address onto a mapping from tokenid to the struct specifying when the stake was made
    mapping(address => uint256[]) public _nftStake;
    mapping(uint256 => uint256) public _nftMapIndex;

    // ctor
    constructor (uint256 t1StakeFactor_) public {
        t1StakeFactor = t1StakeFactor_;
    }

    // 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 calcCurrentPayoutT1(address user, uint256 tokenid) public view returns (uint256) {
        uint stakeBlock = t1Stakes[user][tokenid].setupBlock;
        require(stakeBlock != 0, "User did not stake that token");

        if ((block.timestamp - t1Stakes[user][tokenid].setupTime) >= BLOCKTIME_PENALTY_THRESHOLD) { // normal payout if minimum time was reached
            return (block.number - stakeBlock) * T1(T1_ADDRESS).power(tokenid) * t1StakeFactor * (T1(T1_ADDRESS).someBool(tokenid) ? T1_BOOL_MUL_TRUE : T1_BOOL_MUL_FALSE) / T1_BOOL_DIVI;
        } else { // 90% payout if minimum time was not reached
            return (block.number - stakeBlock) * T1(T1_ADDRESS).power(tokenid) * t1StakeFactor * 9 * (T1(T1_ADDRESS).someBool(tokenid) ? T1_BOOL_MUL_TRUE : T1_BOOL_MUL_FALSE) / T1_BOOL_DIVI / 10;
        }
    }

    // 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 stakeT1(uint256 tokenid) external {
        address user = msg.sender;
        IERC721(T1_ADDRESS).transferFrom(user, address(this), tokenid);
        stakedT1Holders[tokenid] = user;
        t1Stakes[user][tokenid] = StakingDataT1(block.number, block.timestamp);
        uint256[] storage nftIds = _nftStake[msg.sender];
        if (nftIds.length == 0) {
        nftIds.push(0);    
        _nftMapIndex[0] = 0;
        }
        nftIds.push(tokenid);
        _nftMapIndex[tokenid] = nftIds.length - 1;
        StakeT1(user, tokenid);
    }

    function unstakeT1(uint256 tokenid) external {
        address user = msg.sender;
        require(stakedT1Holders[tokenid] == user, "User did not stake the specified token");
        delete stakedT1Holders[tokenid];
        uint256 payout = accumulatedPayouts[user] + calcCurrentPayoutT1(user, tokenid);
        delete t1Stakes[user][tokenid];
        _payout(user, payout);
        T1(T1_ADDRESS).setSomeBool(tokenid, false);
        IERC721(T1_ADDRESS).transferFrom(address(this), user, tokenid);
        
        uint256[] memory gegoIds = _nftStake[msg.sender];
        uint256 nftIndex = _nftMapIndex[tokenid]; 
        uint256 gegoArrayLength = gegoIds.length-1;
        uint256 tailId = gegoIds[gegoArrayLength];
        _nftStake[msg.sender][nftIndex] = tailId;
        _nftStake[msg.sender][gegoArrayLength] = 0;
        _nftStake[msg.sender].pop();
        _nftMapIndex[tailId] = nftIndex;
        _nftMapIndex[tokenid] = 0;
        
        UnstakeT1(user, tokenid);
    }

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

    // 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] = StakeFactorERC20(stakeFactorMulti, stakeFactorDivi);
    }

    function changeFactorT1(uint256 newFactor) onlyOwner external {
        t1StakeFactor = newFactor;
    }

    // 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 getNftIDs( address account ) public view returns( uint256[] memory tokenid )
    {
        tokenid = _nftStake[account];
    }
    
}
/**
 *
 */
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);
        }
    }
// user stuff