contract INCOOOM is ERC721, Ownable {
    using Strings for uint256;

    /* ======== EVENTS ======== */

    event DealPreFlop(address _recipient, uint _amount);
    event Deal(address _recipient, uint _amount);


    /* ======== STRUCTS ======== */

    struct CardInfo {
        string suit;
        string value;
        bool joker;
        uint deck;
        uint cardNumber;
        TIER tier;
    }

    struct Deck {
        TIER tier;
        uint tiersRemaining;
    }

    struct PreSale {
        uint _blockStart;
        uint _blockEnd;
    }

    /* ======== ENUM ======== */

    enum TIER {COMMON, GOLD, PSYCHEDELIC}

    /* ======== STATE VARIABLES ======== */

    uint256[54] private numberArr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
    40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53];

    PreSale public preSale;

    address payable public treasury;
    address payable public fund;
    address public helper;
    string private _baseUri = "ipfs://QmaGRDin2YYRj3zbPE23p6YPeTpMn7YcFqtVDD6aY8Zoo3/";
    
    uint private constant numberOfDecks = 54;
    uint public cardsLeftInDeck;
    uint public currentDeck;
    uint public totalSupply;
    uint private immutable preSaleLength;
    uint256 private constant PRICE_PER_CARD = 99000000000000000 wei;
    uint256 private constant PRICE_PER_CARD_PRESALE = 33000000000000000 wei;

    bool public preSaleActive;
    bool public saleActive;

    /* ======== MAPPINGS ======== */

    mapping(address => mapping(uint => uint)) public cardsPerDeckPurchased;
    mapping(uint => CardInfo) private cardInfo;
    mapping(TIER => uint) private tiersRemaining;
    mapping(TIER => uint) private preSaleTiersRemaining;
    mapping(uint => TIER) private tierOfDeck;
    mapping(uint => Deck) private deckInfo;
    mapping(uint => bool) public deckFullyDelt;

    /* ======== CONSTRUCTOR ======== */

    constructor(address payable _treasury, address payable _fund, address _helper, uint _preSaleLength) ERC721("INCOOOM", "OOO") {
        require(_treasury != address(0));
        treasury = _treasury;

        require(_fund != address(0));
        fund = _fund;

        require(_helper != address(0));
        helper = _helper;

        preSaleLength = _preSaleLength;

        tiersRemaining[TIER.COMMON] = 36;
        tiersRemaining[TIER.GOLD] = 12;
        tiersRemaining[TIER.PSYCHEDELIC] = 6;
    }

    /* ======== ADMIN FUNCTIONS ======== */

    /**
        @notice start presale
    */
    function startPreSale() external onlyOwner() {
        require(preSale._blockStart == 0, "Presale already started");
        preSaleActive = true;

        preSale = PreSale ({
            _blockStart: block.number,
            _blockEnd: block.number + preSaleLength
        });

        preSaleTiersRemaining[TIER.COMMON] = 3;
        preSaleTiersRemaining[TIER.GOLD] = 2;
        preSaleTiersRemaining[TIER.PSYCHEDELIC] = 1;
        _newDeck(_randomTier());
    }

    /**
        @notice start main sale
    */
    function startSale() external onlyOwner() {
        require(preSale._blockStart != 0 && !saleActive && preSale._blockEnd < block.number, "Cannot start presale twice or start while presale is going on");
        preSaleActive = false;
        saleActive = true;
    }

    /**
        @notice allows admin to reserve decks
        @param _recipient address
        @param _amount uint
        @param _tier TIER
     */
    function reserveDeck(address _recipient, uint _amount, TIER _tier) external onlyOwner() {
        require(preSale._blockStart == 0, "Presale has been started");
        for(uint i; i < _amount; i++) {
            _newDeck(_tier);
            for(uint n; n < numberArr.length; n++){
                _mintCard(_recipient, numberArr[n]);
            }
        }
        deckFullyDelt[currentDeck] = true;
    }

    /**
        @notice allows admin to set base URI
        @param _uri string
     */
    function setBaseURI(string calldata _uri) external onlyOwner() {
        _baseUri = _uri;
    }

    /**
        @notice withdraw ETH in contract to fund and treasury
     */
    function withdraw() external onlyOwner() {
        require(address(this).balance > 0, "Balance is 0");
        uint _fundSend = address(this).balance / 2;
        uint _treasurySend = address(this).balance - _fundSend;

        (bool successFund,) = fund.call{value:_fundSend}("");
        require(successFund, "could not send to fund");

        (bool successTreasury,) = treasury.call{value:_treasurySend}("");
        require(successTreasury, "could not send to treasury");
    }

    /* ======== USER FUNCTIONS ======== */


    /**
        @notice allows users to participate in presale
        @param _amount uint
     */
    function dealPreFlop(uint _amount) external payable {
        require(preSale._blockEnd > block.number && preSaleActive, "Presale not active");
        require(cardsPerDeckPurchased[msg.sender][currentDeck] + _amount <= 5, "Over 5 cards in a single deck");
        require( msg.value == PRICE_PER_CARD_PRESALE * _amount, "Wrong amount");

        if(cardsLeftInDeck >= _amount) {
            _deal(_amount);
        } else {
            uint _nextDeck = _amount - cardsLeftInDeck;
            _deal(cardsLeftInDeck);
            _deal(_nextDeck);
        }

        if(currentDeck == 13) {
            preSaleActive = false;
            require(cardsLeftInDeck == 54, "Cannot mint over 6 decks in presale");
        }

        emit DealPreFlop(msg.sender, _amount);
    }

    /**
        @notice allows users to participate in sale
        @param _amount uint
     */
    function deal(uint _amount) external payable {
        require(saleActive);
        require(cardsPerDeckPurchased[msg.sender][currentDeck] + _amount <= 5, "Over 5 cards in a single deck");
        require( msg.value == PRICE_PER_CARD * _amount, "Wrong amount");

        if(cardsLeftInDeck >= _amount) {
            _deal(_amount);
        } else {
            uint _nextDeck = _amount - cardsLeftInDeck;
            _deal(cardsLeftInDeck);
            _deal(_nextDeck);
        }

        emit Deal(msg.sender, _amount);
    }


    /* ======== INTERNAL HELPER FUNCTIONS ======== */


    /**
        @notice internal deal function
        @param _amount uint
     */
    function _deal(uint _amount) internal {
        cardsPerDeckPurchased[msg.sender][currentDeck] += _amount;

        for(uint i; i < _amount; i++) {

            uint _id = numberArr[numberArr.length - cardsLeftInDeck];

            _mintCard(msg.sender, _id);

            cardsLeftInDeck -= 1;
        }

        if(cardsLeftInDeck == 0) {
            _newDeck(_randomTier());
        }
    }

    /**
        @notice gets random tier for new deck
     */
    function _randomTier() internal view returns(TIER) {
        uint256 n = uint256(keccak256(abi.encodePacked(block.timestamp))) % 9;

        TIER _tier;
        if(preSale._blockEnd > block.number && preSaleActive && currentDeck < 12) {
            if(n <= 5 && preSaleTiersRemaining[TIER.COMMON] > 0) {
                _tier = TIER.COMMON;
            } else if(n <= 7 && preSaleTiersRemaining[TIER.GOLD] > 0) {
                _tier = TIER.GOLD;
            } else if( n == 8 && preSaleTiersRemaining[TIER.PSYCHEDELIC] > 0 ) {
                _tier = TIER.PSYCHEDELIC;
            } else {
                if(preSaleTiersRemaining[TIER.GOLD] > 0 ) {
                    _tier = TIER.GOLD;
                } else if(preSaleTiersRemaining[TIER.COMMON] > 0) {
                    _tier = TIER.COMMON;
                } else {
                    _tier = TIER.PSYCHEDELIC;
                }
            }
        } else {
            if(n <= 5 && tiersRemaining[TIER.COMMON] > 0) {
                _tier = TIER.COMMON;
            } else if(n <= 7 && tiersRemaining[TIER.GOLD] > 0) {
                _tier = TIER.GOLD;
            } else if( n == 8 && tiersRemaining[TIER.PSYCHEDELIC] > 0 ) {
                _tier = TIER.PSYCHEDELIC;
            } else {
                if(tiersRemaining[TIER.GOLD] > 0 ) {
                    _tier = TIER.GOLD;
                } else if(tiersRemaining[TIER.COMMON] > 0) {
                    _tier = TIER.COMMON;
                } else {
                    _tier = TIER.PSYCHEDELIC;
                }
            }
        }

        return _tier;
    }

    /**
        @notice sets new deck
        @param _tier TIER
     */
    function _newDeck(TIER _tier) internal {
        require(!deckFullyDelt[54], "All decks delt");
        deckFullyDelt[currentDeck] = true;

        if(preSale._blockEnd > block.number && preSaleActive && currentDeck < 12) {
            preSaleTiersRemaining[_tier] -= 1;
        }

        if(currentDeck < numberOfDecks) {
            deckInfo[currentDeck + 1] = Deck({
                tier: _tier,
                tiersRemaining: tiersRemaining[_tier]
            });

            tiersRemaining[_tier] -= 1;

            currentDeck += 1; 
            cardsLeftInDeck = 54;
            tierOfDeck[currentDeck] = _tier;
            _shuffle();
        }
    }

    /**
        @notice shuffles deck upon new deck being delt
     */
    function _shuffle() internal {
        for (uint256 i = 0; i < numberArr.length; i++) {
            uint256 n = i + uint256(keccak256(abi.encodePacked(block.timestamp))) % (numberArr.length - i);
            uint256 temp = numberArr[n];
            numberArr[n] = numberArr[i];
            numberArr[i] = temp;
        }
    }

    /**
        @notice mints card
        @param _recipient address
        @param _cardInDeck uint
        @return uint
     */
    function _mintCard(address _recipient, uint _cardInDeck) internal returns (uint) {
        uint id = totalSupply;
        totalSupply += 1;

        _safeMint(_recipient, id);

        (string memory _suit, string memory _value, bool _joker) = IHelper(helper).cardValue(_cardInDeck);

        cardInfo[id] = CardInfo({
            suit: _suit,
            value: _value,
            joker: _joker,
            deck: currentDeck,
            cardNumber: _cardInDeck,
            tier: tierOfDeck[currentDeck]
        });

        return(id);
    }

    /* ======== VIEW FUNCTIONS ======== */

    /**
        @notice gets info of a card
        @param _id uint
        @return _cardInfo CardInfo
     */
    function getCardInfo(uint _id) external view returns(CardInfo memory _cardInfo) {
        uint _deck = cardInfo[_id].deck;
        if(deckFullyDelt[_deck]) {
            _cardInfo = cardInfo[_id];
        }
    }

    /**
        @notice gets URI of a specific token ID
        @param tokenId uint
        @return string
     */
    function tokenURI(uint256 tokenId) override public view returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        CardInfo memory _cardInfo = cardInfo[tokenId];
    
        uint _uri;
        uint _tierNumber = deckInfo[_cardInfo.deck].tiersRemaining;
        uint _num = _cardInfo.cardNumber + 1;

        if(!deckFullyDelt[_cardInfo.deck]) {
            _uri = 0;
        } else {
            if(_cardInfo.tier == TIER.COMMON) {
                _uri = _num + (54 * (36 - _tierNumber));
            } else if (_cardInfo.tier == TIER.GOLD) {
                _uri = _num + (54 * (12 - _tierNumber + 36));
            } else {
                _uri = _num + (54 * (6 - _tierNumber + 48));
            }
        }    

        return string(abi.encodePacked(baseURI(), uint256(_uri).toString()));
    }

     /**
        @notice gets base URI
        @return string
     */
    function baseURI() public view virtual returns (string memory) {
        return _baseUri;
    }

}
function _randomTier() internal view returns(TIER) {
    uint256 n = uint256(keccak256(abi.encodePacked(block.timestamp))) % 9;

    TIER _tier;
    if(preSale._blockEnd > block.number && preSaleActive && currentDeck < 12) {
        if(n <= 5 && preSaleTiersRemaining[TIER.COMMON] > 0) {
            _tier = TIER.COMMON;
        } else if(n <= 7 && preSaleTiersRemaining[TIER.GOLD] > 0) {
            _tier = TIER.GOLD;
        } else if( n == 8 && preSaleTiersRemaining[TIER.PSYCHEDELIC] > 0 ) {
            _tier = TIER.PSYCHEDELIC;
        } else {
            if(preSaleTiersRemaining[TIER.GOLD] > 0 ) {
                _tier = TIER.GOLD;
            } else if(preSaleTiersRemaining[TIER.COMMON] > 0) {
                _tier = TIER.COMMON;
            } else {
                _tier = TIER.PSYCHEDELIC;
            }
        }
    } else {
        if(n <= 5 && tiersRemaining[TIER.COMMON] > 0) {
            _tier = TIER.COMMON;
        } else if(n <= 7 && tiersRemaining[TIER.GOLD] > 0) {
            _tier = TIER.GOLD;
        } else if( n == 8 && tiersRemaining[TIER.PSYCHEDELIC] > 0 ) {
            _tier = TIER.PSYCHEDELIC;
        } else {
            if(tiersRemaining[TIER.GOLD] > 0 ) {
                _tier = TIER.GOLD;
            } else if(tiersRemaining[TIER.COMMON] > 0) {
                _tier = TIER.COMMON;
            } else {
                _tier = TIER.PSYCHEDELIC;
            }
        }
    }

    return _tier;
}

/**
 @notice gets random tier for new deck
 */