// 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/clowntown/ClownTownGame.sol



/**
                                                                       ,----,
                                                                     ,/   .`|
  ,----..    ,--,                                                  ,`   .'  :
 /   /   \ ,--.'|                                                ;    ;     /
|   :     :|  | :     ,---.           .---.      ,---,         .'___,/    ,'  ,---.           .---.      ,---,
.   |  ;. /:  : '    '   ,'\         /. ./|  ,-+-. /  |        |    :     |  '   ,'\         /. ./|  ,-+-. /  |
.   ; /--` |  ' |   /   /   |     .-'-. ' | ,--.'|'   |        ;    |.';  ; /   /   |     .-'-. ' | ,--.'|'   |
;   | ;    '  | |  .   ; ,. :    /___/ \: ||   |  ,"' |        `----'  |  |.   ; ,. :    /___/ \: ||   |  ,"' |
|   : |    |  | :  '   | |: : .-'.. '   ' .|   | /  | |            '   :  ;'   | |: : .-'.. '   ' .|   | /  | |
.   | '___ '  : |__'   | .; :/___/ \:     '|   | |  | |            |   |  ''   | .; :/___/ \:     '|   | |  | |
'   ; : .'||  | '.'|   :    |.   \  ' .\   |   | |  |/             '   :  ||   :    |.   \  ' .\   |   | |  |/
'   | '/  :;  :    ;\   \  /  \   \   ' \ ||   | |--'              ;   |.'  \   \  /  \   \   ' \ ||   | |--'
|   :    / |  ,   /  `----'    \   \  |--" |   |/                  '---'     `----'    \   \  |--" |   |/
 \   \ .'   ---`-'              \   \ |    '---'                                        \   \ |    '---'
  `---`                          '---"                                                   '---"

Its 🤡 clown's 🤡
.
all the
.
.
way
.
.
.
down
.
.
.
.
**/

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

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

contract ClownTownGame is Ownable {
  int256 public VERSION = -4;

  event CreateRound(address indexed user, uint256 roundIndex);
  event Wager(address indexed user, uint256 indexed roundIndex, bool sideA, uint256 amount);
  event ClaimWinnings(address indexed user, uint256 indexed roundIndex, uint256 amount);
  event ClaimCreatorFees(address indexed user, uint256 indexed roundIndex, uint256 amount);

  uint256 public minBet;
  uint256 public maxBet;

  // Pack into single storage slot
  uint32 public roundDuration;
  uint16 public maxRoundDescription;
  uint16 public betFeeBpsTotal;
  uint16 public betFeeBpsToCreator;
  uint16 public betFeeBpsToStakers;
  uint64 public numRounds;

  IClownTownStaking public stakingContract;
  address payable public feeWallet;
  RoundInfo[] public rounds;

  mapping(address => mapping(uint256 => bool)) _userRounds;
  mapping(address => uint256[]) public userRounds;

  bool betaPeriod;

  struct RoundInfo {
    address creator;
    string description;

    string sideAHandle;
    string sideBHandle;

    uint256 startBlock;
    uint256 endBlock;

    uint256 totalSideA;
    uint256 totalSideB;

    mapping(address => uint256) sideABets;
    mapping(address => uint256) sideBBets;
    mapping(address => bool) winningsClaimed;

    uint256 unclaimedCreatorFees;
  }

  constructor(
    IClownTownStaking _stakingContract,
    address payable _feeWallet,
    uint256 _minBet,
    uint256 _maxBet,
    uint32 _roundDuration,
    uint16 _maxRoundDescription,
    uint16 _betFeeBpsTotal,
    uint16 _betFeeBpsToCreator,
    uint16 _betFeeBpsToStakers) {
    require(address(_stakingContract)!=address(0));
    require(_feeWallet!=address(0));

    stakingContract = _stakingContract;
    feeWallet = _feeWallet;
    setRoundParams(_minBet, _maxBet, _roundDuration, _maxRoundDescription);
    setBetFees(_betFeeBpsTotal, _betFeeBpsToCreator, _betFeeBpsToStakers);
    betaPeriod = true;
  }

  function setStakingContract(IClownTownStaking _stakingContract) public onlyOwner {
    require(address(_stakingContract)!=address(0));
    stakingContract = _stakingContract;
  }

  function setFeeWallet(address payable _feeWallet) public onlyOwner {
    require(_feeWallet!=address(0));
    feeWallet = _feeWallet;
  }

  function setRoundParams(
    uint256 _minBet,
    uint256 _maxBet,
    uint32 _roundDuration,
    uint16 _maxRoundDescription) public onlyOwner {
    require(_minBet > 0);
    require(_maxBet >= _minBet);
    require(_roundDuration > 0);
    minBet = _minBet;
    maxBet = _maxBet;
    roundDuration = _roundDuration;
    maxRoundDescription = _maxRoundDescription;
  }

  function setBetFees(
    uint16 _betFeeBpsTotal,
    uint16 _betFeeBpsToCreator,
    uint16 _betFeeBpsToStakers) public onlyOwner {
    require(_betFeeBpsTotal < 10000);
    require(_betFeeBpsToCreator+_betFeeBpsToStakers <= _betFeeBpsTotal);
    betFeeBpsTotal = _betFeeBpsTotal;
    betFeeBpsToCreator = _betFeeBpsToCreator;
    betFeeBpsToStakers = _betFeeBpsToStakers;
  }

  // Irreversible
  function endBetaPeriod() public onlyOwner {
    betaPeriod = false;
  }

  // In case of contract bug. Permanently disabled after beta period
  function emergencyWithdraw(
    address payable target,
    uint256 amount) public onlyOwner {
    require(betaPeriod);
    target.transfer(amount);
  }

  function createRound(
    string calldata _description,
    string calldata _sideAHandle,
    string calldata _sideBHandle) public {
    require(bytes(_description).length <= maxRoundDescription);
    require(bytes(_sideAHandle).length <= 50);
    require(bytes(_sideBHandle).length <= 50);

    rounds.push();
    numRounds++;

    uint256 roundIndex = numRounds-1;
    RoundInfo storage round = rounds[roundIndex];
    round.creator = msg.sender;
    round.description = _description;
    round.sideAHandle = _sideAHandle;
    round.sideBHandle = _sideBHandle;
    round.startBlock = block.number;
    round.endBlock = block.number + roundDuration;

    if (!_userRounds[msg.sender][roundIndex]) {
      _userRounds[msg.sender][roundIndex] = true;
      userRounds[msg.sender].push(roundIndex);
    }

    emit CreateRound(msg.sender, roundIndex);
  }

  function wager(
    uint32 roundIndex,
    bool sideA) public payable {
    uint256 amount = msg.value;
    require(amount >= minBet, "wager: amount too small");
    require(amount <= maxBet, "wager: amount too big");
    require(roundIndex < rounds.length, "wager: invalid round");

    RoundInfo storage round = rounds[roundIndex];
    require(block.number <= round.endBlock, "wager: round over");

    uint256 feeTotal = (amount * betFeeBpsTotal) / 10000;
    uint256 feeToCreator = (amount * betFeeBpsToCreator) / 10000;
    uint256 feeToStakers = (amount * betFeeBpsToStakers) / 10000;
    require((feeToCreator+feeToStakers)<=feeTotal); // Sanity check

    uint256 amountAfterFee = amount - feeTotal;

    if (sideA) {
      round.totalSideA += amountAfterFee;
      round.sideABets[msg.sender] += amountAfterFee;
    }
    else {
      round.totalSideB += amountAfterFee;
      round.sideBBets[msg.sender] += amountAfterFee;
    }

    round.unclaimedCreatorFees += feeToCreator;
    stakingContract.addEthReward{value: feeToStakers}();
    feeWallet.transfer(feeTotal-feeToCreator-feeToStakers);

    if (!_userRounds[msg.sender][roundIndex]) {
      _userRounds[msg.sender][roundIndex] = true;
      userRounds[msg.sender].push(roundIndex);
    }

    emit Wager(msg.sender, roundIndex, sideA, amount);
  }

  function claimWinnings(uint256 roundIndex) public {
    RoundInfo storage round = rounds[roundIndex];
    require(round.endBlock < block.number);
    require(!round.winningsClaimed[msg.sender]);

    round.winningsClaimed[msg.sender] = true;
    uint256 winnings = currentWinnings(msg.sender, roundIndex);

    if (winnings > 0) {
      payable(msg.sender).transfer(winnings);
      emit ClaimWinnings(msg.sender, roundIndex, winnings);
    }
  }

  function currentWinnings(address user, uint256 roundIndex) public view returns (uint256) {
    RoundInfo storage round = rounds[roundIndex];
    if (round.totalSideA > round.totalSideB) {
      uint256 userBet = round.sideABets[user];
      return userBet + (userBet * round.totalSideB / round.totalSideA);
    }
    else if (round.totalSideB > round.totalSideA) {
      uint256 userBet = round.sideBBets[user];
      return userBet + (userBet * round.totalSideA / round.totalSideB);
    }
    else {
      return round.sideABets[user] + round.sideBBets[user];
    }
  }

  function claimCreatorFees(uint256 roundIndex) public {
    RoundInfo storage round = rounds[roundIndex];
    require(round.creator==msg.sender);
    require(round.endBlock < block.number);

    if (round.unclaimedCreatorFees > 0) {
      payable(msg.sender).transfer(round.unclaimedCreatorFees);
      emit ClaimCreatorFees(msg.sender, roundIndex, round.unclaimedCreatorFees);
      round.unclaimedCreatorFees = 0;
    }
  }

  function numUserRounds(address user) public view returns (uint256) {
    return userRounds[user].length;
  }

  function winningsClaimed(address user, uint256 roundIndex) public view returns (bool) {
    return rounds[roundIndex].winningsClaimed[user];
  }
}
// File: contracts/clowntown/IClownTownStaking.sol



/**
                                                                       ,----,
                                                                     ,/   .`|
  ,----..    ,--,                                                  ,`   .'  :
 /   /   \ ,--.'|                                                ;    ;     /
|   :     :|  | :     ,---.           .---.      ,---,         .'___,/    ,'  ,---.           .---.      ,---,
.   |  ;. /:  : '    '   ,'\         /. ./|  ,-+-. /  |        |    :     |  '   ,'\         /. ./|  ,-+-. /  |
.   ; /--` |  ' |   /   /   |     .-'-. ' | ,--.'|'   |        ;    |.';  ; /   /   |     .-'-. ' | ,--.'|'   |
;   | ;    '  | |  .   ; ,. :    /___/ \: ||   |  ,"' |        `----'  |  |.   ; ,. :    /___/ \: ||   |  ,"' |
|   : |    |  | :  '   | |: : .-'.. '   ' .|   | /  | |            '   :  ;'   | |: : .-'.. '   ' .|   | /  | |
.   | '___ '  : |__'   | .; :/___/ \:     '|   | |  | |            |   |  ''   | .; :/___/ \:     '|   | |  | |
'   ; : .'||  | '.'|   :    |.   \  ' .\   |   | |  |/             '   :  ||   :    |.   \  ' .\   |   | |  |/
'   | '/  :;  :    ;\   \  /  \   \   ' \ ||   | |--'              ;   |.'  \   \  /  \   \   ' \ ||   | |--'
|   :    / |  ,   /  `----'    \   \  |--" |   |/                  '---'     `----'    \   \  |--" |   |/
 \   \ .'   ---`-'              \   \ |    '---'                                        \   \ |    '---'
  `---`                          '---"                                                   '---"

Its 🤡 clown's 🤡
.
all the
.
.
way
.
.
.
down
.
.
.
.
**/

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

interface IClownTownStaking {
  function addEthReward() external payable;
  function postProcessClownReward(uint256 _amount) external;
}