// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: contracts/Game.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./IterableMapping.sol";

contract Game is Ownable {
using IterableMapping for IterableMapping.Map;
IterableMapping.Map private playersScores;
address public manager;
address payable public address1;
uint256 public playPrice = 1000000000000000; // 0.001 Matic
uint256 public address1Share;
bool public gameStarted = false;
uint256 public highScore = 0;

struct Player {
    address player;
    uint256 score;
}

// Evento para notificar cuando un jugador paga y juega
event PlayerPaid(address indexed player, uint256 amount);

modifier onlyManager() {
    require(manager == msg.sender, "Only manager can call this function.");
    _;
}

receive() external payable {}

constructor(address payable _address1, address _manager) {
    manager = _manager;
    address1 = _address1;
    gameStarted = true; // remove on mainnet
}

function setAddress1(address payable _address1) external onlyOwner {
    address1 = _address1;
}

function setPlayPrice(uint256 _playPrice) external onlyOwner {
    playPrice = _playPrice;
}

function setManager(address _manager) external onlyOwner {
    manager = _manager;
}

function enableGame() external onlyOwner {
    require(!gameStarted, "El juego ya ha comenzado.");
    gameStarted = true;
}

function setPlayerScore(
    address payable _player,
    uint256 _score
) external onlyManager {
    require(gameStarted, "El juego aun no ha comenzado.");
    highScore = _score;
    playersScores.set(_player, _score);
}

// Función para que un usuario pague el fee y juegue
function play() external payable {
    require(gameStarted, "El juego aun no ha comenzado.");
    require(msg.value == playPrice, "Invalid amount of ETH.");

    // Calcula los shares para las direcciones
    address1Share = (msg.value * 30) / 100;

    // Transfiere los pagos a las direcciones
    (bool sent, ) = address1.call{value: address1Share}("");
    require(sent, "Failed to send Ether");

    // Registra al jugador
    playersScores.set(msg.sender, 0);

    // Emite un evento para notificar que un jugador ha pagado y jugado
    emit PlayerPaid(msg.sender, msg.value);
}

// Función para finalizar el juego y retirar el saldo restante
function endGame() external onlyOwner {
    require(gameStarted, "El juego aun no ha comenzado.");
    require(
        playersScores.size() >= 3,
        "Debe haber al menos 3 jugadores para distribuir premios."
    );

    // Obtiene las tres mejores direcciones de jugadores por puntuación
    address[] memory topThreePlayers = getTopThreePlayers();

    // Calcula la parte para los tres ganadores (70% restante)
    uint256 remainingShare = address(this).balance;

    // Calcula las partes para los tres ganadores
    uint256 firstPlaceShare = (remainingShare * 40) / 100;
    uint256 secondPlaceShare = (remainingShare * 30) / 100;
    uint256 thirdPlaceShare = (remainingShare * 30) / 100;

    gameStarted = false;
    for (uint256 i = 0; i < playersScores.size(); i++) {
        playersScores.remove(playersScores.getKeyAtIndex(i));
    }
    highScore = 0;

    // Transfiere las partes a los tres ganadores
    (bool firstSent, ) = topThreePlayers[0].call{value: firstPlaceShare}(
        ""
    );
    require(firstSent, "Fallo al enviar Ether al primer ganador.");

    (bool secondSent, ) = topThreePlayers[1].call{value: secondPlaceShare}(
        ""
    );
    require(secondSent, "Fallo al enviar Ether al segundo ganador.");

    (bool thirdSent, ) = topThreePlayers[2].call{value: thirdPlaceShare}(
        ""
    );
    require(thirdSent, "Fallo al enviar Ether al tercer ganador.");
}

function clearRanking() external onlyOwner {
    for (uint256 i = 0; i < playersScores.size(); i++) {
        playersScores.remove(playersScores.getKeyAtIndex(i));
    }
    highScore = 0;
}

// Función para que el propietario vea todas las direcciones de los jugadores
function getRanking() external view returns (Player[] memory) {
    Player[] memory players = new Player[](playersScores.size());
    for (uint256 i = 0; i < playersScores.size(); i++) {
        players[i] = Player({
            player: playersScores.getKeyAtIndex(i),
            score: playersScores.get(playersScores.getKeyAtIndex(i))
        });
    }
    return players;
}

// function for get top 3 players by score
function getTopThreePlayers() public view returns (address[] memory) {
    address[] memory topThreePlayers = new address[](3);
uint256[3] memory topScores = [uint256(0), uint256(0), uint256(0)];
    for (uint256 i = 0; i < playersScores.size(); i++) {
        uint256 score = playersScores.get(playersScores.getKeyAtIndex(i));
        if (score > topScores[0]) {
            topScores[2] = topScores[1];
            topScores[1] = topScores[0];
            topScores[0] = score;
            topThreePlayers[2] = topThreePlayers[1];
            topThreePlayers[1] = topThreePlayers[0];
            topThreePlayers[0] = playersScores.getKeyAtIndex(i);
        } else if (score > topScores[1]) {
            topScores[2] = topScores[1];
            topScores[1] = score;
            topThreePlayers[2] = topThreePlayers[1];
            topThreePlayers[1] = playersScores.getKeyAtIndex(i);
        } else if (score > topScores[2]) {
            topScores[2] = score;
            topThreePlayers[2] = playersScores.getKeyAtIndex(i);
        }
    }
    return topThreePlayers;
}
}
// File: contracts/IterableMapping.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

library IterableMapping {
    // Iterable mapping from address to uint;
    struct Map {
        address[] keys;
        mapping(address => uint256) values;
        mapping(address => uint256) indexOf;
        mapping(address => bool) inserted;
    }

    function get(Map storage map, address key) public view returns (uint256) {
        return map.values[key];
    }

    function getIndexOfKey(Map storage map, address key)
        public
        view
        returns (int256)
    {
        if (!map.inserted[key]) {
            return -1;
        }
        return int256(map.indexOf[key]);
    }

    function getKeyAtIndex(Map storage map, uint256 index)
        public
        view
        returns (address)
    {
        return map.keys[index];
    }

    function size(Map storage map) public view returns (uint256) {
        return map.keys.length;
    }

    function set(
        Map storage map,
        address key,
        uint256 val
    ) public {
        if (map.inserted[key]) {
            map.values[key] = val;
        } else {
            map.inserted[key] = true;
            map.values[key] = val;
            map.indexOf[key] = map.keys.length;
            map.keys.push(key);
        }
    }

    function remove(Map storage map, address key) public {
        if (!map.inserted[key]) {
            return;
        }

        delete map.inserted[key];
        delete map.values[key];

        uint256 index = map.indexOf[key];
        uint256 lastIndex = map.keys.length - 1;
        address lastKey = map.keys[lastIndex];

        map.indexOf[lastKey] = index;
        delete map.indexOf[key];

        map.keys[index] = lastKey;
        map.keys.pop();
    }
}