// Contract: Sponsio
// Target function: function makePrediction(uint32 eventId, bytes32 predictionString) public EventExists(eventId) returns (bool)
7265637420616e73776572000000000000000000000000000000000000
        StringExactMatch,
        // Expected string that represents a date in the format yyyy.mm.dd. e.g.
        // If correct answer for Event is '2000.01.20' then prediction should be 
        // 0x323030302e30312e323000000000000000000000000000000000000000000000
        DateExactMatch,
        // Expected Unix Timestamp, an integer number representing the time as the number of seconds since January 1st, 1970 at 00:00:00 UTC.
        // If correct answer for Event is '948316800' (that is Wednesday, 19 January 2000 21:20:00) then prediction should be 
        // 0x3934383331363830300000000000000000000000000000000000000000000000
        DateTimeExactMatch
    }
    enum EventStatus {
        // No such event
        NotExists,
        // The Event is active and anyone can make predictions on it.
        // Status form Active can be changed only to Freezed and Finished
        Active,
        // The Event is freezed and nobody can make predictions on it
        // It can become Active again, Finished or Reverted
        Freezed,
        // Event is over but not finished yet. It means than noone can make predictions on it,
        // But results are still unknown. It can become only Finished or Reverted.
        OutOfTime,
        // The Event is finished and nobody can make predictions on it
        // Event prize is distributed and this status can never be changed
        Finished,
        // The Event is reverted and everyone who made a prediction on it can withdraw their prediction price on their wallet
        // Event prize will never be distributed and this status can never be changed
        Reverted
    }

    struct TimeSettings {
        // Timestamp when all predictions are done and Event is ended and become OutOfTime status.
        // If 0 the Event is timeless
        uint32 endTime;
        // Indicates whether Event can be restarted from OutOfTime status or not. 
        // If yes you can restart an event with another endTime value.
        uint8 canBeRestarted;
        // Time when the Event was last active. From this time depending on eventTimeToVotingClosing it is considered
        // Whether the prediction counts when determining the winners or not. 
        uint32 lastActiveTime;
    }
    struct RoyaltySetting {
        // Royalty for creator of the Event. Max is 15%, Min is 5%. 
        // Set by creator once when the event is created, cannot be changed
        uint8 royaltyForCreator;
        // Royalty for creator of the contract. Max is 5%, Min is 0%. 
        // Set by contract once when the event is created, cannot be changed
        uint8 royaltyForContract;
        // A flag that determines whether royalties will be charged from all participants in the event
        // Or only from those who make an incorrect prediction. 
        // In other words - whether to collect royalties from the winners of the event
        // If the flag is true, then royalties will be collected only after the end of the event !!!
        // If false, then immediately when the prediction is made
        uint8 royaltyOnlyLosers;
    }
    struct Event {
        // Event id - always unique
        uint32 eventId;
        // Event Text 
        bytes eventText;
        // Event Status
        EventStatus eventStatus;
        // It is a time interval before the voting closes, during which new predictions are not taken into account.
        // For example, if this interval is 24 hours and you want all predictions to be accepted no later than 6 hours before the real-world event occurs
        // You should freeze it after the real-world event occurs
        // wait until the current time minus 24 hours equals 6 hours before the event
        // and only then close the voting in the contract.
        uint32 eventTimeToVotingClosing;
        // Prediction Type
        PredictionType predictionType;
        // Price in Sponsio tokens for making prediction on this Event
        // cannot be changed
        uint256 predictionPrice;
        // Total prize pool to be shared after the Event ends
        // Updates every time someone makes a prediction
        uint256 prize;
        // Event creator address
        address creator;
        // Correct Answer. It is needed only for History. 
        // Before Event finished it is always 0x0000000000000000000000000000000000000000000000000000000000000000
        // Once the Event Finished, the value is set to the correct response and can never be changed again.
        bytes32 correctAnswer;
        // Event text language, needed to separate events of different language groups
        // Used only like filtering tag
        uint8 language;
        // royaltyForCreator, royaltyForContract and royaltyOnlyLosers Settings
        RoyaltySetting royaltySetting;
        // endTime and canBeRestarted Settings
        TimeSettings timeSettings; 
    }
}
abstract contract Depositable is ERC20, Ownable {
    function deposit() external payable {
        require(msg.value > 0, "Amount invalid");
        _mint(msg.sender, msg.value);
    }
    function withdraw() external {
        uint256 balanceSIO = balanceOf(msg.sender);
        require(balanceSIO > 0, "Zero balance");
        require(address(this).balance >= balanceSIO, "Insufficient contract balance");
        _burn(msg.sender, balanceSIO);
        (bool success,) = msg.sender.call{value: balanceSIO}("");
        require(success, "Failed");
    }
    // If somehow we got ETH with 0 supply of SIO we can withdraw this leftovers
    function withdrawLeftovers() external onlyOwner {
        require(totalSupply() == 0);
        uint256 leftOverEthBalance = address(this).balance;
        require(leftOverEthBalance > 0);
        (bool success,) = owner().call{value: leftOverEthBalance}("");
        require(success, "Failed");
    }
}
contract Sponsio is Depositable, Structures  {
    using SafeMath for uint256;
    using SafeMath for uint32;
    // Address of reward token contract Merces
    address public MercesAddress;
    // Instanse of reward token contract Merces
    Merces contractMercesInstance;

    // Merces address can be set only once
    function setMercesAddress(address _newMercesAddress) external onlyOwner {
        require(MercesAddress == address(0));
        MercesAddress = _newMercesAddress;
        contractMercesInstance = Merces(MercesAddress);
    }
    // Id of last event (or just total amount of Events)
    // Starts from 1
    uint32 public lastEventId;
    modifier EventExists(uint256 eventId) {
        require(eventId > 0 && eventId <= lastEventId, "Event invalid");
        _;
    }
    // All Events mapping EventId -> Event
    mapping(uint32 => Event) public allEvents;
    // Count of predictions for each event for each address
    // mapping EventId -> (mapping address -> prediction count)
    mapping(uint32 => mapping(address => uint16)) predictionsByAddressCounts;
    
    // All predictions for each event for each address
    // mapping EventId -> (mapping prediction in format Bytes32 -> [Address, Timestamp])
    mapping(uint32 => mapping(bytes32 => Prediction[])) public predictions;

    // Initial price of Event Creation in Sponsio tokens
    uint256 public eventPriceInSio = 1*10**decimals();
    // Initial price of Event Creation in Merces tokens
    uint256 public eventPriceInMerces = (1*10**6)*10**decimals();
    // Initial royalty for Contract creator
    uint8 private royaltyForContract = 5;
    // Total possible win all time
    // Increasing when prediction is made
    // Decreasing when Event is Reverted
    uint256 totalWinAllTime;
    // The maximum length of the event text, can be changed by the contract owner
    uint16 private maximumEventTextLength = 1000;

    /* 
        60*60*24*3
        The time interval before the Events ware last active in which all predictions are invalidated. 
        Cannot exceed maxVotingCloseTime interval and the minVotingCloseTime
        (except for time-limited events. In them, this interval is 0 by default)
    */
    uint32 private timeToVotingClosing = 259200;
    /*
        maxVotingCloseTime is needed so that it is not possible to cancel all event predictions
        60*60*24*20
    */
    uint32 private maxVotingCloseTime = 1728000;
    /*
        minVotingCloseTime is needed to take the time to close the event
        (except for time-limited events. In them, this interval is 0 by default)
        60*60
    */
    uint32 private minVotingCloseTime = 3600;

    constructor() ERC20("Sponsio", "SIO") {}

    receive() external payable {
        _mint(msg.sender, msg.value);
    }

    function createEvent(
        createEventCurrency _createEventCurrency,
        PredictionType _predictionType,
        uint8 _royaltyForCreator,
        uint256 _predictionPrice,
        string memory _eventText,
        uint8 _language,
        uint8 _royaltyOnlyLosers,
        uint32 _endTime,
        uint8 _canBeRestarted
    ) public {
        uint256 eventTextLength = bytes(_eventText).length;
        // Event text cannot be empty
        require(eventTextLength != 0, "Text invalid");
        // Event text cannot be too long
        require(eventTextLength <= maximumEventTextLength, "Text ivalid");
        // Event Prediction Type must be valid Prediction Type
        require(
            _predictionType == PredictionType.IntegerExactMatch || 
            _predictionType == PredictionType.StringExactMatch ||
            _predictionType == PredictionType.DateExactMatch ||
            _predictionType == PredictionType.DateTimeExactMatch,
            "Invalid Type"
        );
        // Event Prediction Price should be greateer then zero
        require(_predictionPrice > 0, "Price invalid");
        // If want to pay in Merces
        if (_createEventCurrency == createEventCurrency.Merces) {
            // If it is contract owner, then burn from Merces owner wallet
            if (msg.sender == owner()){
                require(
                    contractMercesInstance.burnFromSponsio(contractMercesInstance.getOwner(), eventPriceInMerces),
                    "Merces Pay Failed"
                );
            }
            else {
                require(
                    contractMercesInstance.burnFromSponsio(msg.sender, eventPriceInMerces),
                    "Merces Pay Failed"
                );
            }
            updateCostInMerces();
        }
        else {
            // For owner event creation in Sponsio tokens is free
            if (msg.sender != owner()){
                uint256 balance = balanceOf(msg.sender);
                require(balance >= eventPriceInSio, "Insufficient payment amount");
                require(transfer(owner(), eventPriceInSio), "Event creation failed");
            }
            updateeventPriceInSio();
        }
        Event memory eventum;
        // If eventId is overload then just start from 1
        // 65535 is a lot of events, so if 65535 Event will be created the 1 one will be already finished
        // And no need to store it
        lastEventId = lastEventId >= 4294967295 ? 1 : uint32(lastEventId+1);
        eventum.eventId = lastEventId;
        eventum.eventText = bytes(_eventText);
        eventum.predictionType = _predictionType;
        eventum.predictionPrice = _predictionPrice;
        eventum.creator = payable(msg.sender);

        RoyaltySetting memory _royaltySetting;
        // royaltyForCreator can`t be more then 15% !
        _royaltySetting.royaltyForCreator = _royaltyForCreator <= 15 ? _royaltyForCreator : 15;
        _royaltySetting.royaltyForContract = royaltyForContract;
        _royaltySetting.royaltyOnlyLosers = _royaltyOnlyLosers;
        eventum.royaltySetting = _royaltySetting;

        eventum.eventStatus = EventStatus.Active;
        // if _endTime == 0 so Event is endless
        eventum.eventTimeToVotingClosing = _endTime == 0 ? timeToVotingClosing : 0;
        eventum.language = _language;

        TimeSettings memory _timeSettings;
        _timeSettings.endTime = _endTime;
        _timeSettings.canBeRestarted = _canBeRestarted;
        eventum.timeSettings = _timeSettings;
        allEvents[lastEventId] = eventum;
    }
    // change language if it is accidently set wrong
    function changeEventLanguage(uint8 _language, uint32 eventId) external onlyOwner EventExists(eventId) {
        Event storage eventum = allEvents[eventId];
        eventum.language = _language;
    }
    // We can change eventTimeToVotingClosing of any Event to prevent predictions from people who knew the outcome in advance
    function changeEventTimeToVotingClosing(uint32 eventId, uint32 newTimeToVotingClosing) external onlyOwner EventExists(eventId) {
        require(newTimeToVotingClosing>=minVotingCloseTime || newTimeToVotingClosing<=maxVotingCloseTime);
        Event storage eventum = allEvents[eventId];
        require(eventum.eventStatus != EventStatus.NotExists);
        eventum.eventTimeToVotingClosing = newTimeToVotingClosing;
    }
    /*
        We can finish Event by providing the correct answer
        It can be done only once. We can Finish only Active, Freezed and OutOfTime Events.
        Once Event is Finished all rewards are distributed and Event is no more available to be changed
    */
    function finishEventSuccess(uint32 eventId, bytes32 eventResult) external onlyOwner EventExists(eventId) {
        Event storage eventum = allEvents[eventId];
        require(
            eventum.eventStatus == EventStatus.Active ||
            eventum.eventStatus == EventStatus.Freezed ||
            eventum.eventStatus == EventStatus.OutOfTime
        );
        if (eventum.eventStatus == EventStatus.Active){
            eventum.timeSettings.lastActiveTime = uint32(block.timestamp);
        }
        // Distribute rewards here
        require(sponsioPoenalis(eventum, eventResult));
        eventum.eventStatus = EventStatus.Finished;
    }
    // Restart Event that is out of time and can be restarted
    function restartEndedEvent(uint32 _endTime, uint32 eventId) external onlyOwner EventExists(eventId) {
        Event storage eventum = allEvents[eventId];
        require(eventum.eventStatus == EventStatus.OutOfTime && eventum.timeSettings.canBeRestarted > 0);
        eventum.timeSettings.endTime = _endTime;
        eventum.eventStatus = EventStatus.Active;
    }
    // Freeze, Unfreeze or Revert event
    function changeEventStatus(EventStatus newStatus, uint32 eventId) external onlyOwner EventExists(eventId) returns (bool){
        require(
            newStatus == EventStatus.Active ||
            newStatus == EventStatus.Freezed ||
            newStatus == EventStatus.Reverted
        );
        Event storage eventum = allEvents[eventId];
        require(
            (
                eventum.eventStatus == EventStatus.Active &&
                newStatus != EventStatus.Reverted
            ) ||
            eventum.eventStatus == EventStatus.Freezed ||
            (
                eventum.eventStatus == EventStatus.OutOfTime &&
                newStatus == EventStatus.Reverted
            )
        );
        if (newStatus == EventStatus.Reverted) {
            totalWinAllTime = totalWinAllTime.sub(eventum.prize);
        }
        else {
            if (eventum.timeSettings.endTime > 0 && uint32(block.timestamp) > eventum.timeSettings.endTime){
                eventum.timeSettings.lastActiveTime = eventum.timeSettings.endTime;
                eventum.eventStatus = EventStatus.OutOfTime;
                return false;
            }
        }
        
        eventum.timeSettings.lastActiveTime = uint32(block.timestamp);
        eventum.eventStatus = EventStatus(newStatus);
        return true;
    }
    // Withdraw all money from reverted event
    function withdrawFromReverted(uint32 eventId) external EventExists(eventId) {
        Event storage eventum = allEvents[eventId];
        require(eventum.eventStatus == EventStatus.Reverted, "Event invalid");
        uint256 votesCount = predictionsByAddressCounts[eventId][msg.sender];
        require(votesCount > 0, "Nothing to withdraw");
        uint256 predictionPriceDiv100 = eventum.predictionPrice.div(100);
        uint256 amountToWithdraw = votesCount * (
            eventum.royaltySetting.royaltyOnlyLosers > 0 ? eventum.predictionPrice : (
                eventum.predictionPrice.sub(
                    predictionPriceDiv100.mul(
                        eventum.royaltySetting.royaltyForContract+eventum.royaltySetting.royaltyForCreator
                    )
                )
            )
            
        );
        predictionsByAddressCounts[eventId][msg.sender] = 0;
        _burn(address(this), amountToWithdraw);
        (bool success,) = msg.sender.call{value: amountToWithdraw}("");
        require(success, "Transfer failed");
    }
    // Deposit and make a prediction at the same time
    function depositAndMakePrediction(uint32 eventId, bytes32 predictionString) external payable {
        require(msg.value > 0, "Amount invalid");
        _mint(msg.sender, msg.value);
        require(makePrediction(eventId, predictionString));
    }