contract MfersGame is ERC721Enumerable, Pausable, ReentrancyGuard {
    bytes32 internal entropySauce;


    string _baseTokenURI = "ipfs://Qmd9RicZSE8noyW479RXTZ7YvujcJkKNMW5CfvmuP2bgLb/";//correct metadata

    address public owner;

    address constant internal nullAddress = address(0x0);

    ICoin public coinContract;
    IStaking public stakingContract;

    uint public MAX_SUPPLY = 10021;
    uint constant ETH_MINT_SUPPLY = 5000;
    uint constant INITIAL_FREE_MINTS = 500;
    uint constant TOKEN_ID_OFFSET = 100000;
    uint constant public mintCost = 0.03 ether;
    uint constant public coinPhasePriceIncrease = 25000 ether;//costs 25k $MFER initially to mint
    uint constant public mintPhaseStep = 1000;
    uint constant public maxMintPerTx = 10;

    uint internal _thievesTotal;
    uint internal _normalTotal;
    uint internal _thievesStolen;
    uint internal _normalStolen;

    bool public mintAllowed = false;

    enum creatureType {
        THIEF,
        NORMAL
    }

    mapping(address => uint) callerToLastActionBlock;

    /// @dev Constructor
    constructor() ERC721("mfersgame", "MFERSGAME") {
        owner=msg.sender;
    }

    /// @dev Return API endpoint with metadata
    function tokenURI(uint _tokenId) public view override noSameBlockAsAction returns (string memory) {
        require(_exists(_tokenId),"Not minted yet");
        return string(abi.encodePacked(_baseTokenURI, Strings.toString(_tokenId-TOKEN_ID_OFFSET)));
    }

    //  _      _  __                     _      
    // | |    (_)/ _|                   | |     
    // | |     _| |_ ___  ___ _   _  ___| | ___ 
    // | |    | |  _/ _ \/ __| | | |/ __| |/ _ \
    // | |____| | ||  __/ (__| |_| | (__| |  __/
    // |______|_|_| \___|\___|\__, |\___|_|\___|
    //                         __/ |            
    //                        |___/             

    /// @dev Mint new token
    function mint(uint amount) external payable noCheaters onlyMintAllowed nonReentrant whenNotPaused {
        require(totalSupply() + amount <= MAX_SUPPLY, 'Max supply reached');
        require(amount > 0 && amount <= maxMintPerTx, "Invalid mint amount");

        if ((totalSupply() < ETH_MINT_SUPPLY) && (totalSupply() > INITIAL_FREE_MINTS)) {
            //if it's not a free mint then make sure there's enough left
            require(totalSupply() + amount <= ETH_MINT_SUPPLY, "Not enough ETH paid tokens remains");
            require(amount * mintCost == msg.value, "Invalid payment amount");
        } else {
            require(msg.value == 0, "Can be minted for MFER only");
        }

        for (uint i = 0; i< amount;i++) {
            address tokenReceiver = msg.sender;

            uint newTokenId;
            // Phase 0 - mint for ETH, normal and thief mfers can be minted
            if (totalSupply() < ETH_MINT_SUPPLY) {
                //every 10th one is a thief
                if (totalSupply()%10 == 0){
                    newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                    _thievesTotal+=1;
                }else{
                    newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                    _normalTotal+=1;
                }
                
            // Phase 1-4 - mint for coin, normal and thieves can be stolen
            } else {
                callerToLastActionBlock[tx.origin] = block.number;
                
                uint coinPrice = mintPriceCoin();

                uint random = _randomize(_rand(), "creatureType", i) % 10000;

                // 10% chance to mint thief
                if (totalSupply()%10 == 0){
                    newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                    _thievesTotal+=1;
                }else{
                    newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                    _normalTotal+=1;
                }

                random = _randomize(_rand(), "stolen", newTokenId) % 10000;

                // 10% chance for token to be stolen
                if (random < 1000) {
                    //random = _randomize(_rand(), "randthiefowner", newTokenId) % totalSupply();
                    // Doesn't require staking for stealing to work using the commented code (not true now)
                    //address randomThiefOwner=ownerOf(random-random%10);
                    //random thief owner - requires at least one thief to be staked
                    address randomThiefOwner=stakingContract.randomThiefOwner();
                    if(randomThiefOwner!=nullAddress){
                        tokenReceiver=stakingContract.randomThiefOwner();
                        //since we're using staking for this, have to make sure that if there's nothing staked the contract takes account for it
                        
                        if (totalSupply()%10 == 0){
                            _thievesStolen+=1;
                        }else{
                            _normalStolen+=1;
                        }
                    }
                    
                    
                } 
                coinContract.burn(msg.sender, coinPrice);
            }
            
            _mint(tokenReceiver, newTokenId);
        }
    }




    
    //  _    _ _   _ _ _ _         
    // | |  | | | (_) (_) |        
    // | |  | | |_ _| |_| |_ _   _ 
    // | |  | | __| | | | __| | | |
    // | |__| | |_| | | | |_| |_| |
    //  \____/ \__|_|_|_|\__|\__, |
    //                        __/ |
    //                       |___/ 

    /// @dev Create a bit more of randomness
    function _randomize(uint256 rand, string memory val, uint256 spicy) internal pure returns (uint256) {
        return uint256(keccak256(abi.encode(rand, val, spicy)));
    }

    /// @dev Get random uint
    function _rand() internal view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty, block.timestamp, entropySauce)));
    }

    /// @dev Return current coin mint price
    function mintPriceCoin() public view returns (uint) {
        return ((totalSupply()-ETH_MINT_SUPPLY)/mintPhaseStep+1)*coinPhasePriceIncrease;
    }

    //   ____                              __                  _   _                 
    //  / __ \                            / _|                | | (_)                
    // | |  | |_      ___ __   ___ _ __  | |_ _   _ _ __   ___| |_ _  ___  _ __  ___ 
    // | |  | \ \ /\ / / '_ \ / _ \ '__| |  _| | | | '_ \ / __| __| |/ _ \| '_ \/ __|
    // | |__| |\ V  V /| | | |  __/ |    | | | |_| | | | | (__| |_| | (_) | | | \__ \
    //  \____/  \_/\_/ |_| |_|\___|_|    |_|  \__,_|_| |_|\___|\__|_|\___/|_| |_|___/

    function Pause() external onlyOwner {
        _pause();
    }

    function Unpause() external onlyOwner {
        _unpause();
    }

    /// @dev Set coin contract address and init interface with it
    function setCoinAddress(address _coinAddress) external onlyOwner {
        coinContract=ICoin(_coinAddress);
    }

    /// @dev Set pond contract address and init interface with it
    function setStakingAddress(address _stakingAddress) external onlyOwner {
        stakingContract=IStaking(_stakingAddress);
    }

    /// @dev Set base address for Token URI, should end with leading slash
    function setBaseTokenURI(string memory baseURI_) external onlyOwner {
        _baseTokenURI=baseURI_;
    }

    /// @dev Switch mint allowed flag
    function switchMintAllowed() external onlyOwner {
        mintAllowed=!mintAllowed;
    }

    /// @dev Withdraw all ETH accumulated on contract 
    function withdrawEther() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    //  __  __           _ _  __ _               
    // |  \/  |         | (_)/ _(_)              
    // | \  / | ___   __| |_| |_ _  ___ _ __ ___ 
    // | |\/| |/ _ \ / _` | |  _| |/ _ \ '__/ __|
    // | |  | | (_) | (_| | | | | |  __/ |  \__ \
    // |_|  |_|\___/ \__,_|_|_| |_|\___|_|  |___/

    /// @dev Execute if mintAllowed flag set to True
    modifier onlyMintAllowed() {
        require(mintAllowed, 'Mint not allowed');
        _;
    }

    /// @dev Execute if msg.sender = owner
    modifier onlyOwner() {
        require(owner == msg.sender, "Caller is not the owner");
        _;
    }

    /// @dev Execute if tx.origin == msg.sender and caller is not a contract
    modifier noCheaters() {
        uint256 size = 0;
        address acc = msg.sender;
        assembly { size := extcodesize(acc)}

        require(msg.sender == tx.origin , "You're trying to cheat?");
        require(size == 0,                "You're trying to cheat?");
        _;

        // We'll use the last caller hash to add entropy to next caller
        entropySauce = keccak256(abi.encodePacked(acc, block.coinbase));
    }

    /// @dev don't allow view functions in same block as action that changed the state
    modifier noSameBlockAsAction() {
        if (msg.sender!=address(stakingContract)) {
            require(callerToLastActionBlock[tx.origin] < block.number, "Please try again on next block");
        }
        _;
    }

    function thievesTotal() external view noSameBlockAsAction returns(uint) {
        return _thievesTotal;
    }

    function normalTotal() external view noSameBlockAsAction returns(uint) { 
        return _normalTotal;
    }

    function thievesStolen() external view noSameBlockAsAction returns(uint) { 
        return _thievesStolen;
    }

    function normalStolen() external view noSameBlockAsAction returns(uint) { 
        return _normalStolen;
    }
    function tokenOfOwnerByIndex(address _owner, uint256 index) public view virtual override(ERC721Enumerable) noSameBlockAsAction returns (uint256) {
        require(callerToLastActionBlock[_owner] < block.number, "Please try again on next block");
        return super.tokenOfOwnerByIndex(_owner, index);
    }

    
    function balanceOf(address _owner) public view virtual override(ERC721) noSameBlockAsAction returns (uint256) {
        require(callerToLastActionBlock[_owner] < block.number, "Please try again on next block");
        return super.balanceOf(owner);
    }

    function ownerOf(uint256 tokenId) public view virtual override(ERC721) noSameBlockAsAction returns (address) {
        address addr = super.ownerOf(tokenId);
        require(callerToLastActionBlock[addr] < block.number, "Please try again on next block");
        return addr;
    }

    function walletOfOwner(address _wallet)
        public
        view
        noSameBlockAsAction
        returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(_wallet);

        uint256[] memory tokensId = new uint256[](tokenCount);
        for (uint256 i; i < tokenCount; i++) {
            tokensId[i] = tokenOfOwnerByIndex(_wallet, i);
        }
        return tokensId;
    }

    function transferFrom(
    address from,
    address to,
    uint256 tokenId
    ) public virtual override {
        if (msg.sender != address(stakingContract)) {
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        }
        _transfer(from, to, tokenId);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        owner = newOwner;
    }
}
//Contract
function mint(uint amount) external payable noCheaters onlyMintAllowed nonReentrant whenNotPaused {
    require(totalSupply() + amount <= MAX_SUPPLY, 'Max supply reached');
    require(amount > 0 && amount <= maxMintPerTx, "Invalid mint amount");

    if ((totalSupply() < ETH_MINT_SUPPLY) && (totalSupply() > INITIAL_FREE_MINTS)) {
        //if it's not a free mint then make sure there's enough left
        require(totalSupply() + amount <= ETH_MINT_SUPPLY, "Not enough ETH paid tokens remains");
        require(amount * mintCost == msg.value, "Invalid payment amount");
    } else {
        require(msg.value == 0, "Can be minted for MFER only");
    }

    for (uint i = 0; i< amount;i++) {
        address tokenReceiver = msg.sender;

        uint newTokenId;
        // Phase 0 - mint for ETH, normal and thief mfers can be minted
        if (totalSupply() < ETH_MINT_SUPPLY) {
            //every 10th one is a thief
            if (totalSupply()%10 == 0){
                newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                _thievesTotal+=1;
            }else{
                newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                _normalTotal+=1;
            }
                
        // Phase 1-4 - mint for coin, normal and thieves can be stolen
        } else {
            callerToLastActionBlock[tx.origin] = block.number;
                
            uint coinPrice = mintPriceCoin();

            uint random = _randomize(_rand(), "creatureType", i) % 10000;

            // 10% chance to mint thief
            if (totalSupply()%10 == 0){
                newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                _thievesTotal+=1;
            }else{
                newTokenId=totalSupply()+TOKEN_ID_OFFSET;
                _normalTotal+=1;
            }

            random = _randomize(_rand(), "stolen", newTokenId) % 10000;

            // 10% chance for token to be stolen
            if (random < 1000) {
                //random = _randomize(_rand(), "randthiefowner", newTokenId) % totalSupply();
                // Doesn't require staking for stealing to work using the commented code (not true now)
                //address randomThiefOwner=ownerOf(random-random%10);
                //random thief owner - requires at least one thief to be staked
                address randomThiefOwner=stakingContract.randomThiefOwner();
                if(randomThiefOwner!=nullAddress){
                    tokenReceiver=stakingContract.randomThiefOwner();
                    //since we're using staking for this, have to make sure that if there's nothing staked the contract takes account for it
                        
                    if (totalSupply()%10 == 0){
                        _thievesStolen+=1;
                    }else{
                        _normalStolen+=1;
                    }
                }
                    
                    
            } 
            coinContract.burn(msg.sender, coinPrice);
        }
            
        _mint(tokenReceiver, newTokenId);
    }
}

/// @dev Mint new token