// File: XengameV2Live/xenGameV2.sol



// SPDX-License-Identifier: MIT

/*


██╗  ██╗███████╗███╗   ██╗ ██████╗  █████╗ ███╗   ███╗███████╗
╚██╗██╔╝██╔════╝████╗  ██║██╔════╝ ██╔══██╗████╗ ████║██╔════╝
 ╚███╔╝ █████╗  ██╔██╗ ██║██║  ███╗███████║██╔████╔██║█████╗  
 ██╔██╗ ██╔══╝  ██║╚██╗██║██║   ██║██╔══██║██║╚██╔╝██║██╔══╝  
██╔╝ ██╗███████╗██║ ╚████║╚██████╔╝██║  ██║██║ ╚═╝ ██║███████╗
╚═╝  ╚═╝╚══════╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝╚══════╝



This project has been developed under the guidelines set forth by the Fair Crypto Foundation, adhering to its first principles of self-custody, transparency, consensus-based trust, and permissionless value exchange. Accordingly, this project values the ethos of decentralization and exercises these principles by not incorporating any administrative access keys or functions.

Given these considerations, it is important to note that all users are encouraged to conduct their own research and due diligence prior to using this contract. Potential users should understand, evaluate, and accept the inherent risks associated with its use, owing to the blockchain and smart contract technology underpinning it.

Please be advised, due to the intentional absence of administrative access keys in this smart contract, and the elimination of central authority, the original developers or contributors maintain no control over the system once it is deployed, placing them on an equivalent footing with any other user.

The developers, hence, assume no liability or responsibility whatsoever for any future errors, exploits, or security breaches that may occur, unforeseen or otherwise. This software is distributed "AS-IS" without warranties or conditions of any kind, either expressed or implied.

As an open-source project, all users participate at their own risk and assume full responsibility for all actions undertaken in connection with this contract, including but not limited to any loss or damage incurred. Each user, by engaging with the contract and its functionalities, comprehensively and irrevocably accepts this accountability.

Moreover, potential and existing users should be fully aware of and stay compliant with the laws and regulations in their respective jurisdictions. It is the sole responsibility of each user to understand and adhere to the relevant laws and regulations applicable in their location.

Please be advised that interactions with cryptocurrencies and blockchain-based technologies come with significant risk. Users should always exercise utmost caution, and use this contract only after fully appreciating the implications on both a technical and legal level.

By using this contract, the user explicitly acknowledges and agrees to these terms and conditions. Failure to heed these disclaimers and instructions may lead to adversarial consequences for which neither the developers nor any other associated party shall be held responsible.

             

*/

pragma solidity 0.8.17;

interface IXENnftContract {
    function ownerOf(uint256 tokenId) external view returns (address);
}

interface INFTRegistry {
    function registerNFT(uint256 tokenId) external;
    function isNFTRegistered(uint256 tokenId) external view returns (bool);
    function addToPool() external payable;
}

interface XENBurn {
    function deposit() external payable returns (bool);
}

interface IPlayerNameRegistry {
    function registerPlayerName(address _address, string memory _name) external payable;
    function getPlayerAddress(string memory _name) external view returns (address);
    function getPlayerFirstName(address playerAddress) external view returns (string memory);
}

contract XenGame {
    IXENnftContract public nftContract;
    INFTRegistry public nftRegistry;
    XENBurn public xenBurn;
    IPlayerNameRegistry private playerNameRegistry;

    uint256 constant KEY_RESET_PERCENTAGE = 1; // 0.001% or 1 basis point
    uint256 constant NAME_REGISTRATION_FEE = 20000000000000000; // 0.02 Ether in Wei
    uint256 constant KEY_PRICE_INCREMENT_PERCENTAGE = 10; // 0.099% or approx 10 basis points
    uint256 constant REFERRAL_REWARD_PERCENTAGE = 1000; // 10% or 1000 basis points
    uint256 constant NFT_POOL_PERCENTAGE = 500; // 5% or 500 basis points
    uint256 constant ROUND_GAP = 24 hours;// 24 hours round gap
    uint256 constant EARLY_BUYIN_DURATION = 300; // *********************************************************** updated to 5 min  

    uint256 constant KEYS_FUND_PERCENTAGE = 5000; // 50% or 5000 basis points
    uint256 constant JACKPOT_PERCENTAGE = 3000; // 30% or 3000 basis points
    uint256 constant BURN_FUND_PERCENTAGE = 1500; // 15% or 1500 basis points
    uint256 constant APEX_FUND_PERCENTAGE = 500; // 5% or 5000 basis points
    uint256 constant PRECISION = 10 ** 18;
    address private playerNames;
    uint256 private loadedPlayers = 0;
    bool public migrationLocked = false;
    bool public migrationPhaseTwoCompleted = false;

    
    struct Player {
        mapping(uint256 => uint256) keyCount; //round to keys
        mapping(uint256 => uint256) burntKeys;
        mapping(uint256 => uint256) earlyBuyinPoints; // Track early buyin points for each round
        uint256 referralRewards;
        string lastReferrer; // Track last referrer name
        mapping(uint256 => uint256) lastRewardRatio; // New variable
        uint256 keyRewards;
        uint256 numberOfReferrals; 
    }

    struct Round {
        uint256 totalKeys ;
        uint256 burntKeys; 
        
        uint256 start;
        uint256 end;
        address activePlayer;
        bool ended;
        bool isEarlyBuyin;
        uint256 keysFunds; // not used in logic, old code 
        uint256 jackpot; // ETH for the jackpot
        uint256 earlyBuyinEth; // Total ETH received during the early buy-in period
        uint256 lastKeyPrice; // The last key price for this round
        uint256 rewardRatio;
        uint256 BurntKeyFunds;
        uint256 uniquePlayers;
        address[] playerAddresses;
        
    }

    uint256 public currentRound = 0;
    mapping(address => Player) public players;
    mapping(uint256 => Round) public rounds;
    mapping(uint256 => mapping(address => bool)) public isPlayerInRound;
    mapping(string => address) public nameToAddress;
    mapping(address => mapping(uint256 => bool)) public earlyKeysReceived;

    constructor(
        
    
    ) {

        

        nftContract = IXENnftContract(0x0a252663DBCc0b073063D6420a40319e438Cfa59);  // Eth address
        nftRegistry = INFTRegistry(0xEDa159A0339826C96c30D39A1be8588d82212395); // X1 Eth address
        xenBurn = XENBurn(0x573E18d9dF2496B1768139Eb4c712B9b086be294); // Eth address
        playerNameRegistry = IPlayerNameRegistry(0x68317FE2590DC605C730628fb645DE0F17F86BFc); // Eth address
        playerNames = 0x68317FE2590DC605C730628fb645DE0F17F86BFc; // Eth address

        currentRound = 1;
        rounds[currentRound].totalKeys = 10097300000000000000000000;
        rounds[currentRound].burntKeys = 338175000000000000000000;
        rounds[currentRound].jackpot = 63566409521227276389;
        rounds[currentRound].earlyBuyinEth = 59101124779840297779;
        rounds[currentRound].lastKeyPrice = 44087673862494;
        rounds[currentRound].rewardRatio = 994363746378;
        rounds[currentRound].uniquePlayers = 198;        
        rounds[currentRound].start = 1694966400;     
        rounds[currentRound].end = block.timestamp + 12 hours;  // Update to set new end time on migration        
        rounds[currentRound].ended = false;

        
        
    }

    function migrateUserBasicData(
        address[] calldata playeraddresses,
        uint256[] calldata playerRound1KeyCount,
        uint256[] calldata playerRound1BurntKeys,
        uint256[] calldata playerRound1EarlyBuyinPoints,
        string[] calldata playerLastReferrer
    ) external {
        require(!migrationLocked, "Migration has been locked");
        require(loadedPlayers + playeraddresses.length == 198, "Cannot exceed 198 users");

        for (uint i = 0; i < 198; i++) {
            players[playeraddresses[i]].keyCount[1] = playerRound1KeyCount[i];

            if (playerRound1BurntKeys[i] > 0) {
                players[playeraddresses[i]].burntKeys[1] = playerRound1BurntKeys[i];
            }
            if (playerRound1EarlyBuyinPoints[i] > 0) {
                players[playeraddresses[i]].earlyBuyinPoints[1] = playerRound1EarlyBuyinPoints[i];
            }

            if (bytes(playerLastReferrer[i]).length > 0) {
                players[playeraddresses[i]].lastReferrer = playerLastReferrer[i];
            }

            isPlayerInRound[currentRound][playeraddresses[i]] = true;

            rounds[currentRound].playerAddresses.push(playeraddresses[i]);
            loadedPlayers++;
            if (loadedPlayers == 198) {
                migrationLocked = true;
            }
        }
    } 

    function migrateUserReferralAndRewardData(
        address[] calldata playeraddresses,
        uint256[] calldata playerReferralRewards,
        uint256[] calldata playerRound1RewardRatio,
        uint256[] calldata playerKeyRewards,
        uint256[] calldata playerNumberOfReferrals
        
    ) external {
        require(migrationLocked, "Migrate base users first");
        require(loadedPlayers == 198, "All players must be loaded first");
        require(!migrationPhaseTwoCompleted, "Phase two migration already completed");

        for (uint i = 0; i < 198; i++) {
            if (playerReferralRewards[i] > 0) {
                players[playeraddresses[i]].referralRewards = playerReferralRewards[i];
            }

            players[playeraddresses[i]].lastRewardRatio[1] = playerRound1RewardRatio[i];

            

            players[playeraddresses[i]].keyRewards = playerKeyRewards[i];

            if (playerNumberOfReferrals[i] > 0) {
                players[playeraddresses[i]].numberOfReferrals = playerNumberOfReferrals[i];
            }

            if (players[playeraddresses[i]].earlyBuyinPoints[1] > 0 && players[playeraddresses[i]].keyCount[1] > 0) {
                earlyKeysReceived[playeraddresses[i]][1] = true;
            }
        }

        migrationPhaseTwoCompleted = true;
    }  

    function seedEth() external payable {
        // This function is only here to reseed the eth from the migration         
    }

    /**
    * @dev Allows a player to buy keys with a referral.
    * @param _referrerName The name of the referrer.
    * @param _numberOfKeys The number of keys to purchase.
    */
    function buyWithReferral(string memory _referrerName, uint256 _numberOfKeys) public payable {
        Player storage player = players[msg.sender];

        // Get the player and referrer information
        string memory referrerName = bytes(_referrerName).length > 0 ? _referrerName : player.lastReferrer;
        address referrer = playerNameRegistry.getPlayerAddress(referrerName);

        Round storage round = rounds[currentRound];

            // Check if the player is not already in the current round
            if (!isPlayerInRound[currentRound][msg.sender]) {
                // Add the player address to the list of player addresses for the current round
                round.playerAddresses.push(msg.sender);
                // Set isPlayerInRound to true for the current player in the current round
                isPlayerInRound[currentRound][msg.sender] = true;
                // Increment the uniquePlayers count for the current round
                round.uniquePlayers++;
            }

            // Calculate the referral reward as a percentage of the incoming ETH
            uint256 referralReward = (msg.value * REFERRAL_REWARD_PERCENTAGE) / 10000; // 10% of the incoming ETH

            if (referralReward > 0) {
                // Added check here to ensure referral reward is greater than 0
                uint256 splitReward = referralReward / 2; // Split the referral reward

                // Add half of the referral reward to the referrer's stored rewards
                players[referrer].referralRewards += splitReward;
                players[referrer].numberOfReferrals++;

                if (referrer != address(0)){
                // Add the other half of the referral reward to the player's stored rewards
                player.referralRewards += splitReward;
                }

                emit ReferralPaid(msg.sender, referrer, splitReward, block.timestamp);
            }

            
            if (_numberOfKeys > 0) {
                buyCoreWithKeys(msg.value, _numberOfKeys);
            } else {
                buyCore(msg.value);
            }

            // Set the referrer name for the player
            player.lastReferrer = referrerName;
        
    }

    /**
    * @dev Handles the core logic of purchasing keys based on the amount of ETH sent.
    * @param _amount The amount of ETH sent by the player.
    */
    function buyCore(uint256 _amount) private {
        // Check if the round is active or has ended
        require(isRoundActive() || isRoundEnded(), "Cannot purchase keys during the round gap");

        uint256 _roundId = currentRound;
        Round storage round = rounds[currentRound];

        // If the round has ended and there are no total keys, set a new end time for the round
        if (isRoundEnded()) {

            if (round.totalKeys == 0){
                round.end = block.timestamp + 600;
                players[msg.sender].keyRewards += (_amount * 90 / 100);
                return;
            }

            endRound();
            startNewRound();
            players[msg.sender].keyRewards += (_amount * 90 / 100);
            return;
        }

        // If the round is active
        if (isRoundActive()) {
            if (block.timestamp <= round.start + EARLY_BUYIN_DURATION) {
                // If we are in the early buy-in period, follow early buy-in logic
                buyCoreEarly(_amount);
            } else if (!round.ended) {
                
                // Check if this is the first transaction after the early buy-in period
                if (round.isEarlyBuyin) {
                    updateTotalKeysForRound();
                    finalizeEarlyBuyinPeriod();
                }

                // Check if the last key price exceeds the jackpot threshold and reset it if necessary
                if (round.lastKeyPrice > calculateJackpotThreshold()) {
                    uint256 newPrice = resetPrice();
                    round.lastKeyPrice = newPrice;
                    emit PriceReset(msg.sender, newPrice, block.timestamp);
                }

                // Calculate the maximum number of keys to purchase and the total cost
                (uint256 maxKeysToPurchase, ) = calculateMaxKeysToPurchase(_amount);
                    

                // Process users rewards for the current round
                processRewards(_roundId);

                // Set the last reward ratio for the player in the current round
                if (players[msg.sender].lastRewardRatio[_roundId] == 0) {
                    players[msg.sender].lastRewardRatio[_roundId] = round.rewardRatio;
                }

                // Process the key purchase with the maximum number of keys and total cost
                processKeyPurchase(maxKeysToPurchase, _amount);

                // Set the active player for the round
                round.activePlayer = msg.sender;

                // Adjust the end time of the round based on the number of keys purchased
                adjustRoundEndTime(maxKeysToPurchase);
            }
        } 
    }

    /**
    * @dev Handles the core logic of purchasing keys with a specified number of keys and amount of ETH.
    * @param _amount The amount of ETH sent by the player.
    * @param _numberOfKeys The number of keys to purchase.
    */
    function buyCoreWithKeys(uint256 _amount, uint256 _numberOfKeys) private {
        // Check if the round is active or has ended\
        require(isRoundActive() || isRoundEnded(), "Cannot purchase keys during the round gap");

        uint256 _roundId = currentRound;
        Round storage round = rounds[currentRound];
        // If the round has ended and there are no total keys, set a new end time for the round
        if (isRoundEnded()) {

            if (round.totalKeys == 0){
                round.end = block.timestamp + 600;
                players[msg.sender].keyRewards += (_amount * 90 / 100);
                return;
            }
            
            // End the current round and start a new one
            endRound();
            startNewRound();
            players[msg.sender].keyRewards += (_amount * 90 / 100);
            return;
        }

        if (isRoundActive()) {

            if (block.timestamp <= round.start + EARLY_BUYIN_DURATION) {
                // If we are in the early buy-in period, follow early buy-in logic
                buyCoreEarly(_amount);
            } else if (!round.ended) {
                // Check if this is the first transaction after the early buy-in period
                if (round.isEarlyBuyin) {
                    updateTotalKeysForRound();
                    finalizeEarlyBuyinPeriod();
                }

                // Check if the last key price exceeds the jackpot threshold and reset it if necessary
                if (round.lastKeyPrice > calculateJackpotThreshold()) {
                    uint256 newPrice = resetPrice();
                    round.lastKeyPrice = newPrice;
                    emit PriceReset(msg.sender, newPrice, block.timestamp);

                }

                // Calculate cost for _numberOfKeys
                uint256 cost = calculatePriceForKeys(_numberOfKeys);
                require(cost <= _amount, "Not enough ETH to buy the specified number of keys");

                
                // Process user rewards for the current round
                processRewards(_roundId);

                // Set the last reward ratio for the player in the current round if first user key buy. 
                if (players[msg.sender].lastRewardRatio[_roundId] == 0) {
                    players[msg.sender].lastRewardRatio[_roundId] = round.rewardRatio;
                }

                // Process the key purchase with the specified number of keys and cost
                processKeyPurchase(_numberOfKeys, _amount);

                // Set the active player for the round
                round.activePlayer = msg.sender;

                // Adjust the end time of the round based on the number of keys purchased
                adjustRoundEndTime(_numberOfKeys);

                
            }
        } 
    }

    /**
    * @dev Allows a player to purchase keys using their accumulated rewards.
    */
    function buyKeysWithRewards() public {
        // Check if the current round is active
        require(isRoundActive(), "Round is not active");

        Player storage player = players[msg.sender];

        // Check for any early keys
        checkForEarlyKeys(currentRound);

        // Calculate the player's rewards unprocessed 
        uint256 reward = (
            (player.keyCount[currentRound] / 1 ether)
                * (rounds[currentRound].rewardRatio - player.lastRewardRatio[currentRound])
        ); // using full keys for reward calc

        // Add any processed keyRewards to the calculated reward
        reward += player.keyRewards;

        // Reset player's keyRewards
        player.keyRewards = 0;

        require(reward > 0, "No rewards to buy keys with");

        // Reset player's lastRewardRatio for the round
        player.lastRewardRatio[currentRound] = rounds[currentRound].rewardRatio; //

        // Calculate max keys that can be purchased with the reward
        (uint256 maxKeysToPurchase,) = calculateMaxKeysToPurchase(reward);

        // Make sure there are enough rewards to purchase at least one key
        require(maxKeysToPurchase > 0, "Not enough rewards to purchase any keys");

        address referrer = playerNameRegistry.getPlayerAddress(players[msg.sender].lastReferrer);
                   

            // Calculate the referral reward as a percentage of the incoming ETH
            uint256 referralReward = (reward * REFERRAL_REWARD_PERCENTAGE) / 10000; // 10% of the incoming ETH

            if (referralReward > 0) {
                // Added check here to ensure referral reward is greater than 0
                uint256 splitReward = referralReward / 2; // Split the referral reward

                // Add half of the referral reward to the referrer's stored rewards
                players[referrer].referralRewards += splitReward;
                players[referrer].numberOfReferrals++;

                if (referrer != address(0)){
                // Add the other half of the referral reward to the player's stored rewards
                player.referralRewards += splitReward;
                }

                emit ReferralPaid(msg.sender, referrer, splitReward, block.timestamp);
            }            
            
                
            
        // Buy keys using rewards
        buyCore(reward);
    }

    /**
    * @dev Handles the logic of purchasing keys during the early buy-in period.
    * @param _amount The amount of ETH sent by the player.
    */
    function buyCoreEarly(uint256 _amount) private {
        // Accumulate the ETH and track the user's early buy-in points

        // Calculate the referral reward as a percentage of the incoming ETH
        uint256 referralReward = (_amount * REFERRAL_REWARD_PERCENTAGE) / 10000;

        // Check if the player's last referrer is not valid, and halve the referral reward
        if (playerNameRegistry.getPlayerAddress(players[msg.sender].lastReferrer) == address(0)){
            referralReward = (referralReward / 2);
        }

        // Calculate the amount of ETH without the referral reward
        uint256 amount = _amount - referralReward;

        // Accumulate the amount of ETH sent during the early buy-in period
        rounds[currentRound].earlyBuyinEth += amount;

        // Accumulate the early buy-in points for the player
        players[msg.sender].earlyBuyinPoints[currentRound] += amount;

        // Set the last reward ratio for the player in the current round to 1
        players[msg.sender].lastRewardRatio[currentRound] = 1;

        // Set isEarlyBuyin to true to indicate the early buy-in period is active
        rounds[currentRound].isEarlyBuyin = true;
    }

    /**
    * @dev Fallback function to handle incoming ETH payments and execute buy or withdraw rewards logic.
    */
fallback() external payable {
    // If the incoming value is 0, withdraw rewards for all rounds
    if (msg.value == 0) {
        for (uint256 i = 1; i <= currentRound; i++) {
            withdrawRewards(i);
        }
    }
    // Call buyWithReferral function with empty referrer name and 0 number of keys
    buyWithReferral("", 0);
}

/**
    * @dev Receive function to handle incoming ETH payments and execute buy or withdraw rewards logic.
    */
receive() external payable {
    // If the incoming value is 0, withdraw rewards for all rounds
    if (msg.value == 0) {
        for (uint256 i = 1; i <= currentRound; i++) {
            withdrawRewards(i);
        }
    }
    // Call buyWithReferral function with empty referrer name and 0 number of keys
    buyWithReferral("", 0);
}

    /**
    * @dev Checks if the current round is active.
    * @return bool indicating whether the round is active or not.
    */
    function isRoundActive() public view returns (bool) {
        uint256 _roundId = currentRound;
        return block.timestamp >= rounds[_roundId].start && block.timestamp < rounds[_roundId].end;
    }

    /**
    * @dev Checks if the current round has ended.
    * @return bool indicating whether the round has ended or not.
    */
    function isRoundEnded() public view returns (bool) {
        uint256 _roundId = currentRound;
        return block.timestamp >= rounds[_roundId].end;
    }

    /**
    * @dev Updates the total number of keys for the current round.
    * If there was early buy-in ETH, it adds 10,000,000 keys. Otherwise, it adds 1 key.
    */
    function updateTotalKeysForRound() private {
        // Check if there was early buy-in ETH        
        if (rounds[currentRound].earlyBuyinEth > 0) {
            // Add 10,000,000 keys to the total keys count for the round
            rounds[currentRound].totalKeys += 10000000 ether;

        } else {

            // Add 1 key to the total keys count for the round if no early buyin.
            rounds[currentRound].totalKeys += 1 ether;
        }
    }

    /**
    * @dev Finalizes the early buy-in period by setting necessary variables and adding early buy-in funds to the jackpot.
    */
    function finalizeEarlyBuyinPeriod() private {
        // Set isEarlyBuyin to false to signify the early buy-in period is over
        Round storage round = rounds[currentRound];
        
        round.isEarlyBuyin = false;

        // Calculate the last key price for the round
        if (round.earlyBuyinEth > 0) {
            round.lastKeyPrice = round.earlyBuyinEth / (10 ** 7); // using full keys
        } else {
            round.lastKeyPrice = 0.000000009 ether; // Set to 0.000000009 ether if there is no early buying ETH or no keys purchased
        }

        

        // Add early buy-in funds to the jackpot
        round.jackpot += round.earlyBuyinEth;
    }

    /**
    * @dev Calculates the maximum number of keys that can be purchased and the total cost within a given amount of ETH.
    * @param _amount The amount of ETH to spend on keys.
    * @return maxKeys The maximum number of keys that can be purchased.
    * @return totalCost The total cost in ETH to purchase the maximum number of keys.
    */
    function calculateMaxKeysToPurchase(uint256 _amount) public view returns (uint256 maxKeys, uint256 totalCost) {
        // Fetch the initial price of a key
        uint256 initialKeyPrice = getKeyPrice();

        // If the user's amount is less than the price of a single key, return as no keys can be bought
        if (_amount < initialKeyPrice) {
            return (0, 0);
        }

        // left and right are the boundaries for the binary search of the maximum number of keys that can be bought.
        // Initialize the left to zero and right to the maximum number of keys that could be bought if the price never increased.
        uint256 left = 0;
        uint256 right = _amount / initialKeyPrice;

        // Variable to store the total cost of keys
        uint256 _totalCost;

        // Binary search to find the maximum number of keys the user can buy
        while (left < right) {
            // Find the mid point
            uint256 mid = (left + right) / 2;

            // Calculate the cost to purchase mid number of keys
            _totalCost = calculatePriceForKeys(mid);
            
            uint256 nextCost;

            // Ensure we don't get an overflow when we add 1 to mid
            if (mid + 1 > mid) {
                // Calculate the cost to purchase mid + 1 keys
                nextCost = calculatePriceForKeys(mid + 1);
            }

            // Check to see if the user is able to purchase mid keys.
            // If not, decrease the upper limit. If they can, increase the lower limit
            // If the user can purchase mid+1 keys, then it should not exit, hence nextCost > _amount is required
            if (_totalCost <= _amount && (mid == right || nextCost > _amount)) {
                maxKeys = mid;
                break;
            } else if (_totalCost <= _amount) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        // If the binary search completes without finding a suitable number of keys, set maxKeys to left
        if (maxKeys == 0) {
            maxKeys = left;
        }

        // Calculate the cost for the final number of keys again to ensure accuracy
        _totalCost = calculatePriceForKeys(maxKeys);

        // Return the maximum number of keys that can be bought and the total cost for those keys
        return (maxKeys, _totalCost);
    }

    /**
    * @dev Calculates the total price for a specified number of keys based on the current key price.
    * @param _keys The number of keys to calculate the price for.
    * @return totalPrice The total price in ETH for the specified number of keys.
    */
    function calculatePriceForKeys(uint256 _keys) public view returns (uint256 totalPrice) {
        uint256 initialKeyPrice = getKeyPrice();
        uint256 increasePerKey = 0.000000009 ether;

         // Calculate the total price based on the number of keys
        if (_keys <= 1) {
            totalPrice = initialKeyPrice * _keys;
        } else {
            uint256 lastPrice = initialKeyPrice + ((_keys - 1) * increasePerKey);
            totalPrice = (_keys * (initialKeyPrice + lastPrice)) / 2;
        }

        return totalPrice;
    }

    /**
    * @dev Handles the purchase of keys by a player and updates relevant data.
    * @param maxKeysToPurchase The maximum number of keys to purchase.
    * @param _amount The amount of ETH sent by the player.
    */
    function processKeyPurchase(uint256 maxKeysToPurchase, uint256 _amount) private {
        // Check if the amount is greater than or equal to 0
        require(_amount > 0, "Not enough Ether to purchase keys");

        // Calculate the fractional keys based on the maximum number of keys to purchase
        uint256 fractionalKeys = maxKeysToPurchase * 1 ether;
        Round storage round = rounds[currentRound];

        // Increase the player's key count for the current round
        players[msg.sender].keyCount[currentRound] += fractionalKeys;

        // Reset the last reward ratio for the player in the current round
        players[msg.sender].lastRewardRatio[currentRound] = round.rewardRatio; // reset fallback in case user has gap betewwn burn and next buyin. 

        // Increase the total keys for the current round
        round.totalKeys += fractionalKeys;

        // Calculate the final key price based on the last key price and the increase per key
        uint256 finalKeyPrice = round.lastKeyPrice;
        uint256 increasePerKey = 0.000000009 ether;
        finalKeyPrice += increasePerKey * maxKeysToPurchase;

        // Update the last key price for the current round
        round.lastKeyPrice = finalKeyPrice;

        // Distribute the funds to different purposes (keys funds, jackpot, etc.)
        distributeFunds(_amount);

        emit BuyAndDistribute(msg.sender,  maxKeysToPurchase, finalKeyPrice,  block.timestamp);
    }

    /**
    * @dev Burns the keys owned by a player in a specific round.
    * @param player The address of the player.
    * @param roundNumber The round number in which to burn the keys.
    */
    function BurnKeys(address player, uint roundNumber) private {
        
        // Check if the round number is the current round
        if (roundNumber == currentRound) {
            uint256 Keys = players[player].keyCount[roundNumber];

            // Reset the key count of the player for the specific round
            players[player].keyCount[roundNumber] = 0;

            // Update the burnt keys count for the player and round
            players[player].burntKeys[roundNumber]+= Keys;
            rounds[roundNumber].totalKeys -= Keys;
            rounds[roundNumber].burntKeys += Keys;

            
            emit KeyBurn(player,  Keys,  block.timestamp);
        }
    }

    /**
    * @dev Checks if the player has early buy-in points for the current round and adds early keys if applicable.
    */
    function checkForEarlyKeys(uint _round) private {

        // Check if the player has early buy-in points and has not received early keys for the current round
        if (players[msg.sender].earlyBuyinPoints[_round] > 0 && !earlyKeysReceived[msg.sender][_round]) {

            // Calculate early keys based on the amount of early ETH sent
            uint256 totalPoints = rounds[_round].earlyBuyinEth;
            uint256 playerPoints = players[msg.sender].earlyBuyinPoints[_round];
            uint256 earlyKeys = ((playerPoints * 10_000_000) / totalPoints) * 1 ether;

            // Add the early keys to the player's key count for the current round
            players[msg.sender].keyCount[_round] += earlyKeys;
            
            // Mark that early keys were received for this round
            earlyKeysReceived[msg.sender][_round] = true;
        }
    }

    /**
    * @dev Adjusts the end time of the current round based on the maximum number of keys purchased.
    * @param maxKeysToPurchase The maximum number of keys purchased in the current transaction.
    */
    function adjustRoundEndTime(uint256 maxKeysToPurchase) private {
        // Calculate the time extension based on the maximum keys purchased
        uint256 timeExtension = maxKeysToPurchase * 30 seconds;

        // Set the maximum end time as the current timestamp plus 2 hours
        uint256 maxEndTime = block.timestamp + 12 hours;

        // Adjust the end time of the current round by adding the time extension, capped at the maximum end time
        rounds[currentRound].end = min(rounds[currentRound].end + timeExtension, maxEndTime);
    }

    /**
    * @dev Retrieves the current key price for the active round.
    * @return The current key price.
    */
    function getKeyPrice() public view returns (uint256) {
        uint256 _roundId = currentRound;

        // Fetch the last key price
        uint256 lastKeyPrice = rounds[_roundId].lastKeyPrice;

        // If the price is 0, return 0.000000009 ether, else return the last set price
        if(lastKeyPrice == 0) {
            return 0.000000009 ether;
        } else {
            return lastKeyPrice;
        }
    }

    /**
    * @dev Calculates the jackpot threshold as a percentage of the current round's jackpot.
    * @return The jackpot threshold.
    */
    function calculateJackpotThreshold() private view returns (uint256) {
        uint256 _roundId = currentRound;

        // Calculate the jackpot threshold as 0.0001% of the jackpot
        return rounds[_roundId].jackpot / 1000000; 
    }

    /**
    * @dev Resets the key price by dividing the current round's jackpot by 10 million.
    * @return The new key price after resetting.
    */
    function resetPrice() private view returns (uint256) {
        uint256 _roundId = currentRound;
        return rounds[_roundId].jackpot / 10000000; 
    }

    /**
    * @dev Updates the reward ratio for a specific round based on the amount of ETH received.
    * @param _amount The amount of ETH received.
    * @param _roundNumber The round number to update the reward ratio for.
    */
    function updateRoundRatio(uint256 _amount, uint256 _roundNumber) private {

        // Calculate the reward ratio by dividing the amount by the total keys in the current round
        rounds[_roundNumber].rewardRatio += (_amount / (rounds[currentRound].totalKeys / 1 ether));
    }

    /**
    * @dev Distributes the incoming ETH to different funds and updates the reward ratio.
    * @param _amount The amount of ETH received.
    */
    function distributeFunds(uint256 _amount) private {
        // Calculate the referral reward as a percentage of the incoming ETH
        uint256 referralReward = (_amount * REFERRAL_REWARD_PERCENTAGE) / 10000;

        // Check if the last referrer is not registered and adjust the referral reward
        if (playerNameRegistry.getPlayerAddress(players[msg.sender].lastReferrer) == address(0)){
            referralReward = (referralReward / 2);
        }

        // Calculate the remaining amount after deducting the referral reward
        uint256 amount = _amount - referralReward;

        // Calculate the keys fund as a percentage of the remaining amount
        uint256 keysFund = (amount * KEYS_FUND_PERCENTAGE) / 10000;

        // Update the reward ratio for the current round based on the keys fund
        updateRoundRatio(keysFund, currentRound);
        
        // Calculate the jackpot as a percentage of the remaining amount
        uint256 jackpot = (amount * JACKPOT_PERCENTAGE) / 10000;

        // Add the jackpot to the current round's jackpot
        rounds[currentRound].jackpot += jackpot;

        // Calculate the apex fund as a percentage of the remaining amount
        uint256 apexFund = (amount * APEX_FUND_PERCENTAGE) / 10000;

        // Transfer the apex fund to the nftRegistry
        nftRegistry.addToPool{value: apexFund}();

        // Calculate the burn fund as a percentage of the remaining amount
        uint256 burnFund = (amount * BURN_FUND_PERCENTAGE) / 10000;

        // Deposit the burn fund to the xenBurn contract
        xenBurn.deposit{value: burnFund}();

        
    }

    /**
    * @dev Allows a player to register a name by paying the registration fee.
    * @param name The name to register.
    */
    function registerPlayerName(string memory name) public payable {
        // Check if the player has provided enough funds to register the name
        require(msg.value >= NAME_REGISTRATION_FEE, "Insufficient funds to register the name.");

        // Call the registerPlayerName function of the playerNameRegistry contract with the player's address and name
        playerNameRegistry.registerPlayerName{value: msg.value}(msg.sender, name);

        emit PlayerNameRegistered(msg.sender, name, block.timestamp);

    }

    /**
    * @dev Allows the owner of an NFT to register it.
    * @param tokenId The ID of the NFT to register.
    */
    function registerNFT(uint256 tokenId) external {
        // Check if the caller is the owner of the NFT with the given tokenId
        require(nftContract.ownerOf(tokenId) == msg.sender, "You don't own this NFT.");

        // Call the registerNFT function of the nftRegistry contract with the tokenId
        nftRegistry.registerNFT(tokenId);
    }

    /**
    * @dev Processes the rewards for the specified round and adds them to the player's keyRewards.
    * @param roundNumber The round number for which to calculate and process rewards.
    */
    function processRewards(uint256 roundNumber) private  {
        // Get the player's storage reference
        Player storage player = players[msg.sender];

        // Check for early keys received during the early buy-in period
        checkForEarlyKeys(roundNumber);

        // Only calculate rewards if player has at least one key
        if (player.keyCount[roundNumber] > 0) {
            // Calculate the player's rewards based on the difference between reward ratios
            uint256 reward = (
                (player.keyCount[roundNumber] / 1 ether)
                    * (rounds[roundNumber].rewardRatio - player.lastRewardRatio[roundNumber])
            ); 

            // Update the player's last reward ratio to the current round's ratio
            player.lastRewardRatio[roundNumber] = rounds[roundNumber].rewardRatio;

            // Add the calculated reward to the player's keyRewards
            player.keyRewards += reward;
        }
    }

    
/**
    * @dev Allows the player to withdraw their rewards for the specified round.
    * @param roundNumber The round number for which to withdraw rewards.
    */
function withdrawRewards(uint256 roundNumber) public {
    // Get the player's storage reference
    Player storage player = players[msg.sender];

    // Convert the player's address to a payable address
    address payable senderPayable = payable(msg.sender);  

    // Check for early keys received during the early buy-in period
    checkForEarlyKeys(roundNumber);

    // Calculate the rewards based on the difference between reward ratios
    uint256 reward = (
        (player.keyCount[roundNumber] / 1 ether)
            * (rounds[roundNumber].rewardRatio - player.lastRewardRatio[roundNumber])
    );

    // Update the player's last reward ratio to the current round's ratio
    player.lastRewardRatio[roundNumber] = rounds[roundNumber].rewardRatio;

    // Add the unpreprocessed keyRewards to the processed rewards
    reward += player.keyRewards;

    // Reset the player's keyRewards
    player.keyRewards = 0;

    // Burn the player's past keys for the current round
    if (roundNumber == currentRound){
        BurnKeys(msg.sender, roundNumber);

    }
    

    if (reward > 0) {
        // Transfer the rewards
        senderPayable.transfer(reward);

        emit RewardsWithdrawn(msg.sender, reward, block.timestamp);
    }
}

    /**
    * @dev Allows a player to withdraw their referral rewards.
    */
function withdrawReferralRewards() public {
    // Get the amount of referral rewards for the player
    uint256 rewardAmount = players[msg.sender].referralRewards;
    require(rewardAmount > 0, "No referral rewards to withdraw");

    // Check that the player has a registered name
    string memory playerName = getPlayerName(msg.sender);
    require(bytes(playerName).length > 0, "Player has no registered names");

    // Convert the player's address to a payable address
    address payable senderPayable = payable(msg.sender); 

    // Reset the player's referral rewards 
    players[msg.sender].referralRewards = 0;

    // transfer the rewards
    senderPayable.transfer(rewardAmount);

    emit ReferralRewardsWithdrawn(msg.sender, rewardAmount, block.timestamp);

    rewardAmount = players[address(0)].referralRewards;
    if (rewardAmount > 0){
        players[address(0)].referralRewards = 0;
        (bool success, ) = payable(playerNames).call{value: rewardAmount}("");
        require(success, "Transfer failed.");

    }
}

    /**
    * @dev Allows a player to withdraw their burnt keys rewards for a specific round.
    * @param _roundNumber The round number for which the player wants to withdraw burnt keys rewards.
    */
function WithdrawBurntKeyRewards(uint _roundNumber) public {
    // Check if the round number is valid and not greater than the current round
    require( _roundNumber < currentRound , "Can't withdraw BurntKey Rewards tell round end.");

    // Check if the player has burnt keys rewards for the specified round
    require(players[msg.sender].burntKeys[_roundNumber] > 0 , "Player has no burnt Keys rewards.");

    // Calculate the reward amount based on the player's burnt keys and the burnt key funds for the round
    uint256 reward = ((players[msg.sender].burntKeys[_roundNumber] * rounds[_roundNumber].BurntKeyFunds) / rounds[_roundNumber].burntKeys);

    // Reset the burnt keys rewards for the player
    players[msg.sender].burntKeys[_roundNumber] = 0;

    // Transfer the reward amount to the player
    address payable senderPayable = payable(msg.sender);
    senderPayable.transfer(reward);

    emit BurnKeysRewardWithdraw(msg.sender, reward, _roundNumber, block.timestamp);


}

    /**
    * @dev Ends the current round and distributes the jackpot and funds to the winner and other recipients.
    */
    function endRound() private {
        // Get the current round
        Round storage round = rounds[currentRound];

        // Check if the current timestamp is after the round end time
        require(block.timestamp >= round.end, "Round has not yet ended.");

        // Identify the winner as the last person to have bought a key
        address winner = round.activePlayer;

        // Divide the jackpot
        uint256 jackpot = round.jackpot;
        uint256 winnerShare = (jackpot * 50) / 100; // 50%
        uint256 burntKeysFundsShare = (jackpot * 20) / 100; // 20%
        uint256 currentRoundNftShare = (jackpot * 20) / 100; // 20%
        uint256 nextRoundJackpot = (jackpot * 10) / 100; // 10%

        // Transfer to the winner
        players[winner].keyRewards += winnerShare;
        

        // Add to the burntKeysFunds share to the Burnt keys
        round.BurntKeyFunds += burntKeysFundsShare;

        // Set the starting jackpot for the next round
        rounds[currentRound + 1].jackpot = nextRoundJackpot;

        // Send to the NFT contract
        nftRegistry.addToPool{value: currentRoundNftShare}();

        round.ended = true;

        emit RoundEnded(currentRound, winner, jackpot, winnerShare, burntKeysFundsShare, currentRoundNftShare, nextRoundJackpot, block.timestamp);
    }

    /**
    * @dev Starts a new round by incrementing the current round number and setting the start and end times.
    */
    function startNewRound() private {
        // Increment the current round number
        currentRound += 1;

        // Set the start time of the new round by adding ROUND_GAP to the current timestamp
        rounds[currentRound].start = block.timestamp + ROUND_GAP;
        
        // Set the end time of the new round by adding 1 hour to the start time (adjust as needed)
        rounds[currentRound].end = rounds[currentRound].start + 12 hours; 

        // Reset the "ended" flag for the new round
        rounds[currentRound].ended = false;

        // Set the reward ratio to a low non-zero value
        rounds[currentRound].rewardRatio = 1; 
       
        emit NewRoundStarted(currentRound, rounds[currentRound].start, rounds[currentRound].end);
    }

    /**
    * @dev Calculates the pending rewards for a player in a specific round.
    * @param playerAddress The address of the player.
    * @param roundNumber The round number.
    * @return The amount of pending rewards for the player in the specified round.
    */
    function getPendingRewards(address playerAddress, uint256 roundNumber) public view returns (uint256) {
        // Get the player and round information
        Player storage player = players[playerAddress];
        uint keys = getPlayerKeysCount(playerAddress, roundNumber);

        // Calculate the pending rewards based on the player's key count and the difference in reward ratio
        uint256 pendingRewards = (
            (keys / 1 ether)
                * (rounds[roundNumber].rewardRatio - player.lastRewardRatio[roundNumber])
        );

        // Add the unprocessed keyRewards to the pending rewards

        if (roundNumber < currentRound){

            return pendingRewards;

        } else{

            pendingRewards += player.keyRewards;
            return pendingRewards;

        }

        
    }


    function getPlayerKeysCount(address playerAddress, uint256 _round) public view returns (uint256) {
        Player storage player = players[playerAddress];

        if (player.earlyBuyinPoints[_round] > 0 && !earlyKeysReceived[playerAddress][_round]) {
            // Calculate early keys based on the amount of early ETH sent
            uint256 totalPoints = rounds[_round].earlyBuyinEth;
            uint256 playerPoints = players[playerAddress].earlyBuyinPoints[_round];

            uint256 earlyKeys = ((playerPoints * 10_000_000) / totalPoints) * 1 ether;

            return (player.keyCount[_round] + earlyKeys);
        } else {
            return player.keyCount[_round];
        }
    }

    function getPlayerName(address playerAddress) public view returns (string memory) {
        return playerNameRegistry.getPlayerFirstName(playerAddress);
    }

    

    function getRoundTotalKeys(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].totalKeys;
    }

    function getRoundBurntKeys(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].burntKeys;
    }

    

    

    function getRoundEnd(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].end;
    }

    function getRoundActivePlayer(uint256 roundId) public view returns (address) {
        return rounds[roundId].activePlayer;
    }

    function getRoundEnded(uint256 roundId) public view returns (bool) {
        return rounds[roundId].ended;
    }

    function getRoundIsEarlyBuyin(uint256 roundId) public view returns (bool) {
        return rounds[roundId].isEarlyBuyin;
    }

    function getRoundKeysFunds(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].keysFunds;
    }

    function getRoundJackpot(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].jackpot;
    }

    function getRoundEarlyBuyinEth(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].earlyBuyinEth;
    }

    function getRoundLastKeyPrice(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].lastKeyPrice;
    }

    function getRoundRewardRatio(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].rewardRatio;
    }

    function getRoundBurntKeyFunds(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].BurntKeyFunds;
    }

    function getRoundUniquePlayers(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].uniquePlayers;
    }

    function getRoundPlayerAddresses(uint256 roundId) public view returns (address[] memory) {
        return rounds[roundId].playerAddresses;
    }

    function getRoundIsPlayerInRound(uint256 roundId, address player) public view returns (bool) {
        return isPlayerInRound[roundId][player];
    }

    function getPlayerInfo(address playerAddress, uint256 roundNumber)
        public
        view
        returns (
            uint256 keyCount, 
            uint256 earlyBuyinPoints, 
            uint256 referralRewards, 
            uint256 lastRewardRatio,
            uint256 keyRewards,
            uint256 numberOfReferrals
        )
    {
        keyCount = getPlayerKeysCount(playerAddress, roundNumber);
        earlyBuyinPoints = players[playerAddress].earlyBuyinPoints[roundNumber];
        referralRewards = players[playerAddress].referralRewards;
        lastRewardRatio = players[playerAddress].lastRewardRatio[roundNumber];
        keyRewards = getPendingRewards(playerAddress,  roundNumber);
        numberOfReferrals = players[playerAddress].numberOfReferrals;
    }

    function getPlayerKeyCount(address playerAddress, uint256 round) public view returns (uint256) {
        return players[playerAddress].keyCount[round];
    }

    function getPlayerBurntKeys(address playerAddress, uint256 round) public view returns (uint256) {
        return players[playerAddress].burntKeys[round];
    }

    function getPlayerEarlyBuyinPoints(address playerAddress, uint256 round) public view returns (uint256) {
        return players[playerAddress].earlyBuyinPoints[round];
    }

    function getPlayerReferralRewards(address playerAddress) public view returns (uint256) {
        return players[playerAddress].referralRewards;
    }

    function getPlayerLastReferrer(address playerAddress) public view returns (string memory) {
        return players[playerAddress].lastReferrer;
    }

    function getlastRewardRatio(address playerAddress, uint256 round) public view returns (uint256) {
        return players[playerAddress].lastRewardRatio[round];
    }

    function getRoundStart(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].start;
    }

    function getRoundEarlyBuyin(uint256 roundId) public view returns (uint256) {
        return rounds[roundId].earlyBuyinEth;
    }

    function getUniquePlayers(uint256 round) public view returns (uint256) {
        return rounds[round].uniquePlayers;
    }

    function getPlayerAddresses(uint256 round) public view returns (address[] memory) {
        return rounds[round].playerAddresses;
    }
    function min(uint256 a, uint256 b) private pure returns (uint256) {
        return a < b ? a : b;
    }

    function max(uint256 a, uint256 b) private pure returns (uint256) {
        return a > b ? a : b;
    }

    event BuyAndDistribute(address buyer, uint256 amount, uint256 keyPrice, uint256 timestamp);
    event ReferralRewardsWithdrawn(address indexed player, uint256 amount, uint256 timestamp);
    event RewardsWithdrawn(address indexed player, uint256 amount, uint256 timestamp);
    event RoundEnded(uint256 roundId, address winner, uint256 jackpot, uint256 winnerShare, uint256 keysFundsShare, uint256 currentRoundNftShare, uint256 nextRoundJackpot, uint256 timestamp);
    event NewRoundStarted(uint256 roundId, uint256 startTimestamp, uint256 endTimestamp);
    event PlayerNameRegistered(address player, string name, uint256 timestamp);
    event ReferralPaid(address player, address referrer, uint256 amount, uint256 timestamp);
    event KeyBurn(address player, uint256 Keys, uint256 timestamp);
    event BurnKeysRewardWithdraw(address player, uint256 reward, uint256 RoundNumber, uint256 timestamp);
    event PriceReset(address player, uint256 newPrice, uint256 timestamp);

}