// Contract: Sponsio
// Target function: function finishEventSuccess(uint32 eventId, bytes32 eventResult) external onlyOwner EventExists(eventId)
from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * has been transferred to `to`.
     * - when `from` is zero, `amount` tokens have been minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens have been burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
}

// Interface of reward token Merces
interface Merces {
    // Function that transact amount Merces to receiver address from Merces contract address.
    // It is called only when prediction is made
    function getRewardFromSponsio(address reciever) external returns (bool);
    // Function that burn amount Merces from burner address. Needed to create new Events
    function burnFromSponsio(address burner, uint256 amount) external returns (bool);
    // Total Supply of Merces token. Needed for calculation of event price and reward amount 
    function totalSupply() external view returns (uint256);
    // Owner of Merces contract. Needed to pay for the Event creation
    function getOwner() external view returns (address);
}
// Structures that is used in Sponsio contract
contract Structures {
    // Structure to keep track of when each address made a prediction.
    struct Prediction{
        // Adress that made a prediction
        address predictionAddress;
        // Prediction Timestamp
        uint32 timestamp;
    }
    // The type of currency that the event creator will pay to create it
    enum createEventCurrency {
        Sponsio,
        Merces
    }
    // The type of prediction expected in the event prediction.
    // It is needed only in order to immediately clarify what is expected in the prediction when creating an event.
    // In fact, all predictions must be in Bytes32 format converted from a string.
    enum PredictionType {
        // Expected Integer as an answer e.g.
        // If correct answer for Event is 1 then prediction should be 
        // 0x3100000000000000000000000000000000000000000000000000000000000000
        // note that the prediction 
        // 0x1000000000000000000000000000000000000000000000000000000000000000
        // will be INCORRECT !!!
        IntegerExactMatch,
        // Expected any string that can be converted it Bytes32 format. 
        // Unless otherwise stated in the text of the event, the response must always be in LOWERCASE !!!
        // E.g. if correct answer for Event is 'correct answer' then prediction should be 
        // 0x636f727265637420616e73776572000000000000000000000000000000000000
        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;
    }