contract Chimera is
    ERC721Enumerable,
    Ownable,
    ERC721Burnable,
    ERC721Pausable,
    ReentrancyGuard,
    ERC2981ContractWideRoyalties 
{
    using SafeMath for uint256;
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdTracker;

    uint256 public constant MAX_ELEMENTS = 10101;
    uint256 public PRICE = 3 * 10**16; //0.03 ETH
    uint256 public PRICE1 = 15 * 10**14; //0.0015 ETH potion
    uint256 public PRICE2 = 20 * 10**14; //0.0020 ETH potion
    uint256 public PRICE3 = 25 * 10**14; //0.0025 ETH potion
    uint256 public PRICE4 = 50 * 10**14; //0.0050 ETH super and mystery potion
    uint256 public constant MAX_BY_MINT = 1;
    uint256 public MAX_BY_ADDRESS;
    address public creatorAddress = 0xFc099dDd857a105adEca96425122D41Db9bedf4F;
    address public devAddress = 0x0e2282583EFd4f6302D9646F49965353327CDA1A;
    string public baseTokenURI;
    bool public isPresale = true;

    address internal constant _DEFAULT_ROYALTY_ADDRESS = 0x0d8FC58353754dbDd91eA505Fb02E2C725C348F1;
    uint16 internal constant _DEFAULT_ROYALTY_BASIS = 1000; // 10%

    string MSG_NOT_ENOUGHT_BALANCE     = "Not Enought Balance";
    string MSG_YOU_ALREADY_STRONG      = "You already strong";
    string MSG_MAX_LIMIT               = "Max limit";
    string MSG_SALE_END                = "Sale end";
    string MSG_EXCEEDS_NUMBER          = "Exceeds number";
    string MSG_EXCEEDS_BY_ADDRESS      = "Exceeds mint allowed per address";
    string MSG_VALUE_BELOW_PRICE       = "Value below price";    
    string MSG_ONLY_OWNER_NFT_REST     = "Only owner of the NFT can put it to rest";
    string MSG_NFT_ALREADY_RESTING     = "Your NFT is already resting";
    string MSG_NFT_NOT_RESTING         = "Your NFT is not resting";
    string MSG_ONLY_OWNER_NFT_NOT_REST = "Only owner of the NFT can take it out of rest";
    string MSG_TOKENS_STILL_LOCKED     = "Tokens are still locked";
    string MSG_NFT_PLAYING_ALREADY     = "Your NFT is playing already";
    string MSG_NFT_IS_RESTING          = "Your NFT is resting check if you can stop resting in order to battle again";
    string MSG_NFT_ZERO_HEALTH         = "Your Chimera has zero health, please replenish his health with potions";
    string MSG_ERROR                   = "Error";
    string MSG_PAUSE                   = "Pausable: paused";
    string MSG_INVALID_POTION          = "Invalid feeding potion, valid potions: 2, 3, 4, 5, 6, 7, 8, 9 and 10";
    string MSG_ARRAY_NOT_WHITELIST     = "Address not whitelisted";

    struct EachMonster{
        string name;
        uint winnings;
        uint losings;
        uint health;
        uint strength; //attack
        uint stamina; //defense
        uint reflex; //speed
        uint perk; 
        bool training; //check if resting
        uint unlockDate; // resting unlock
        uint lockedDay; // resting day
        bool battleCreated; //checks if already created battle
    } 
    EachMonster[] public monsters; 

    struct Battle {
        uint index;
        uint p1CarId;
        uint scoreP1;
        uint scoreP2;
        uint p2CarId;
        uint ended;
        uint winnerId;
        uint createdAt;
    }
    Battle[] public savas;
    
    mapping(address => bool) public whitelist;

    mapping(uint256 => address) public idToOwner; 

    event CreateChimera(uint256 indexed id);

    constructor(string memory baseURI) ERC721("Chimera", "CHIM") {
        setBaseURI(baseURI);
        pause(true);
        MAX_BY_ADDRESS = 1;
        setMainAddress(_DEFAULT_ROYALTY_ADDRESS, _DEFAULT_ROYALTY_BASIS, creatorAddress);
    }

    modifier saleIsOpen() {
        require(_totalSupply() <= MAX_ELEMENTS, MSG_SALE_END);
        if (_msgSender() != owner()) {
            require(!paused(), MSG_PAUSE);
        }
        _;
    }
    //clear all battle array just in case
    function clearBattle () public onlyOwner{
        delete savas;
    }
    //create battle
    function createBattle (uint _p1CarId) public nonReentrant{
        EachMonster storage m = monsters[_p1CarId];
        require(ownerOf(_p1CarId) == msg.sender);
        require(m.battleCreated == false, MSG_NFT_PLAYING_ALREADY);
        require(m.training == false, MSG_NFT_IS_RESTING);
        require(m.health > 0,MSG_NFT_ZERO_HEALTH);
        uint id = savas.length;
        uint _score = m.stamina + m.reflex;
        m.battleCreated = true;
        savas.push(Battle(id, _p1CarId, _score, 0, 0, 0, _p1CarId, block.timestamp));
    }
 
    //accept battle
    function acceptBattle (uint _battleId, uint _p2CarId) public nonReentrant{
        Battle storage b = savas[_battleId];  
        require(b.ended == 0,MSG_ERROR);
        require(ownerOf(_p2CarId) == msg.sender);
        require(b.p1CarId != _p2CarId,MSG_ERROR); //prevent same token battle
        require(ownerOf(_p2CarId) != ownerOf(b.p1CarId)); //prevent same address battle
        EachMonster storage m = monsters[_p2CarId];
        EachMonster storage m2 = monsters[b.p1CarId];
        require(m.training == false, MSG_NFT_IS_RESTING);
        require(m.health > 0,MSG_NFT_ZERO_HEALTH);
        b.p2CarId = _p2CarId;
        b.scoreP2 = m.strength + m.reflex;
        m2.battleCreated = false;
        if(b.scoreP2 <= b.scoreP1) {
            b.winnerId = b.p1CarId;
            b.ended = 1;
        } else if (b.scoreP2 > b.scoreP1) {
            b.winnerId = b.p2CarId;
            b.ended = 1;
        } else {
            b.ended = 1;
        }
    }

    //end battle
    function endBattle (uint _battleId) public nonReentrant{
        Battle storage b = savas[_battleId];
        require(b.ended == 1,MSG_ERROR);
        if(b.scoreP2 <= b.scoreP1) {
            require(ownerOf(b.p1CarId) == msg.sender);
            upgradeWinner(b.p1CarId,1);
            upgradeLoser(b.p2CarId);
            b.ended = 2;
        } else if (b.scoreP2 > b.scoreP1) {
            require(ownerOf(b.p2CarId) == msg.sender);
            upgradeWinner(b.p2CarId,2);
            upgradeLoser(b.p1CarId);
            b.ended = 2;
        }
        clearBattleElement(_battleId);
    }

    // Start Resting to earn Health based on user desirable period
    function startRest(uint256 _tokenId, uint256 day) public {
        EachMonster storage m = monsters[_tokenId];
        require(ownerOf(_tokenId) == msg.sender, MSG_ONLY_OWNER_NFT_REST);
        require(m.training == false, MSG_NFT_ALREADY_RESTING);
        uint256 until = day*86400;

        m.training = true;
        m.unlockDate = until + block.timestamp;
        m.lockedDay = day;
    }

    // Stop the Resting and release NFT
    function leaveRest(uint256 _tokenId) public {
        EachMonster storage m = monsters[_tokenId];
        require(m.training == true, MSG_NFT_NOT_RESTING);
        require(ownerOf(_tokenId) == msg.sender, MSG_ONLY_OWNER_NFT_NOT_REST);
        
        require(block.timestamp >= m.unlockDate, MSG_TOKENS_STILL_LOCKED);
        uint addedHealth = m.lockedDay*2;
        m.health += addedHealth;
        m.training = false;
        m.unlockDate = 0;
        m.lockedDay = 0;
    }

    // Move the last element to the deleted spot.
    // Remove the last element.
    function clearBattleElement(uint index) internal {
        require(index < savas.length);
        savas[index] = savas[savas.length-1];
        savas.pop();
    }

    //upgrade chimeras
    function upgradeWinner(uint _carId, uint condition) internal {
        EachMonster storage m = monsters[_carId];
        m.winnings += 1;
        if(condition == 1){
         m.stamina += 5;
        }else{
         m.strength += 5; 
        }
        m.reflex += 1;  
    }

    function upgradeLoser(uint _carId) internal {
        EachMonster storage m = monsters[_carId];
        m.losings += 1;
        uint randomchance = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce))) % 3;
        nonce++;
        if(randomchance == 1){
            if (m.strength > 0) {
                m.strength -= 1;
            } else if (m.strength == 0) {
                m.strength = 0;
            }
        } else if(randomchance == 2){
            if (m.stamina > 0) {
                m.stamina -= 1;
            } else if (m.stamina == 0) {
                m.stamina = 0;
            }
        } else{
            if (m.reflex > 0) {
                m.reflex -= 1;
            } else if (m.reflex == 0) {
                m.reflex = 0;
            }
        }

        if (m.health > 0) {
            m.health -= 1;
        } else if (m.health == 0) {
            m.health = 0;
        }
    }

    //get battle counts
    function getBattlesCount() public view returns(uint) {
        return savas.length;
    }

    function _totalSupply() internal view returns (uint256) {
        return _tokenIdTracker.current();
    }

    function totalMint() public view returns (uint256) {
        return _totalSupply();
    }

    uint nonce = 0;
    function random() internal returns (uint) {
        uint randomnumber = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce))) % 750;
        randomnumber = randomnumber + 100;
        nonce++;
        return randomnumber;
    }

    function setMainAddress(address _royaltyAddress, uint256 _royaltyValue, address _creatorAddress) public onlyOwner {
        _setRoyalties(_royaltyAddress, _royaltyValue);
        creatorAddress = _creatorAddress;
    }

    function mint(string memory _name, address _to, uint256 _count) public payable saleIsOpen nonReentrant{
        uint256 total = _totalSupply();
        uint256 balance = balanceOf(_to);
        bool isOwner = _msgSender() == owner();
        bool isWhite = isPresale ? whitelist[_to] : true;
        require(total + _count <= MAX_ELEMENTS, MSG_MAX_LIMIT);
        require(total <= MAX_ELEMENTS, MSG_SALE_END);
        require(isOwner || _count <= MAX_BY_MINT, MSG_EXCEEDS_NUMBER);
        require(isOwner || balance < MAX_BY_ADDRESS, MSG_EXCEEDS_BY_ADDRESS);
        require(isOwner || msg.value >= price(_count), MSG_VALUE_BELOW_PRICE);
        require(isOwner || isWhite, MSG_ARRAY_NOT_WHITELIST);

        for (uint256 i = 0; i < _count; i++) {
            _mintAnElement(_name, _to);
        }
    }

    function _mintAnElement(string memory _name, address _to) private {
        uint256 id = _totalSupply();
        _tokenIdTracker.increment();
        _safeMint(_to, id);
        emit CreateChimera(id);
        idToOwner[id] = msg.sender; 
        monsters.push(EachMonster(_name, 0, 0, 10, random(), random(), random(), random(), false, 0, 0, false )); 
    }

    function getPotionPrices() public view returns (uint256, uint256, uint256, uint256){
        return (PRICE1,PRICE2,PRICE3,PRICE4);
    }
    
    function setPotionPrices(uint256 _newPotion1,uint256 _newPotion2,uint256 _newPotion3,uint256 _newPotion4) external onlyOwner {
        PRICE1 = _newPotion1;
        PRICE2 = _newPotion2;
        PRICE3 = _newPotion3;
        PRICE4 = _newPotion4;
    }

    function feedMonsterWithPotion (uint _monsterID, uint _potionID) public payable nonReentrant{
        
        require(ownerOf(_monsterID) == msg.sender);
        require(_potionID >= 2 && _potionID <= 10, MSG_INVALID_POTION);        
        EachMonster storage monster1 =  monsters[_monsterID];
        if(_potionID == 2){
            require(msg.value == PRICE1, MSG_NOT_ENOUGHT_BALANCE);
            monster1.health += 2;
        }
        else if(_potionID == 3){
            require(msg.value == PRICE2, MSG_NOT_ENOUGHT_BALANCE);
            monster1.health += 2;
            monster1.strength += 4; 
        }
        else if(_potionID == 4){
            require(msg.value == PRICE2, MSG_NOT_ENOUGHT_BALANCE);
            monster1.health += 2;
            monster1.stamina += 4; 
        }
        else if(_potionID == 5){
            require(msg.value == PRICE3, MSG_NOT_ENOUGHT_BALANCE);
            monster1.health += 3;
            monster1.strength += 4;
            monster1.stamina += 2;
        }
        else if(_potionID == 6){
            require(msg.value == PRICE3, MSG_NOT_ENOUGHT_BALANCE);
            monster1.health += 3;
            monster1.strength += 2;
            monster1.stamina += 4;
        }
        else if(_potionID == 7){
            require(msg.value == PRICE4, MSG_NOT_ENOUGHT_BALANCE);
            require(monster1.strength <= 250, MSG_YOU_ALREADY_STRONG);
            monster1.strength += 300;
        }
        else if(_potionID == 8){
            require(msg.value == PRICE4, MSG_NOT_ENOUGHT_BALANCE);
            require(monster1.stamina <= 250, MSG_YOU_ALREADY_STRONG);
            monster1.stamina += 300;
        }
        else if(_potionID == 9){
            require(msg.value == PRICE4, MSG_NOT_ENOUGHT_BALANCE);
            require(monster1.reflex <= 250, MSG_YOU_ALREADY_STRONG);
            monster1.reflex += 300;
        }
        else if(_potionID == 10){
            require(msg.value == PRICE4, MSG_NOT_ENOUGHT_BALANCE);

            uint randomchance = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce))) % 3;
            nonce++;
            if(randomchance == 1){
                uint randomspeed = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce))) % 100;
                nonce++;
                monster1.reflex += randomspeed;
            } else if(randomchance == 2){
                uint randomattack = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce))) % 100;
                nonce++;
                monster1.strength += randomattack;
            } else{
                uint randomdefense = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce))) % 100;
                nonce++;
                monster1.stamina += randomdefense;
            }
        }
    }
   
    //change NFT name
    function changeNFTName (uint _monsterID, string memory _newName) public nonReentrant {
        require(ownerOf(_monsterID) == msg.sender);
        EachMonster storage monster1 =  monsters[_monsterID];
        monster1.name = _newName;
    }

    function price(uint256 _count) public view returns (uint256) {
        return PRICE.mul(_count);
    }
    
    // Make it possible to change the price: just in case
    function setPrice(uint256 _newPrice) external onlyOwner {
        PRICE = _newPrice;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
    }

    function setBaseURI(string memory baseURI) public onlyOwner {
        baseTokenURI = baseURI;
    }

    function walletOfOwner(address _owner)
        external
        view
        returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(_owner);

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

        return tokensId;
    }

    function pause(bool val) public onlyOwner {
        if (val == true) {
            _pause();
            return;
        }
        _unpause();
    }

    function presale(bool val) public onlyOwner {
        isPresale = val;
    }

    function clearAll() public onlyOwner { 
        address payable Owner = payable(msg.sender); 
        Owner.transfer(address(this).balance); 
    }

    function withdrawAll() public payable onlyOwner {
        uint256 _creator = address(this).balance * 95/100;
        uint256 _dev = address(this).balance * 5/100;
        require(payable(creatorAddress).send(_creator));
        require(payable(devAddress).send(_dev));
    }

    function setWhitelist(bytes32[] memory _whitelist) external onlyOwner {
        uint256 count1 = _whitelist.length;
        for(uint256 i = 0 ; i < count1 ; i++){
            address addrs = address(uint160(bytes20(_whitelist[i])));            
            whitelist[addrs] = true;
        }
        
    }

    function setMaxByAddress(uint256 _newMaxByAddress) external onlyOwner {
        MAX_BY_ADDRESS = _newMaxByAddress;        
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC721Enumerable, ERC2981Base)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
    
}
function upgradeWinner(uint _carId, uint condition) internal {
    EachMonster storage m = monsters[_carId];
    m.winnings += 1;
    if(condition == 1){
     m.stamina += 5;
    }else{
     m.strength += 5; 
    }
    m.reflex += 1;  
}

//upgrade chimeras