// Contract: OniWorld
// Target function: function _incrementTokenId() private
`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` will be burned.
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, tokenId);

        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    /**
     * @dev Private function to add a token to this extension's ownership-tracking data structures.
     * @param to address representing the new owner of the given token ID
     * @param tokenId uint256 ID of the token to be added to the tokens list of the given address
     */
    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = ERC721.balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    /**
     * @dev Private function to add a token to this extension's token tracking data structures.
     * @param tokenId uint256 ID of the token to be added to the tokens list
     */
    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    /**
     * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
     * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
     * gas optimizations e.g. when performing a transfer operation (avoiding double writes).
     * This has O(1) time complexity, but alters the order of the _ownedTokens array.
     * @param from address representing the previous owner of the given token ID
     * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
     */
    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId)
        private
    {
        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    /**
     * @dev Private function to remove a token from this extension's token tracking data structures.
     * This has O(1) time complexity, but alters the order of the _allTokens array.
     * @param tokenId uint256 ID of the token to be removed from the tokens list
     */
    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}

contract OniWorld is
    ContextMixin,
    ERC721Enumerable,
    Ownable
{
    using SafeMath for uint256;

    uint256 public _currentTokenId = 0;

    uint256 MAX_SUPPLY =  12000;
    
    string[5] public baseTokenURI = [
        "https://oniworld.mypinata.cloud/ipfs/Qmb85VnHn6BkLA3BBAsncnHH884ov18ttiKoKcM8BrH2hR/",
        "https://oniworld.mypinata.cloud/ipfs/QmdYzGee7R9GXsVBmZBiN27KfFSrxc4ATehuPmZEN2erkz/",
        "https://oniworld.mypinata.cloud/ipfs/QmQdD1kcahFtyP9NQLCqgYmTpVbqz8xovqbKt9ir225o2s/",
        "https://oniworld.mypinata.cloud/ipfs/QmNnhHg3RSd72axM7x6wmebyWjmzyiUdPVyb5siRCStjxJ/",
        "https://oniworld.mypinata.cloud/ipfs/QmdsGos9TNUpT4mAYmj4vbYGb3jvYyHxZWqZqbXSt2xPmK/"
        ];
        
    uint256 public NFT_small_price = 0.059 ether; 
    uint256 public NFT_big_price = 0.129 ether; 

    uint256 public startTime = 1637942100; // 26 Nov 22:55:00 UTC+7 - 1637942100
    string _name = "OniWorld";
    string _symbol = "ONINFT";
    
    bool public is_promotion = true;
    bool public is_surprise = true;
    bool public is_test_open = false;
    
    uint8 public currentRound = 1;
    
    mapping(uint256 => uint8) public oni;
    uint8 maxLevel = 4;

    //Smart Contract Events
    event OniLevelIncreased(uint256 tokenId, uint previousLevel, uint8 currentLevel);
    event NewBaseURISet(uint256 tokenId, string newBaseURI);
    event SurpriseClaimed(address owner, uint256 claimTokenId);
    event OniMinted(address minter, uint256 tokenId);
    event OniTestingResult(uint256 tokenId, bool passed);

    constructor() ERC721(_name, _symbol) {
    }

    /**
    * @dev check if function called by the tokensOwner ? 
    */
    modifier onlyOniOwner(uint256[] calldata providedTokens) {
        uint256 matchedTokenCount = 0;
        uint256[] memory ownedTokens = _walletOfOwner(_msgSender());
        uint256[] memory matchedTokens = new uint256[](ownedTokens.length);
        for(uint256 i = 0; i < providedTokens.length; i++) {
            for(uint256 j = 0; j < ownedTokens.length; j++) {
                if(providedTokens[i] == ownedTokens[j]) {
                    if(providedTokens[i] != matchedTokens[j]) {
                        matchedTokens[j] = providedTokens[i];
                        matchedTokenCount++;
                    } 
                }
            }
        }
        require(matchedTokenCount == providedTokens.length, 'Only the owner of all tokens and no duplication can use this function.');
        _;
    }
    
    function _walletOfOwner(address _owner) internal 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;
    }

    /**
    * @dev level checking   
    */
    function checkOniLevel(uint256[] calldata tokens, uint8 level) public view returns(bool) {
        uint256 validTokenCount = 0;
        for(uint256 i  = 0; i < tokens.length; i++) {
            if(oni[tokens[i]] == level) {
                validTokenCount++;
            }
        }
        
        if(tokens.length == validTokenCount) {
            return true;
        } else {
            return false;
        }
    }
    /** 
     * @dev check if tokens can be tested.
    */
    function _onlyCanTest(uint256[] calldata tokens) internal view returns(bool) {
        uint failed = 0;
        for(uint256 i = 0; i < tokens.length; i++) {
            if(oni[tokens[i]] == 1) {
                failed++;
            }
        }
        
        if(failed > 0) {
            return false;
        } else {
            return true;
        }
    }

    /**
    * @dev level up 
    */
    function _increseOniLevel(uint256 tokenId, uint8 level) internal {
        uint8 previousLevel = oni[tokenId];
        require(previousLevel != 1, 'Only token that passed the test.');
        require(previousLevel <= maxLevel, 'Token max level reached.');
        oni[tokenId] = previousLevel + level;

        emit OniLevelIncreased(tokenId, previousLevel, oni[tokenId]);
    }
    
    /**
     * @dev calculate chance
    */
    function _calculateChance() internal view returns(uint256) {
        uint256 rand = uint256(keccak256(abi.encodePacked(Strings.toString(block.timestamp))));
        uint256 chance = rand.mod(100).add(1);
        return chance;
    }
    
    /** 
     * @dev update winning token and set fail = true to all the rest.
    */
    
    function _batchUpdate(uint256[] calldata tokens, uint8 level) internal {
         _increseOniLevel(tokens[0], level);
        for(uint256 i = 1; i < tokens.length; i++) {
            _updateFailToken(tokens[i]);
        }
    }
    
    /**
     * @dev update token to fail status
    */
    
    function _updateFailToken(uint256 tokenId) internal {
        oni[tokenId] = 1;
    }
    
    /**
     * @dev next round
    */
    function setNextRound() public onlyOwner {
        require(currentRound < 3,'We are at the final round now.');
        currentRound++;
    }
    
    /**
     * @dev start-stop testing function
    */
    function setOpenTest(bool val) public onlyOwner {
        is_test_open = val;
    }
    
    //Testing function
    /** 
    * @dev 3 round to be at level 4 (need to pass 3 stages (1 - 4)) - oni start at level 0, (** level 1 is the token that fail the test).
    */
    function oniTest(uint256[] calldata tokens) public onlyOniOwner(tokens) {
      require(tokens.length > 0, 'Please provide at least 1 oni into the test');
      require(_onlyCanTest(tokens), 'Please provide available Oni');
      require(is_test_open, 'The pilgrimage is close');
      
      if(tokens.length < 2) {
          //calculate chance;
          uint chance = _calculateChance();
          
          //Round 1
          if(currentRound == 1 && checkOniLevel(tokens, 0) && chance <= 40) {
              _increseOniLevel(tokens[0], 2);
          } else {
              _updateFailToken(tokens[0]);
          }
          
          //Round 2
          if(currentRound == 2) {
              if(checkOniLevel(tokens, 2) && chance <= 40) {
                  _increseOniLevel(tokens[0], 1);
              } else if (checkOniLevel(tokens, 1) && chance <= 16) {
                  _increseOniLevel(tokens[0], 2);
              } else {
                  _updateFailToken(tokens[0]);
              }
          }
          
          //Round 3
          if(currentRound == 3) {
              if(checkOniLevel(tokens, 3) && chance <= 40) {
                  _increseOniLevel(tokens[0], 1);
              } else if (checkOniLevel(tokens, 2) && chance <= 16) {
                  _increseOniLevel(tokens[0], 2);
              } else if (checkOniLevel(tokens, 1) && chance <= 6) {
                  _increseOniLevel(tokens[0], 3);
              } else {
                  _updateFailToken(tokens[0]);
              }
          }
     
      } else {
         //multiple tokens provided
         if(currentRound == 1 && checkOniLevel(tokens, 0)) {
             // round 1
             require(tokens.length == 2, '[ROUND 1] Need 2 Oni with level 0 for this testing.');
             _batchUpdate(tokens, 2);
         }
         
         if(currentRound == 2) {
             // round 2
             if(checkOniLevel(tokens, 2)) {
                 require(tokens.length == 2, '[ROUND 2] Need 2 Oni with level 2 for this testing.');
                 _batchUpdate(tokens, 1);
             } 
             if (checkOniLevel(tokens, 0)) {
                 require(tokens.length == 4, '[ROUND 2] Need 4 Oni with level 0 for this testing.');
                 _batchUpdate(tokens, 3);
             }
         }
         
         if(currentRound == 3) {
             // round 3
             if(checkOniLevel(tokens, 3)) {
                 require(tokens.length == 2, '[ROUND 3] Need 2 Oni with level 3 for this testing.');
                 _batchUpdate(tokens, 1);
             }
             if(checkOniLevel(tokens, 2)) {
                 require(tokens.length == 4, '[ROUND 3] Need 4 Oni with level 2 for this testing.');
                 _batchUpdate(tokens, 2);
             }
             if(checkOniLevel(tokens, 0)) {
                 require(tokens.length == 8, '[ROUND 3] Need 8 Oni with level 0  for this testing.');
                 _batchUpdate(tokens, 4);
             }
         }
      }
    }

   //Owner Functions 
   function setSurpriseMint(bool result) external onlyOwner {
       is_surprise  = result;
   }

    function setPromotion(bool result) external onlyOwner{
        is_promotion = result;
    }
    
    function setStartTime(uint256 time) external onlyOwner{
        startTime = time;
    }
    
    function setSmallPrice(uint256 small_price) external onlyOwner{
        NFT_small_price = small_price;
    }
    
    function setBigPrice(uint256 big_price) external onlyOwner{
        NFT_big_price = big_price;
    }
    
    //Public Mint
    function surpriseClaim() external {
        require(is_surprise == true, "no more surprises.");
        require(balanceOf(_msgSender()) == 0, "You have already Claimed Free Nft.");
        _mint(_msgSender(), _getNextTokenId());
        _incrementTokenId();

        emit SurpriseClaimed(_msgSender(), _currentTokenId);
    }

    function smallTeam() public payable {
        require(msg.value == NFT_small_price, "Sent Amount Not Enough");
        _mintOni(1);
    }
    
    function bigTeam() public payable {
        require(msg.value == NFT_big_price, "Sent Amount Not Enough");
        _mintOni(3);
    }
    
    function _mintOni(uint8 num) internal {
        require(block.timestamp >= startTime, "It's not time yet");
        
        if(is_promotion == true){
            num = num * 2;
        }
        if(balanceOf(_msgSender()) == 0 && is_surprise){
            num = num + 1;
        }
        
        require(_currentTokenId.add(num) < MAX_SUPPLY, "Max Supply Reached");
        
        for(uint256 i=0; i<num; i++){
            _mint(_msgSender(), _getNextTokenId());
            _incrementTokenId();

            emit OniMinted(_msgSender(), _currentTokenId);
        }
    }

    
    ////////
    
    //Owner Mint Functions
    
    function mintMany(uint256 num, address _to) public onlyOwner {
        require(_currentTokenId + num < MAX_SUPPLY, "Max Supply Reached");
        require(num <= 20, "Max 20 Allowed.");
        for(uint256 i=0; i<num; i++){

            _mint(_to, _getNextTokenId());
            _incrementTokenId();

            emit OniMinted(_msgSender(), _currentTokenId);
        }
    }

  function withdraw() external onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }

    /**
     * @dev calculates the next token ID based on value of _currentTokenId
     * @return uint256 for the next token ID
     */
    function _getNextTokenId() private view returns (uint256) {
        return _currentTokenId.add(1);
    }