contract NFTRewardB is Governance {
    using SafeERC20 for IERC20;
    using SafeMath for uint256;
    
    IERC20 public _artd = IERC20(0xA23F8462d90dbc60a06B9226206bFACdEAD2A26F);
    IAnftToken public _anftToken = IAnftToken(0x99a7e1188CE9a0b7514d084878DFb8A405D8529F);
    address public _anftFactory = 0x448364834cb97e6dddF153b8a40CC7bb8D3bB82D;
    address public _playerLink = address(0x4eD7A3721F203Cf108b4279061B36CC20b14E57A);

    address public _teamWallet = 0x3b2b4f84cFE480289df651bE153c147fa417Fb8A;
    address public _rewardPool = 0x4D732FA01032b41eE0fA152398B22Bfab6689DCb;

    uint256 public constant DURATION = 30 days;
    uint256 public _initReward = 700 * 1e18;
    
    uint256 public _startTime =  now + 3650 days;
    uint256 public _periodFinish = 0;
    uint256 public _rewardRate = 0;
    uint256 public _lastUpdateTime;
    uint256 public _rewardPerWeightStored;

    uint256 public _teamRewardRate = 500;
    uint256 public _poolRewardRate = 1000;
    uint256 public _baseRate = 10000;
    uint256 public _punishTime = 5 days;
  
    mapping(address => uint256) public _userRewardPerWeightPaid;
    mapping(address => uint256) public _userrewards;
    mapping(uint256 => uint256) public _idRewardPerWeightPaid;
    mapping(uint256 => uint256) public _idrewards;
    mapping(address => uint256) public _lastStakedTime;

    bool public _hasStart = false;
    bool public _hasRewardStart = false;
    uint256 public _fixRateBase = 100000;
    
    uint256 public _totalWeight;
    mapping(address => uint256) public _weightBalances;
    mapping(address => uint256) public _artdBalances;
    mapping(address => uint256[]) public _playerAnft;
    mapping(uint256 => uint256) public _anftMapIndex;
    
    uint256 private _totalPower;
    mapping(address => uint256) private _powerBalances;

    struct User {
        address owner;
        uint256 nftId;
        uint index;
    }
    mapping (uint256 => User) private users;
    uint256[] private usersRecords;

    event ReadyStake(uint256 timestamp);
    event RewardReserved(uint256 reward);
    event StakedANft(address indexed user, uint256 anftId);
    event WithdrawnANft(address indexed user, uint256 anftId);
    event RewardPaid(address indexed user, uint256 reward);
    event NFTReceived(address operator, address from, uint256 tokenId, bytes data);
    event NewJoinId(uint256 indexed anftId, uint index, address owner);

    modifier checkHalve() {
        if (block.timestamp >= _periodFinish) {
            _initReward = _initReward.mul(50).div(100);

            _artd.mint(address(this), _initReward);

            _rewardRate = _initReward.div(DURATION);
            _periodFinish = block.timestamp.add(DURATION);
            emit RewardReserved(_initReward);
        }
        _;
    }
    
    modifier checkStart() {
        require(block.timestamp > _startTime, "not start");
        _;
    }

    modifier updateReward(address account) {
        _rewardPerWeightStored = rewardPerWeight();
        _lastUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) {
            _userrewards[account] = earned(account);
            _userRewardPerWeightPaid[account] = _rewardPerWeightStored;
        }
        _;
    }
    
    modifier updateIdReward(uint256 anftId) {
        _rewardPerWeightStored = rewardPerWeight();
        _lastUpdateTime = lastTimeRewardApplicable();
        if (anftId != 0) {
            _idrewards[anftId] = earnedId(anftId);
            _idRewardPerWeightPaid[anftId] = _rewardPerWeightStored;
        }
        _;
    }  

    function addUserId(uint256 _nftid) private returns(bool success)
    {
        users[_nftid].nftId = _nftid;
        users[_nftid].owner = msg.sender;
        users[_nftid].index = usersRecords.push(_nftid) -1;
        emit NewJoinId( _nftid, users[_nftid].index, users[_nftid].owner );
        return true;
    }

    function quitUser(uint256 _nftid) private returns(uint index)
    {
        uint toDelete = users[_nftid].index;
        uint256 lastIndex = usersRecords[usersRecords.length-1];
        usersRecords[toDelete] = lastIndex;
        users[lastIndex].index = toDelete; 
        usersRecords.length--;
        return toDelete;   
    }    
    
    function getStaker(uint256 _nftid) public view returns( address )
    {
        return users[_nftid].owner;
    }   

    function UserRecord(uint256 number) public view returns( uint256 )
    {
        return usersRecords[number];
    }   

    function stakeInfo(uint256 number) public view returns( uint256, address, uint256, uint256 )
    {
        uint256 nftid = usersRecords[number];
        address staker = users[nftid].owner;
        uint256 weight = getNftWeight(nftid);
        uint256 earnedbyid = earnedId(nftid);
        return (nftid, staker, weight, earnedbyid);
    }   

    function stakeCount() public view returns( uint )
    {
        return usersRecords.length;
    }  

    function setNftFactoy(address anftfactory) external onlyGovernance{
        _anftFactory = anftfactory;
    }
    
    /* Fee collection for any other token */
    function seize(IERC20 token, uint256 amount) external onlyGovernance{
        require(token != _artd, "reward");
        token.safeTransfer(_governance, amount);
    }
    
    function lastTimeRewardApplicable() public view returns (uint256) {
        return Math.min(block.timestamp, _periodFinish);
    }

    function rewardPerWeight() public view returns (uint256) {
       
        if( _hasRewardStart == false){
            return _rewardPerWeightStored;
        }
          
        if (totalWeight() == 0) {
            return _rewardPerWeightStored;
        }
        
        if( lastTimeRewardApplicable() >= _startTime )
        {
            return  _rewardPerWeightStored.add(
                        lastTimeRewardApplicable()
                        .sub(_lastUpdateTime)
                        .mul(_rewardRate)
                        .mul(1e18)
                        .div(totalWeight())
                     );
        }
        else
            return _rewardPerWeightStored;
    }

    function earned(address account) public view returns (uint256) {
        return
            getWeight(account)
                .mul(rewardPerWeight().sub(_userRewardPerWeightPaid[account]))
                .div(1e18)
                .add(_userrewards[account]);
    }

    function earnedId(uint256 anftId) public view returns (uint256) {
        return
            getNftWeight(anftId)
                .mul(rewardPerWeight().sub(_idRewardPerWeightPaid[anftId]))
                .div(1e18)
                .add(_idrewards[anftId]);
    }
    
    function getNftWeight( uint256 anftId ) public view returns ( uint256 )
    {
        uint256 era;
        uint256 grade;
        uint256 promote;
        uint256 artdAmount;
        uint256 apwrAmount;
        uint256 skill;
        address factory;

        NFTFactory _nftfactory =  NFTFactory(_anftFactory);
        (era, grade, promote, artdAmount, apwrAmount, skill, factory) = _nftfactory.getMeta(anftId);

        require(artdAmount > 0,"No ARTD amount error");
        uint256 weight = 0;
        require(era > 0, "era zero error");
           
        uint256 apwr = apwrAmount.div(1e15).div(2);
        uint256 prom = promote.div(500);

        weight = grade + apwr + prom;

        return weight;
    }

    function getWeight(address account) public view returns (uint256) {
        return _weightBalances[account];
    }

    function totalWeight() public view returns (uint256) {
        return _totalWeight;
    }
    
    
    function stakeAnft(uint256 anftId, string memory affCode)
        public
        updateReward(msg.sender)
        checkHalve
    {
        uint256[] storage anftIds = _playerAnft[msg.sender];
        if (anftIds.length == 0) {
            anftIds.push(0);    
            _anftMapIndex[0] = 0;
        }
        anftIds.push(anftId);
        _anftMapIndex[anftId] = anftIds.length - 1;
        
        if( _idRewardPerWeightPaid[anftId] == 0 )
        {
            _idRewardPerWeightPaid[anftId] = rewardPerWeight();
            _idrewards[anftId] = 0;
        }

        address owner = NFTownerOf(anftId);
        require(msg.sender == owner);
        
        uint256 stakeweight = 0;

        require(anftId > 0, "Cannot stake zero ID");
        stakeweight = getNftWeight(anftId);

        _totalWeight = _totalWeight.add(stakeweight);
        _weightBalances[msg.sender] = _weightBalances[msg.sender].add(stakeweight);

        _anftToken.safeTransferFrom(msg.sender, address(this), anftId);

        if (!IPlayerLink(_playerLink).hasRefer(msg.sender)) {
            IPlayerLink(_playerLink).bindRefer(msg.sender, affCode);
        }
        
        addUserId(anftId);
        _lastStakedTime[msg.sender] = now;
        emit StakedANft(msg.sender, anftId);        
   
    }
    
    function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) public returns (bytes4) {
        if(_hasStart == false) {
            return 0;
        }

        emit NFTReceived(operator, from, tokenId, data);
        return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
    }

    function withdrawAnft(uint256 anftId)
        public
        updateReward(msg.sender)
        updateIdReward(anftId)
        checkHalve
        checkStart
    {
        require(anftId > 0, "the anftId error");
        
        uint256[] memory anftIds = _playerAnft[msg.sender];
        uint256 anftIndex = _anftMapIndex[anftId];
        
        require(anftIds[anftIndex] == anftId, "not AnftId owner");

        uint256 anftArrayLength = anftIds.length-1;
        uint256 tailId = anftIds[anftArrayLength];

        _playerAnft[msg.sender][anftIndex] = tailId;
        _playerAnft[msg.sender][anftArrayLength] = 0;
        _playerAnft[msg.sender].length--;
        _anftMapIndex[tailId] = anftIndex;
        _anftMapIndex[anftId] = 0;

        uint256 weight = 0;

        weight = getNftWeight(anftId);

        if( weight > _totalWeight )
        {
            _totalWeight = 0;
        }
        else
        {
            _totalWeight = _totalWeight.sub(weight);
        }
        
        if( weight > _powerBalances[msg.sender])
        {
            _weightBalances[msg.sender] = 0;
        }
        else
        {
            _weightBalances[msg.sender] = _weightBalances[msg.sender].sub(weight);
        }
        
        _anftToken.safeTransferFrom(address(this), msg.sender, anftId);
        quitUser(anftId);
        
        emit WithdrawnANft(msg.sender, anftId);
    }        

    function withdrawall()
        public
        checkStart
    {
        uint256[] memory anftId = _playerAnft[msg.sender];
        for (uint8 index = 1; index < anftId.length; index++) {
            if (anftId[index] > 0) {
                withdrawAnft(anftId[index]);
            }
        }
    }

    function getPlayerIds( address account ) public view returns( uint256[] memory anftId )
    {
        anftId = _playerAnft[account];
    }

    function exit() external {
        withdrawall();
        getReward();
    }

    function getReward() public 
      updateReward(msg.sender) 
      checkHalve 
      checkStart 
    {
        uint256 reward = earned(msg.sender);
        if (reward > 0) {
            _userrewards[msg.sender] = 0;

            uint256 fee = IPlayerLink(_playerLink).settleReward(msg.sender, reward);
            if(fee > 0){
                _artd.safeTransfer(_playerLink, fee);
            }
            
            uint256 teamReward = reward.mul(_teamRewardRate).div(_baseRate);
            if(teamReward>0){
                _artd.safeTransfer(_teamWallet, teamReward);
            }
            uint256 leftReward = reward.sub(fee).sub(teamReward);
            uint256 poolReward = 0;

            if(now  < (_lastStakedTime[msg.sender] + _punishTime) ){
                poolReward = leftReward.mul(_poolRewardRate).div(_baseRate);
            }
            if(poolReward>0){
                _artd.safeTransfer(_rewardPool, poolReward);
                leftReward = leftReward.sub(poolReward);
            }

            if(leftReward>0){
                _artd.safeTransfer(msg.sender, leftReward );
            }
    
            uint256[] memory anftId = _playerAnft[msg.sender];
            uint256 nftid = 0;
            
            for (uint8 index = 1; index < anftId.length; index++) {
                if (anftId[index] > 0) 
                {
                    nftid = anftId[index];
                    _idrewards[nftid] = 0;
                    _idRewardPerWeightPaid[nftid] = 0;
                }
            }
        
            emit RewardPaid(msg.sender, leftReward);
        }
 
    }

    function readyNFTStake( )
        external
        onlyGovernance
    {
        require(_hasStart == false, "has started");
        _hasStart = true;

         _startTime = now;
         
        emit ReadyStake(now);
    }

    // set Timestamp to start NFTReward
    function startNFTReward(uint256 startTime)
        external
        onlyGovernance
        updateReward(address(0))
        updateIdReward(0)
    {
        require(_hasStart == true, "To start NFTStake firstly");
        _hasRewardStart = true;
        
        _startTime = startTime;

        _rewardRate = _initReward.div(DURATION); 
        _artd.mint(address(this), _initReward);

        _lastUpdateTime = _startTime;
        _periodFinish = _startTime.add(DURATION);

        emit RewardReserved(_initReward);
    }


    // Extend reward for increase players
    function reserveMintAmount(uint256 reward)
        external
        onlyGovernance
        updateReward(address(0))
        updateIdReward(0)
    {
        _artd.mint(address(this), reward);
        if (block.timestamp >= _periodFinish) {
            _rewardRate = reward.div(DURATION);
        } else {
            uint256 remaining = _periodFinish.sub(block.timestamp);
            uint256 leftover = remaining.mul(_rewardRate);
            _rewardRate = reward.add(leftover).div(DURATION);
        }
        _lastUpdateTime = block.timestamp;
        _periodFinish = block.timestamp.add(DURATION);
        emit RewardReserved(reward);
    }

    function setTeamRewardRate( uint256 teamRewardRate ) public onlyGovernance {
        _teamRewardRate = teamRewardRate;
    }

    function setPoolRewardRate( uint256  poolRewardRate ) public onlyGovernance{
        _poolRewardRate = poolRewardRate;
    }

    function setWithDrawPunishTime( uint256  punishTime ) public onlyGovernance{
        _punishTime = punishTime;
    }

    /**
     * @dev Gets the owner of the NFT ID
     * @param nftId uint256 ID of the token to query the owner of
     * @return owner address currently marked as the owner of the given NFT ID
     */
    function NFTownerOf(uint256 nftId) public view returns (address) {
        IAnftToken _anftx =  IAnftToken(_anftToken);
        address owner = _anftx.ownerOf(nftId);
        require(owner != address(0));
        return owner;
    }
    
    function IsReward() public view returns (bool) {
        return (block.timestamp > _lastUpdateTime);
    }
    
    function rewardStarttime() public view returns (uint256) {
        return _startTime;
    }
    
}
function NFTownerOf(uint256 nftId) public view returns (address) {
    IAnftToken _anftx =  IAnftToken(_anftToken);
    address owner = _anftx.ownerOf(nftId);
    require(owner != address(0));
    return owner;
}

/**
 * @dev Gets the owner of the NFT ID
 * @param nftId uint256 ID of the token to query the owner of
 * @return owner address currently marked as the owner of the given NFT ID
 */