contract FabFourGearSullivanTradingCards is ERC1155, Ownable, ERC1155Supply, PriceConsumerV3 {

    constructor() ERC1155("bafybeid762h2v3olaeydlkzleh7sdbslpnibom5qbksnp6naayvflpwryq") {}

    // ---------------------------------------------------------------------------------------------
    // CONSTANT VARIABLES
    // ---------------------------------------------------------------------------------------------

    uint256 public constant EXCLUSIVE_NEEDED = 4; 
    uint256 public constant ULTIMATE_NEEDED = 16; 
    uint256 public constant TOTAL_SETS = 64; 
    uint256 public constant TOTAL_IDS = 80;    
    uint256 public constant AVAILABLE_SUPPLY_FOR_SET = 128;
    uint256 public constant USD_AMOUNT = 400;  
    uint256 public constant SUPPLY_AVAILABLE_FOR_SALE = 9792; 
    uint256 public constant PRICE = 0.01 ether;      
    uint256 public constant PRESALE_PRICE = 0.0075 ether;       
    uint256 public constant MAX_ULTIMATE_PRIZE = 0.64 ether;   
    address public constant WITHDRAWAL_ADDRESS = 0xDae5E244C98F1a9b200a2c72D529DAa12BDD53Dc;
    address public constant GENESIS_VIP_PASS_CONTRACT_ADDRESS = 0x42412d4F9007102D64CC6B56ed4439A6D31F4Ad9;

    // ---------------------------------------------------------------------------------------------
    // EVENTS
    // ---------------------------------------------------------------------------------------------

    event Reveal (string previousURI, string newURI);
    event MicrophoneClaimed (address claimer);    
    event UltimatePrize (address claimer, uint prize, bool first);    
    event ExclusiveCard (address claimer, uint exclusiveId);

    // ---------------------------------------------------------------------------------------------
    // SALE MANAGING VARIABLES
    // ---------------------------------------------------------------------------------------------

    bool public saleStatus;
    bool public presaleStatus;
    bool public firstUltimatePrize;

    // ---------------------------------------------------------------------------------------------
    // MAPPINGS
    // ---------------------------------------------------------------------------------------------

    mapping(uint => uint[]) public _combinations;
    mapping(address => uint) private _totalSpent;
    mapping(address => bool) private _microphoneClaimed;       
    mapping(address => mapping(uint => bool)) private _exclusiveClaimed;

    // ---------------------------------------------------------------------------------------------
    // CHECK FUNCTIONS
    // ---------------------------------------------------------------------------------------------

    function checkIds(uint[] calldata ids) private pure returns(bool){
        for(uint i; i < ids.length; i++) {
            if(ids[i] >= TOTAL_SETS){
                return false;
            }
        }
        return true;
    }

    function checkMicrophone(address wallet) private view returns(bool) {
        uint checkCounter;
        for(uint i = TOTAL_SETS; i < TOTAL_IDS; i++) {
            if(balanceOf(wallet, i) > 0){
                checkCounter++;
            }
            if(checkCounter >= 4){
                return true;
            }
        }
        return false;
    }

    function checkSetAvailability(uint[] calldata ids, uint[] calldata amounts) private view returns(bool){
        for(uint i; i < ids.length; i++) {
            if(totalMintedById(ids[i]) + amounts[i] > AVAILABLE_SUPPLY_FOR_SET){
                return false;
            }
        }
        return true;
    }

    function checkUltimatePublic() private view returns(bool) {
        for(uint i = TOTAL_SETS; i < TOTAL_IDS; i++) {
            if(balanceOf(msg.sender, i) == 0) {
                return false;
            }
        } 
        return true;
    }

    function checkExclusivePublic(uint id) public view returns(bool) {
        for(uint i; i < 4; i++) {
            uint checkId = _combinations[id][i];
            if(balanceOf(msg.sender, checkId) == 0) {
                return false;
            }
        } 
        return true;
    }

   function checkMicrophonePublic(address claimer) public view returns(bool) {
        uint checkCounter;
        for(uint i = TOTAL_SETS; i < TOTAL_IDS; i++) {
            if(balanceOf(claimer, i) > 0){
                checkCounter++;
            }
            if(checkCounter >= 4){
                return true;
            }
        }
        return false;
    }

    function checkSetAvailabilityPublic(uint amount, uint id) public view returns(bool){
        if(totalMintedById(id) + amount > AVAILABLE_SUPPLY_FOR_SET){
            return false;
        }else{
            return true;
        }
    }

    // ---------------------------------------------------------------------------------------------
    // MANAGING FUNCTIONS
    // ---------------------------------------------------------------------------------------------

    function flipSaleStatus() external onlyOwner {
        saleStatus = !saleStatus;
    }

    function flipPresaleStatus() external onlyOwner {
        presaleStatus = !presaleStatus;
    }

    function addCombinations(uint[TOTAL_SETS] calldata combinations) external onlyOwner {
        uint idCounter = TOTAL_SETS;
        for(uint i; i < TOTAL_SETS; i += 4) {
            uint[] memory combination = new uint[](4);
            for(uint q; q < 4; q++){
                combination[q] = combinations[i+q];
            }
            _combinations[idCounter++] = combination;
        }
    }    

    function revealURI(string calldata newURI) external onlyOwner {
        string memory previousURI = _uri;
        _setURI(newURI);        
        emit Reveal (previousURI, _uri);
    }

    // ---------------------------------------------------------------------------------------------
    // MINTING FUNCTIONS
    // ---------------------------------------------------------------------------------------------

    function mintCardsOnPresale(uint256[] calldata ids, uint256[] calldata amounts) external payable {
        require(totalMinted() < SUPPLY_AVAILABLE_FOR_SALE, "Not allowed!");
        require(IERC721(GENESIS_VIP_PASS_CONTRACT_ADDRESS).balanceOf(msg.sender) > 0, "Not allowed!");   
        require(checkSetAvailability(ids, amounts), "Insufficient availability!");        
        require(presaleStatus, "Presale not active!");         
        require(checkIds(ids), "Incorrect ID!");
        uint priceMultiplier;
        for(uint i; i < amounts.length; i++) {
            priceMultiplier += amounts[i];
        }
        require(msg.value == priceMultiplier * PRESALE_PRICE, "Incorrect value!");
        _mintBatch(msg.sender, ids, amounts, "");
        if(_totalSpent[msg.sender] + msg.value <= MAX_ULTIMATE_PRIZE){
            _totalSpent[msg.sender] += msg.value;
        }
    }

    function mintCardsOnSale(uint256[] calldata ids, uint256[] calldata amounts) external payable {
        require(totalMinted() < SUPPLY_AVAILABLE_FOR_SALE, "Not allowed!");
        require(checkSetAvailability(ids, amounts), "Insufficient availability!");               
        require(checkIds(ids), "Incorrect ID!");        
        require(saleStatus, "Sale not active!");
        uint priceMultiplier;
        for(uint i; i < amounts.length; i++) {
            priceMultiplier += amounts[i];
        }
        require(msg.value == priceMultiplier * PRICE, "Incorrect value!");
        _mintBatch(msg.sender, ids, amounts, "");  
        if(_totalSpent[msg.sender] + msg.value <= MAX_ULTIMATE_PRIZE){
            _totalSpent[msg.sender] += msg.value;
        }
    }

    function giveaway(address to, uint[] calldata ids, uint[] calldata amounts) external onlyOwner {
        require(checkSetAvailability(ids, amounts), "Insufficient availability!"); 
        require(checkIds(ids), "Incorrect ID!");
        _mintBatch(to, ids, amounts, "");
    }

    // ---------------------------------------------------------------------------------------------
    // CLAIMING FUNCTIONS
    // ---------------------------------------------------------------------------------------------

    function claimExclusive(uint id) external {
        require(!_exclusiveClaimed[msg.sender][id], "Not allowed!");
        uint256[] memory burnAmounts = new uint256[](4);
        for(uint i; i < 4; i++) {
            burnAmounts[i] = 1;
        } 
        _burnBatch(msg.sender, _combinations[id], burnAmounts);
        _mint(msg.sender, id, 1, "");
        _exclusiveClaimed[msg.sender][id] = !_exclusiveClaimed[msg.sender][id];
        emit ExclusiveCard (msg.sender, id);
    }

    function claimMicrophone() external {
        require(checkMicrophone(msg.sender), "Not allowed!");    
        require(!_microphoneClaimed[msg.sender], "Not allowed!");
        _mint(msg.sender, TOTAL_IDS, 1, "");
        _microphoneClaimed[msg.sender] = !_microphoneClaimed[msg.sender];
        emit MicrophoneClaimed (msg.sender);
    }    

    function claimUltimate() external {
        uint256[] memory burnAmounts = new uint256[](ULTIMATE_NEEDED);
        uint256[] memory burnIds = new uint256[](ULTIMATE_NEEDED);
        uint idCounter = TOTAL_SETS;
        for(uint i; i < ULTIMATE_NEEDED; i++) {
            burnAmounts[i] = 1;
            burnIds[i] = idCounter++;
        } 
        _burnBatch(msg.sender, burnIds, burnAmounts);
        uint prize;
        if(!firstUltimatePrize){
            prize = _totalSpent[msg.sender];
            payable(msg.sender).transfer(prize);
            emit UltimatePrize (msg.sender, prize, true);
        }else{
            prize = (uint(getLatestPrice()) * USD_AMOUNT) / 100;
            payable(msg.sender).transfer(prize);
            emit UltimatePrize (msg.sender, prize, false);
        }
    }

    function withdraw(uint amount) external {
        require(msg.sender == WITHDRAWAL_ADDRESS, "Not allowed!");
        payable(msg.sender).transfer(amount);
    }

    // The following functions are overrides required by Solidity.

    function _beforeTokenTransfer(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
        internal
        override(ERC1155, ERC1155Supply)
    {
        super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
    }
}
function claimExclusive(uint id) external {
    require(!_exclusiveClaimed[msg.sender][id], "Not allowed!");
    uint256[] memory burnAmounts = new uint256[](4);
    for(uint i; i < 4; i++) {
        burnAmounts[i] = 1;
    } 
    _burnBatch(msg.sender, _combinations[id], burnAmounts);
    _mint(msg.sender, id, 1, "");
    _exclusiveClaimed[msg.sender][id] = !_exclusiveClaimed[msg.sender][id];
    emit ExclusiveCard (msg.sender, id);
}

// ---------------------------------------------------------------------------------------------
// CLAIMING FUNCTIONS
// ---------------------------------------------------------------------------------------------