// File: MainContract.sol



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

import "./SharedStruct.sol";
import "./SharedState.sol";
import "./Pool.sol";
import "./TicketOptions.sol";
import "./Ticket.sol";

contract CipherHub is SharedState {
    using SharedStruct for SharedStruct.TicketOption;
    using SharedStruct for SharedStruct.Pool;

    Pool private poolContract;
    TicketOption private ticketOptionContract;
    Ticket private ticketContract;

    mapping(address => uint) public creatorPayment;
    bool private emergencyStop;
    uint private latestPoolCreatedAt;

    modifier isNotStopped() {
        require(!emergencyStop, "There has been an emergency stop, please stand by for further details.");
        _;
    }

    function changePoolContract(address _poolAddress) public hasAuthority {
        poolContract = Pool(_poolAddress);
    }

    function changeTicketContract(address _ticketAddress) public hasAuthority {
        ticketContract = Ticket(_ticketAddress);
    }

    function changeTicketOptionContract(address _ticketOptionAddress) public hasAuthority {
        ticketOptionContract = TicketOption(_ticketOptionAddress);
    }

    function setEmergencyStop(bool _emergencyStop) public hasAuthority {
        emergencyStop = _emergencyStop;
    }

    function getEmergencyStop() public view returns (bool) {
        return emergencyStop;
    }

    function createTicketOption(uint baseTickets, uint additionalTickets) public hasAuthority {
        ticketOptionContract.createTicketOption(baseTickets, additionalTickets);
    }

    function changeUsabilityTicketOption(uint _ticketOptionIdx, bool newVal) public hasAuthority {
        ticketOptionContract.switchUsabilityTicketOption(_ticketOptionIdx, newVal);
    }

    function createPool(uint8 multiplier) public isNotStopped payable {
        require(msg.value >= baseCreationFee * multiplier, "You did not send enough ETH to create a pool.");
        uint256 joiningEnd = block.timestamp + baseJoiningPeriod;
        uint256 activationEnd = block.timestamp + baseJoiningPeriod + baseActivationPeriod;
        poolContract.createPool(msg.sender, multiplier, msg.value, joiningEnd, activationEnd);
        latestPoolCreatedAt = block.timestamp;
    }

    function purchaseTicket(uint poolIdx, uint8 ticketOptionIdx, string memory encryptedRes, bytes32 hashedRes) public payable {
        SharedStruct.Pool memory chosenpool = poolContract.getPool(poolIdx);
        require(block.timestamp < chosenpool.joiningPeriodEnd, "Joining period has ended for this pool");
        SharedStruct.TicketOption memory ticketOpt = ticketOptionContract.getTicketOptionDetails(ticketOptionIdx);
        require(msg.value >= ticketOpt.baseTickets * chosenpool.multiplier * baseJoiningFee, "You did not send enough ETH.");
        uint totalNumbers = ticketOpt.baseTickets + ticketOpt.additionalTickets;
        uint addToPool = (msg.value / 100 * (100 - poolCreatorFeePercentage - cipherHubFeePercentage));
        ticketContract.purchaseTicket(msg.sender, poolIdx, totalNumbers, encryptedRes, hashedRes);
        poolContract.addPurchaseData(poolIdx, totalNumbers, addToPool);
        creatorPayment[chosenpool.creator] += msg.value / 100 * poolCreatorFeePercentage;
        addToManagementPool(msg.value / 100 * cipherHubFeePercentage);
    }

    function activateTicket(uint poolIdx, uint ticketIdx, uint8[] memory salt, uint32[] memory data) public {
        require(poolContract.canActivate(poolIdx), "You're not in the activation period.");
        SharedStruct.Ticket memory ticket = ticketContract.getTicketByIdx(msg.sender, poolIdx, ticketIdx);
        uint32[] memory dataBasedOnTicketCount = new uint32[](ticket.numberCount);
        for (uint i = 0; i < ticket.numberCount; i++) {
            dataBasedOnTicketCount[i] = data[i];
        }
        ticketContract.activateTicket(msg.sender, poolIdx, ticketIdx, salt, data, dataBasedOnTicketCount);
        poolContract.addNumbers(poolIdx, dataBasedOnTicketCount);
    }

    function claimWinningEth(uint poolIdx) public nonReentrant {
        poolContract.checkWinner(poolIdx, claimingPeriodEnd);
        SharedStruct.Pool memory pool = poolContract.getPool(poolIdx);
        require(ticketContract.validateWinner(msg.sender, poolIdx, pool.lowestUniqueNumber), "You do not have the winning number");
        poolContract.setWinner(msg.sender, poolIdx);
        (bool success,) = payable(msg.sender).call{value: pool.amountWinnable}("");
        require(success, "Claiming failed");
        poolContract.setAsClaimed(poolIdx);
    }

    function claimAsPoolCreator() public nonReentrant {
        require(creatorPayment[msg.sender] > 0, "There is nothing to claim");
        (bool success,) = payable(msg.sender).call{value: creatorPayment[msg.sender]}("");
        require(success, "Claiming failed");
        creatorPayment[msg.sender] = 0;
    }

    function claimAsCipherHub(uint poolIdx) public hasAuthority nonReentrant {
        SharedStruct.Pool memory pool = poolContract.getPool(poolIdx);
        require(!pool.claimed, "This pool has already been claimed.");
        require(block.timestamp > (pool.activationPeriodEnd + claimingPeriodEnd), "Claiming period has not ended yet.");
        (bool success,) = payable(msg.sender).call{value: pool.amountWinnable}("");
        require(success, "Claiming failed");
        poolContract.setAsClaimed(poolIdx);
    }

    function claimLastResort() public hasAuthority nonReentrant {
        uint halfYear = 3 * 30 days;
        require(block.timestamp > (latestPoolCreatedAt + halfYear), "There are still pools being created.");
        (bool success,) = payable(msg.sender).call{value: address(this).balance}("");
        require(success, "Claiming failed");
    }
}
// File: Ownership.sol



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


contract Ownership {
    uint public managementPool;
    mapping(address => bool) public owners;
    bool private locked;

    modifier hasAuthority() {
        require(verifyOwner(), "You are not authorized to do this action.");
        _;
    }

    modifier nonReentrant() {
        require(!locked, "No re-entrancy.");
        locked = true;
        _;
        locked = false;
    }

    constructor() {
        owners[msg.sender] = true;
    }

    function verifyOwner() public view returns (bool) {
        return owners[msg.sender];
    }

    function addOwner(address _owner) public hasAuthority {
        require(owners[_owner] == false, "This address already belongs to an owner.");
        owners[_owner] = true;
    }

    function removeOwner(address _owner) public hasAuthority {
        require(owners[_owner] == true, "There is no owner with this address.");
        owners[_owner] = false;
    }

    function claimManagementPool() public hasAuthority nonReentrant {
        require(managementPool > 0, "There are no funds in the pool");
        payable(msg.sender).transfer(managementPool);
        managementPool = 0;
    }

    function addToManagementPool(uint amount) internal {
        managementPool += amount;
    }

    receive() external payable {
        addToManagementPool(msg.value);
    }
}

// File: Pool.sol



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

import "./SharedStruct.sol";

contract Pool {
    using SharedStruct for SharedStruct.Pool;

    address public mainContract;

    SharedStruct.Pool[] public allPools;
    mapping(uint => uint[]) public numberDumpPerPool;
    mapping(uint => mapping(uint => uint)) private numberCount;
    mapping(uint => uint[]) private uniqueNumbers;

    modifier isMainContract() {
        require(msg.sender == mainContract, "You called this contract directly.");
        _;
    }

    modifier poolExists(uint _poolId) {
        require(isPool(_poolId), "There is no pool with this index.");
        _;
    }

    constructor(address _mainContract) {
        mainContract = _mainContract;
    }

    function isPool(uint poolIdx) public view returns (bool) {
        return poolIdx < allPools.length;
    }

    function getPool(uint poolIdx) public view poolExists(poolIdx) returns (SharedStruct.Pool memory) {
        return allPools[poolIdx];
    }

    function canPurchase(uint poolIdx) public view poolExists(poolIdx) returns (bool) {
        SharedStruct.Pool memory pool = allPools[poolIdx];
        return block.timestamp < pool.joiningPeriodEnd;
    }

    function canActivate(uint poolIdx) public view poolExists(poolIdx) returns (bool) {
        SharedStruct.Pool memory pool = allPools[poolIdx];
        return block.timestamp > pool.joiningPeriodEnd && block.timestamp < pool.activationPeriodEnd;
    }

    function createPool(address creator, uint8 multiplier, uint winnableAmount, uint joiningEnd, uint activationEnd) public isMainContract {
        SharedStruct.Pool memory newPool;
        newPool.idx = allPools.length;
        newPool.amountWinnable = winnableAmount;
        newPool.winner = address(0);
        newPool.lowestUniqueNumber = 1_000_000;
        newPool.claimed = false;
        newPool.joiningPeriodEnd = joiningEnd;
        newPool.activationPeriodEnd = activationEnd;
        newPool.creator = creator;
        newPool.ticketsSold = 0;
        newPool.multiplier = multiplier;
        allPools.push(newPool);
    }

    function getAllPools() public view returns (SharedStruct.Pool[] memory) {
        return allPools;
    }

    function getTicketCount(uint poolIdx) public view poolExists(poolIdx) returns (uint) {
        return allPools[poolIdx].ticketsSold;
    }

    function addNumbers(uint poolIdx, uint32[] memory data) public isMainContract poolExists(poolIdx) {
        for (uint8 idx = 0; idx < data.length; idx++) {
            uint32 currNumber = data[idx];
            if (currNumber > 0 && currNumber < 1_000_000) {
                numberDumpPerPool[poolIdx].push(currNumber);
                numberCount[poolIdx][currNumber]++;
                if (numberCount[poolIdx][currNumber] == 1) {
                    uniqueNumbers[poolIdx].push(currNumber);
                }
            }
        }
    }

    function getNumberDumpForPool(uint256 poolIdx) public view poolExists(poolIdx) returns (uint256[] memory) {
        SharedStruct.Pool memory pool = allPools[poolIdx];
        require(block.timestamp > pool.joiningPeriodEnd, "There is no activation of tickets yet");
        return numberDumpPerPool[poolIdx];
    }

    function setLowestUniqueNumber(uint256 poolIdx) public poolExists(poolIdx) {
        SharedStruct.Pool storage pool = allPools[poolIdx];
        require(block.timestamp > pool.activationPeriodEnd, "Activation period has not ended yet.");
        require(pool.lowestUniqueNumber == 1_000_000, "Lowest number has already been calculated.");
        uint256 lowestNumber = pool.lowestUniqueNumber;
        for (uint256 idx = 0; idx < uniqueNumbers[poolIdx].length; idx++) {
            if (numberCount[poolIdx][uniqueNumbers[poolIdx][idx]] == 1 && uniqueNumbers[poolIdx][idx] < lowestNumber) {
                lowestNumber = uniqueNumbers[poolIdx][idx];
            }
        }
        pool.lowestUniqueNumber = lowestNumber;
    }

    function getLowestUniqueNumber(uint poolIdx) public view poolExists(poolIdx) returns (uint) {
        SharedStruct.Pool memory pool = allPools[poolIdx];
        return pool.lowestUniqueNumber;
    }

    function addPurchaseData(uint poolIdx, uint totalNumbers, uint addToPool) public isMainContract poolExists(poolIdx) {
        SharedStruct.Pool storage pool = allPools[poolIdx];
        pool.ticketsSold += totalNumbers;
        pool.amountWinnable += addToPool;
    }

    function setWinner(address winner, uint poolIdx) public isMainContract poolExists(poolIdx) {
        SharedStruct.Pool storage pool = allPools[poolIdx];
        pool.winner = winner;
    }

    function setAsClaimed(uint poolIdx) public isMainContract poolExists(poolIdx) {
        SharedStruct.Pool storage pool = allPools[poolIdx];
        pool.claimed = true;
    }

    function checkWinner(uint poolIdx, uint claimingPeriodEnd) public view isMainContract poolExists(poolIdx) {
        SharedStruct.Pool memory pool = allPools[poolIdx];
        require(block.timestamp > pool.activationPeriodEnd, "Activation period has not ended yet.");
        require(block.timestamp < (pool.activationPeriodEnd + claimingPeriodEnd), "Claiming window has ended");
        require(!pool.claimed, "This pool has already been claimed.");
        require(pool.lowestUniqueNumber < 1_000_000, "There is no winner.");
    }
}
// File: SharedState.sol



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

import "./Ownership.sol";
import "./SharedStruct.sol";

contract SharedState is Ownership {
    using SharedStruct for SharedStruct.FeeOverview;

    uint internal baseCreationFee = 0.1 ether;
    uint internal baseJoiningFee = 0.001 ether;
    uint internal baseJoiningPeriod = 2 minutes;
    uint internal baseActivationPeriod = 2 minutes;
    uint internal claimingPeriodEnd = 2 minutes;

    uint internal cipherHubFeePercentage = 4;
    uint internal poolCreatorFeePercentage = 6;

    function updateBaseCreationFee(uint _creationAmount) public hasAuthority {
        baseCreationFee = _creationAmount;
    }

    function updateBaseJoiningFee(uint _joiningFee) public hasAuthority {
        baseJoiningFee = _joiningFee;
    }

    function updateBaseJoiningPeriod(uint _joiningPeriod) public hasAuthority {
        baseJoiningPeriod = _joiningPeriod * (1 minutes);
    }

    function updateBaseActionPeriod(uint _activationPeriod) public hasAuthority {
        baseActivationPeriod = _activationPeriod * (1 minutes);
    }

    function updateClaimingPeriodEnd(uint _claimingPeriodEnd) public hasAuthority {
        claimingPeriodEnd = _claimingPeriodEnd * (1 minutes);
    }

    function updateCipherhubFeePercentage(uint _cipherHubFeePercentage) public hasAuthority {
        cipherHubFeePercentage = _cipherHubFeePercentage;
    }

    function updatePoolCreatorFee(uint _poolCreatorFeePercentage) public hasAuthority {
        poolCreatorFeePercentage = _poolCreatorFeePercentage;
    }

    function getFeeOverview() public view returns (SharedStruct.FeeOverview memory){
        SharedStruct.FeeOverview memory fees;
        fees.claimingPeriodEnd = claimingPeriodEnd;
        fees.activationPeriod = baseActivationPeriod;
        fees.joiningFee = baseJoiningFee;
        fees.creationFee = baseCreationFee;
        fees.joiningPeriod = baseJoiningPeriod;
        return fees;
    }
}

// File: SharedStruct.sol



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

library SharedStruct {
    struct FeeOverview {
        uint creationFee;
        uint joiningFee;
        uint joiningPeriod;
        uint activationPeriod;
        uint claimingPeriodEnd;
    }

    struct Ticket {
        uint idx;
        uint numberCount;
        string encryptedData;
        bytes32 hashedData;
        bool activated;
        uint32[] unlockedValues;
    }

    struct TicketOption {
        uint idx;
        uint baseTickets;
        uint additionalTickets;
        bool active;
    }

    struct Pool {
        uint idx;
        uint joiningPeriodEnd;
        uint activationPeriodEnd;
        uint amountWinnable;
        uint lowestUniqueNumber;
        uint ticketsSold;
        uint8 multiplier;
        address creator;
        address winner;
        bool claimed;
    }
}
// File: Ticket.sol



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

import "./SharedStruct.sol";

contract Ticket {
    using SharedStruct for SharedStruct.Ticket;

    mapping(address => mapping(uint => SharedStruct.Ticket[])) private addressTickets;
    address public mainContract;

    modifier isMainContract() {
        require(msg.sender == mainContract, "You called this contract directly.");
        _;
    }

    modifier ticketExists(address buyer, uint poolIdx, uint ticketIdx) {
        require(ticketIdx < addressTickets[buyer][poolIdx].length, "There is no such ticket");
        _;
    }

    constructor(address _mainContract) {
        mainContract = _mainContract;
    }

    function getTicketByIdx(address buyer, uint poolIdx, uint ticketIdx) public view ticketExists(buyer, poolIdx, ticketIdx) returns (SharedStruct.Ticket memory) {
        return addressTickets[buyer][poolIdx][ticketIdx];
    }

    function getNewTicket(uint poolIdx, uint ticketIdx) public view ticketExists(msg.sender, poolIdx, ticketIdx) returns (SharedStruct.Ticket memory) {
        return addressTickets[msg.sender][poolIdx][ticketIdx];
    }

    function getMyTickets(uint poolIdx) public view returns (SharedStruct.Ticket[] memory){
        return addressTickets[msg.sender][poolIdx];
    }

    function getMyTicketCount(uint poolIdx) public view returns (uint) {
        return addressTickets[msg.sender][poolIdx].length;
    }

    function purchaseTicket(address buyer, uint poolIdx, uint _numberCount, string memory encryptedRes, bytes32 hashedRes) public isMainContract {
        SharedStruct.Ticket memory newTicket;
        newTicket.idx = addressTickets[buyer][poolIdx].length;
        newTicket.numberCount = _numberCount;
        newTicket.encryptedData = encryptedRes;
        newTicket.hashedData = hashedRes;
        newTicket.activated = false;
        addressTickets[buyer][poolIdx].push(newTicket);
    }

    function activateTicket(address buyer, uint poolIdx, uint ticketIdx, uint8[] memory salt, uint32[] memory data, uint32[] memory dataBasedOnTicketCount) public isMainContract ticketExists(buyer, poolIdx, ticketIdx) {
        SharedStruct.Ticket storage ticketToActivate = addressTickets[buyer][poolIdx][ticketIdx];
        require(!ticketToActivate.activated, "This ticket has already been activated.");
        require(ticketToActivate.hashedData == keccak256(abi.encodePacked(salt, data)), "The data does not match.");
        ticketToActivate.activated = true;
        ticketToActivate.unlockedValues = dataBasedOnTicketCount;
    }

    function validateWinner(address buyer, uint poolIdx, uint lowestNumber) public view isMainContract returns (bool){
        bool isWinner;
        SharedStruct.Ticket[] memory ticketLst = addressTickets[buyer][poolIdx];
        for (uint idx = 0; idx < ticketLst.length; idx++) {
            SharedStruct.Ticket memory ticket = ticketLst[idx];
            if (ticket.activated) {
                for (uint numberIdx = 0; numberIdx < ticket.unlockedValues.length; numberIdx++) {
                    if (ticket.unlockedValues[numberIdx] == lowestNumber) {
                        isWinner = true;
                        break;
                    }
                }
            }
        }
        return isWinner;
    }
}

// File: TicketOptions.sol



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

import "./SharedStruct.sol";

contract TicketOption {
    using SharedStruct for SharedStruct.TicketOption;

    SharedStruct.TicketOption[] public allTicketOptions;

    address public mainContract;

    modifier ticketOptionExists(uint _ticketOptionIdx) {
        require(isTicketOption(_ticketOptionIdx), "There is no ticket with this index.");
        _;
    }

    modifier isMainContract() {
        require(msg.sender == mainContract, "You called this contract directly.");
        _;
    }

    constructor(address _mainContract) {
        mainContract = _mainContract;
    }

    function createTicketOption(uint baseTickets, uint additionalTickets) public isMainContract {
        SharedStruct.TicketOption memory ticketOpt;
        ticketOpt.idx = allTicketOptions.length;
        ticketOpt.baseTickets = baseTickets;
        ticketOpt.additionalTickets = additionalTickets;
        ticketOpt.active = true;
        allTicketOptions.push(ticketOpt);
    }

    function isTicketOption(uint ticketIdx) private view returns (bool) {
        return ticketIdx < allTicketOptions.length;
    }

    function isActiveTicketOption(uint ticketIdx) public view ticketOptionExists(ticketIdx) returns (bool) {
        SharedStruct.TicketOption memory ticketOpt = allTicketOptions[ticketIdx];
        return ticketOpt.active;
    }

    function getTicketOptionDetails(uint ticketIdx) public view ticketOptionExists(ticketIdx) returns (SharedStruct.TicketOption memory) {
        require(isActiveTicketOption(ticketIdx), "This ticket is currently unavailable");
        return allTicketOptions[ticketIdx];
    }

    function getAllTicketOptions() public view returns (SharedStruct.TicketOption[] memory) {
        return allTicketOptions;
    }

    function switchUsabilityTicketOption(uint _ticketOptionIdx, bool newVal) public isMainContract ticketOptionExists(_ticketOptionIdx) {
        SharedStruct.TicketOption storage foundTicketOption = allTicketOptions[_ticketOptionIdx];
        foundTicketOption.active = newVal;
    }
}