/**
* @title whoopdoopRaffle
* @author Captain Unknown
*/
pragma solidity ^0.8.19.0;




interface IERC20{
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint256);
    function balanceOf(address _owner) external view returns (uint256 balance);
    function transfer(address _to, uint256 _value) external returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
    function approve(address _spender, uint256 _value) external returns (bool success);
    function allowance(address _owner, address _spender) external view returns (uint256 remaining);
}

interface IERC721 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    function approve(address to, uint256 tokenId) external;
    function setApprovalForAll(address operator, bool approved) external;
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function balanceOf(address owner) external view returns (uint256 balance);
    function transferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
}

contract WhoopDoopRaffle is Ownable, Initializable {
    using Counters for Counters.Counter;
    Counters.Counter public currentRaffleId;

    struct Prize {
        address NFTContract;
        uint256 NFTTokenId;
    }

    struct Raffle {
        uint256 raffleId;
        uint256 ethPrice;
        uint256 doopPrice;
        bool payableWithDoop;
        bool hasEnded;
        uint256 endTime;
        address[] participants;
        address winner;
        Prize rafflePrize;
    }

    // Context
    address public EAPass;
    address public WDCollection;
    address public doopToken;
    uint256 public maxTicketCount;

    mapping (address => bool) private admin;
    mapping (uint256 => Raffle) public OnGoingRaffles;

    event RaffleStarted(uint256 indexed raffleId, uint256 price, uint256 endTime, Prize rafflePrize);
    event Winner(uint256 indexed raffleId, address winner);

    constructor(address _EAPass, address _WDCollection, address _doopToken, uint256 _maxTicketCount) {
        EAPass = _EAPass;
        WDCollection = _WDCollection;
        doopToken = _doopToken; 
        maxTicketCount = _maxTicketCount;
        _disableInitializers();
    }

    function startRaffle(uint256 _priceInEth, uint256 _doopPrice, bool _payableWithDoop, uint256 _duration, address _prizeNFTContract, uint256 _prizeNFTTokenId) public {
        require(admin[msg.sender], "Only admins can invoke this function.");
        uint256 _endTime = block.timestamp + _duration;

        Raffle memory newRaffle = Raffle(currentRaffleId.current(), _priceInEth, _doopPrice, _payableWithDoop, false, _endTime,
            new address[](0), address(0), Prize(_prizeNFTContract, _prizeNFTTokenId));
        OnGoingRaffles[currentRaffleId.current()] = newRaffle;

        currentRaffleId.increment();
        IERC721(_prizeNFTContract).transferFrom(msg.sender, address(this), _prizeNFTTokenId);
        emit RaffleStarted(newRaffle.raffleId, newRaffle.ethPrice, newRaffle.endTime, newRaffle.rafflePrize);
    }

    function buyTicket(uint256 _RaffleId, uint256 ticketCount) public payable {
        require(raffleExists(_RaffleId), "Invalid Raffle Id.");
        require(block.timestamp < OnGoingRaffles[_RaffleId].endTime, "Raffle entry time is over.");
        require(!(OnGoingRaffles[_RaffleId].hasEnded), "Raffle has already ended.");
        require(ticketCount > 0, "Requested number of tickets should be greater than 0.");
        require(!admin[msg.sender], "Admins can't participate in the raffle.");
        require(msg.sender != owner(), "Owner can't participate in the raffle.");
        require(ticketCount <= maxTicketCount, "Cant buy this much tickets in one go.");

        if (OnGoingRaffles[_RaffleId].payableWithDoop) {
            require(IERC20(doopToken).balanceOf(msg.sender) >= OnGoingRaffles[_RaffleId].doopPrice * ticketCount, "Not enough DOOP balance.");
            require(IERC20(doopToken).transferFrom(msg.sender, address(this), OnGoingRaffles[_RaffleId].doopPrice * ticketCount), "Doop Transfer Failed.");
        } else {
            require(OnGoingRaffles[_RaffleId].ethPrice * ticketCount == msg.value, "Invalid value sent.");
        }

        registerPurchase(_RaffleId, ticketCount);
    }

    function registerPurchase(uint256 _RaffleId, uint256 ticketCount) private {
        if (isHolderOf(msg.sender, EAPass)) {
            ticketCount *= 5;
        } else if (isHolderOf(msg.sender, WDCollection)) {
            ticketCount *= 3;
        } else {
            ticketCount *= 1;
        }

        for (uint256 i = 0; i < ticketCount; i++) {
            OnGoingRaffles[_RaffleId].participants.push(msg.sender);
        }
    }

    function pickWinner(uint256 _RaffleId, string memory seed) private {
        uint256 randomIndex = uint256(keccak256(abi.encodePacked(
                block.timestamp,
                seed,
                blockhash(block.number - 1)
            ))) % OnGoingRaffles[_RaffleId].participants.length;
        address winner = OnGoingRaffles[_RaffleId].participants[randomIndex];
        OnGoingRaffles[_RaffleId].winner = winner;

        IERC721(OnGoingRaffles[_RaffleId].rafflePrize.NFTContract).safeTransferFrom(address(this), winner, OnGoingRaffles[_RaffleId].rafflePrize.NFTTokenId);
        emit Winner(_RaffleId, winner);
    }

    function endRaffle(uint256 _RaffleId, string memory seed) public {
        require(msg.sender == owner() || admin[msg.sender], "Either the owner or an admin can end the raffle.");
        require(raffleExists(_RaffleId), "Invalid Raffle Id.");
        require(block.timestamp >= OnGoingRaffles[_RaffleId].endTime, "Raffle has not ended yet.");
        require(!(OnGoingRaffles[_RaffleId].hasEnded), "Raffle has already ended.");

        OnGoingRaffles[_RaffleId].hasEnded = true;

        if (OnGoingRaffles[_RaffleId].participants.length == 0) {
            IERC721(OnGoingRaffles[_RaffleId].rafflePrize.NFTContract).safeTransferFrom(address(this), owner(), OnGoingRaffles[_RaffleId].rafflePrize.NFTTokenId);
        } else {
            pickWinner(_RaffleId, seed);
        }
    }
    
    function cancelRaffle(uint256 _RaffleId) public {
        require(raffleExists(_RaffleId), "Invalid Raffle Id.");
        require(msg.sender == owner() || admin[msg.sender], "Either the owner or an admin can cancel the raffle.");
        OnGoingRaffles[_RaffleId].hasEnded = true;
        IERC721(OnGoingRaffles[_RaffleId].rafflePrize.NFTContract).safeTransferFrom(address(this), owner(), OnGoingRaffles[_RaffleId].rafflePrize.NFTTokenId);
    }

    // Utilities
    function raffleExists(uint256 _RaffleId) public view returns (bool) {
        return _RaffleId < currentRaffleId.current();
    }

    function getTotalParticipants(uint256 _RaffleId) public view returns (uint256) {
        require(raffleExists(_RaffleId), "Invalid Raffle Id.");
        return OnGoingRaffles[_RaffleId].participants.length;
    }

    function getOngoingRaffles() public view returns (uint256[] memory) {
        uint256[] memory raffleIds = new uint256[](currentRaffleId.current());
        uint256 count = 0;
        for (uint256 i = 0; i < currentRaffleId.current(); i++) {
            if (!OnGoingRaffles[i].hasEnded) {
                raffleIds[count] = i;
                count++;
            }
        }
        uint256[] memory result = new uint256[](count);
        for (uint256 i = 0; i < count; i++) {
            result[i] = raffleIds[i];
        }
        return result;
    }

    function getEndedRaffles() public view returns (uint256[] memory) {
        uint256[] memory raffleIds = new uint256[](currentRaffleId.current());
        uint256 count = 0;
        for (uint256 i = 0; i < currentRaffleId.current(); i++) {
            if (OnGoingRaffles[i].hasEnded) {
                raffleIds[count] = i;
                count++;
            }
        }
        uint256[] memory result = new uint256[](count);
        for (uint256 i = 0; i < count; i++) {
            result[i] = raffleIds[i];
        }
        return result;
    }

    function isHolderOf(address user, address NFT) private view returns(bool) {
        return IERC721(NFT).balanceOf(user) > 0;
    }

    function withdrawAll() public payable onlyOwner {
        require(address(this).balance > 0 || IERC20(doopToken).balanceOf(address(this)) > 0, "Balance is 0.");
        IERC20(doopToken).transfer(msg.sender, IERC20(doopToken).balanceOf(address(this)));
        payable(msg.sender).transfer(address(this).balance);
    }

    function withdrawEth(uint256 amount) public payable onlyOwner {
        require(address(this).balance >= amount, "Insufficient balance in the contract.");
        payable(msg.sender).transfer(amount);
    }

    function withdrawDoop(uint256 amount) public payable onlyOwner {
        require(IERC20(doopToken).balanceOf(address(this)) >= amount, "Insufficient balance in the contract.");
        IERC20(doopToken).transfer(msg.sender, amount);
    }

    function addAdmin(address _admin) public onlyOwner {
        admin[_admin] = true;
    }

    function removeAdmin(address _admin) public onlyOwner {
        admin[_admin] = false;
    }
}
[{"inputs":[{"internalType":"address","name":"_EAPass","type":"address"},{"internalType":"address","name":"_WDCollection","type":"address"},{"internalType":"address","name":"_doopToken","type":"address"},{"internalType":"uint256","name":"_maxTicketCount","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"raffleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"endTime","type":"uint256"},{"components":[{"internalType":"address","name":"NFTContract","type":"address"},{"internalType":"uint256","name":"NFTTokenId","type":"uint256"}],"indexed":false,"internalType":"struct WhoopDoopRaffle.Prize","name":"rafflePrize","type":"tuple"}],"name":"RaffleStarted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"raffleId","type":"uint256"},{"indexed":false,"internalType":"address","name":"winner","type":"address"}],"name":"Winner","type":"event"},{"inputs":[],"name":"EAPass","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"OnGoingRaffles","outputs":[{"internalType":"uint256","name":"raffleId","type":"uint256"},{"internalType":"uint256","name":"ethPrice","type":"uint256"},{"internalType":"uint256","name":"doopPrice","type":"uint256"},{"internalType":"bool","name":"payableWithDoop","type":"bool"},{"internalType":"bool","name":"hasEnded","type":"bool"},{"internalType":"uint256","name":"endTime","type":"uint256"},{"internalType":"address","name":"winner","type":"address"},{"components":[{"internalType":"address","name":"NFTContract","type":"address"},{"internalType":"uint256","name":"NFTTokenId","type":"uint256"}],"internalType":"struct WhoopDoopRaffle.Prize","name":"rafflePrize","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WDCollection","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"name":"addAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_RaffleId","type":"uint256"},{"internalType":"uint256","name":"ticketCount","type":"uint256"}],"name":"buyTicket","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_RaffleId","type":"uint256"}],"name":"cancelRaffle","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"currentRaffleId","outputs":[{"internalType":"uint256","name":"_value","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"doopToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_RaffleId","type":"uint256"},{"internalType":"string","name":"seed","type":"string"}],"name":"endRaffle","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getEndedRaffles","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOngoingRaffles","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_RaffleId","type":"uint256"}],"name":"getTotalParticipants","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTicketCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_RaffleId","type":"uint256"}],"name":"raffleExists","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"name":"removeAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_priceInEth","type":"uint256"},{"internalType":"uint256","name":"_doopPrice","type":"uint256"},{"internalType":"bool","name":"_payableWithDoop","type":"bool"},{"internalType":"uint256","name":"_duration","type":"uint256"},{"internalType":"address","name":"_prizeNFTContract","type":"address"},{"internalType":"uint256","name":"_prizeNFTTokenId","type":"uint256"}],"name":"startRaffle","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawAll","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawDoop","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawEth","outputs":[],"stateMutability":"payable","type":"function"}]