// Contract: SocietyOfTheHourglass
// Target function: function mintNigelRaffle(address _to, uint256 _qty) external onlyOwner cabinetMustBeSet
, 
    ExternalAccount,
    ERC1155, 
    ERC1155Burnable
{
    mapping(uint256 => uint256) private _totalSupply;

    struct CabinetItem {
        uint256 price;
        uint256 amountMinted;
        uint256 amountAllowed;
        uint256 maxMintPerAddress;
        uint256 mainCollectionQty;
    }
    mapping(uint256 => CabinetItem) public items;

    /// @notice How many items someone can mint
    uint256 public maxMintEachAddress = 2;
    mapping(address => uint) public hasMinted;

    string private cabinetUri = "https://storage.hashku.com/api/soth/cabinet/{id}.json";

    uint256 constant private DUST = 0;
    uint256 constant private SCOPE = 1;
    uint256 constant private BOOK = 2;
    uint256 constant private BOT = 3;
    uint256 constant private POSTER = 4;
    uint256 constant private BOOK_CLOSED = 5;
    uint256 constant private BOT_ACTIVE = 6;

    uint256 public currentRaffleIndex = 11;

    mapping(uint256 => bool) public nigelWinners;

    constructor() ERC1155("https://storage.hashku.com/soth/cabinet/{id}.json") {
        // forget me dust
        items[DUST] = CabinetItem(60000000000000000, 0, 222, 2, 2);

        // pocket spectroscope
        items[SCOPE] = CabinetItem(120000000000000000, 0, 166, 2, 4);

        // book of epochs
        items[BOOK] = CabinetItem(170000000000000000, 0, 99, 2, 4);

        // autotranslator bot
        items[BOT] = CabinetItem(170000000000000000, 0, 99, 2, 4);

        // wanted poster of nigel
        items[POSTER] = CabinetItem(180000000000000000, 0, 99, 2, 4);

        // opened book of epochs
        items[BOOK_CLOSED] = CabinetItem(0, 0, 99, 2, 0);

        // activated autotranslator bot
        items[BOT_ACTIVE] = CabinetItem(0, 0, 99, 2, 0);

        // Raffle tickets will be IDs 11-99
    }

    /*
    * @notice Total supply of a given item
    */
    function totalSupply(uint256 id) public view returns (uint256) {
        return _totalSupply[id];
    }

    function _mint(
        address account,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal override {
        super._mint(account, id, amount, data);
        _totalSupply[id] += amount;
    }

    function _burn(
        address account,
        uint256 id,
        uint256 amount
    ) internal override {
        super._burn(account, id, amount);
        _totalSupply[id] -= amount;
    }

    /*
    * @notice Set overall quanitity a user is allowed to mint
    */
    function setMaxMintEachAddress(uint256 _amt) public onlyOwner {
        maxMintEachAddress = _amt;
    }

    /*
    * @notice Set a Nigel raffle ticket as a winner
    */
    function setNigelWinner(uint256 _id) public onlyOwner {
        nigelWinners[_id] = true;
    }

    /*
    * @notice Allow updating the item data
    */
    function setItem( uint256 _id, uint256 _price, uint256 _amountAllowed, uint256 _maxMintPerAddress, uint256 _mainCollectionQty ) public onlyOwner {
        items[_id].price = _price;
        items[_id].amountAllowed = _amountAllowed;
        items[_id].maxMintPerAddress = _maxMintPerAddress;
        items[_id].mainCollectionQty = _mainCollectionQty;
    }

    function uri(uint256) public view override returns (string memory) {
        return cabinetUri;
    }
    
    function setURI(string memory _newuri) public onlyOwner {
        cabinetUri = _newuri;
    }

    /*
    * @notice Shop for a single item of varyiable quantity
    */
    function shop( uint256 _id, uint256 _qty, bytes memory _signature ) public payable {
        // check signature - will be provided by Hashku
        require(verifySignature("SHOP", group, _id, _qty, _signature), "invsig");
        require(_id < 5, "invitm");
        require(_qty * items[_id].price == msg.value, "incfnds");

        mintInternal(msg.sender, _id, _qty);
    }

    /*
    * @notice Shop for more than a single item
    */
    function shopMultiple( uint256[] memory _ids, uint256[] memory _qtys, bytes memory _signature ) public payable {
        // check signature - will be provided by Hashku
        require(verifySignature("SHOP", group, _ids[0], _qtys[0], _signature), "invsig");
        require(_ids.length == _qtys.length, "idqtymm");
        require(_ids.length < 6, "maxitms");

        uint256 _totalPrice;
        for (uint256 _i = 0; _i < _ids.length; _i++) {
            require(_ids[_i] < 5, "invitm");
            _totalPrice += items[_ids[_i]].price * _qtys[_i];
        }

        require(_totalPrice == msg.value, "incfnds");

        for (uint256 _i = 0; _i < _ids.length; _i++) {
            mintInternal(msg.sender, _ids[_i], _qtys[_i]);
        }
    }

    /*
    * @notice Mint an item, used by other functions only
    */
    function mintInternal(address _to, uint256 _id, uint256 _qty) internal {
        require(hasMinted[_to] + _qty <= maxMintEachAddress, "mintmax");
        require(items[_id].amountMinted + _qty <= items[_id].amountAllowed, "itmunv");
        require(_qty + balanceOf(_to, _id) <= items[_id].maxMintPerAddress, "itmmax");

        hasMinted[_to] += _qty;
        items[_id].amountMinted += _qty;
        _mint(_to, _id, _qty, "");
    }

    /*
    * @notice Send tokens to an address (to be used for rewarding community members and partners)
    */
    function send(address _to, uint256[] memory _ids, uint256[] memory _qtys) public onlyOwner {
        require(_ids.length == _qtys.length, "idqtymm");
        require(_ids.length < 6, "maxitms");

        for (uint256 _i = 0; _i < _ids.length; _i++) {
            require(_ids[_i] < 5, "invitm");
        }

        for (uint256 _i = 0; _i < _ids.length; _i++) {
            mintInternal(_to, _ids[_i], _qtys[_i]);
        }
    }

    /*
    * @notice Mint BOOK_CLOSED or BOT_ACTIVE from redemption contract
    */
    function mintExternal(address _to, uint256 _id, uint256 _qty) external onlyExternalAccount {
        require(_id > 4, "invitm");
        require(_id < 7, "invitm");
        items[_id].amountMinted += _qty;
        _mint(_to, _id, _qty, "");
    }

    /*
    * @notice Mint Nigel raffle ticket (IDs 11+)
    */
    function mintNigelRaffle(address _to, uint256 _qty) external onlyExternalAccount {
        for (uint256 _i = 0; _i < _qty; _i++) {
            _mint(_to, currentRaffleIndex, 1, "");
            currentRaffleIndex += 1;
        }
    }

    /*
    * @notice Burn token from redemption account
    */
    function burnExternal(address _to, uint256 _id, uint256 _qty) external onlyExternalAccount {
        _burn(_to, _id, _qty);
    }

    /*
    * @notice Shortcut for retrieving number of main collection NFTs are redemeed from a Cabinet item
    */
    function itemMainCollectionQuantity(uint256 _id) public view returns (uint256) {
        return items[_id].mainCollectionQty;
    }

    function burn(
        address _account,
        uint256 _id,
        uint256 _value
    ) override public virtual onlyOwner {}

    function burnBatch(
        address _account,
        uint256[] memory _ids,
        uint256[] memory _values
    ) override public virtual onlyOwner {}
}


// Dependency file: @openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol


// pragma solidity ^0.8.0;

// import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

/**
 * @dev Implementation of the {IERC721Receiver} interface.
 *
 * Accepts all token transfers.
 * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}.
 */
contract ERC721Holder is IERC721Receiver {
    /**
     * @dev See {IERC721Receiver-onERC721Received}.
     *
     * Always returns `IERC721Receiver.onERC721Received.selector`.
     */
    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) public virtual override returns (bytes4) {
        return this.onERC721Received.selector;
    }
}


// Root file: contracts/SocietyOfTheHourglass.sol

pragma solidity 0.8.9;

/*
*
* Society of the Hourglass: Main Collection + Cabinet of Curiosities Redemption
* 
* Contract by Matt Casanova [Twitter: @DevGuyThings]
* 
* Website: https://www.societyofthehourglass.com/
* Mint Page: https://app.hashku.com/team/society-of-the-hourglass/main/mint
*
*/

// import "contracts/Hashku.sol";
// import "contracts/CabinetOfCuriosities.sol";
// import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
// import "@openzeppelin/contracts/utils/Strings.sol";

contract SocietyOfTheHourglass is Hashku, ERC721Holder {
    using Strings for uint256;

    // total main collection tokens based on 2022-01-01 cabinet items; will decrease as cabinet items are redeemed
    uint256 public reservedFromCabinet = 642;

    // mapping of which tokens have a bot
    mapping(uint256 => bool) public hasBot;

    // mappig of which tokens are in the book
    mapping(uint256 => bool) public inBook;

    // track which nigel token will mint next
    uint256 public nextNigelIndex;

    // how many nigels total
    uint256 constant private nigelsAvailable = 10;

    constructor() Hashku(
        "Society of the Hourglass", 
        "SOTH", 
        "https://storage.hashku.com/api/soth/main/", 
        "SOTH", 
        8888, 
        20, 
        30, 
        70000000000000000, 
        5, 
        0xED0998a1489Ab5cF1900f546f620aB67Af19915B
    ) {
        // on deploy, mint the first 10 tokens to the contract
        // these first ten tokens are nigels
        for (uint256 i = 0; i < nigelsAvailable; i++) {
            mint(address(this));
        }
    }

    // set however many tokens should be reserved for cabinet redemption
    function setReservedFromCabinet(uint256 _amt) public onlyOwner {
        require(nextToken() + _amt <= maxTokens, "too_many");
        reservedFromCabinet = _amt;
    }

    // owner can ONLY decrease the total number of available tokens
    function decreaseMaxTokens(uint256 _amount) external override onlyOwner {
        require(_amount < maxTokens, "only_decrease");
        require(_amount > reservedFromCabinet + nextToken(), "cabinet_reserved");
        maxTokens = _amount;
    }

    // internal function for transferring a nigel
    function transferNigel(address _to) internal {
        // nigels will be 0-9, nigelsAvailable will be 10
        require(nextNigelIndex + 1 < nigelsAvailable, "none_available");
        uint256 nigelIdToClaim = nextNigelIndex;
        nextNigelIndex += 1;
        _safeTransfer(address(this), _to, nigelIdToClaim, "");
        delete nigelIdToClaim;
    }

    // internal function for setting bot on a token
    function setHasBot(uint256 _tokenId) internal {
        require(!hasBot[_tokenId], "already_has_bot");
        hasBot[_tokenId] = true;
    }

    // internal function for marking a token in the book
    function setIsInBook(uint256 _tokenId) internal {
        require(!inBook[_tokenId], "already_has_bot");
        inBook[_tokenId] = true;
    }

    // once final tokens are set and we know all tokens to be in book, will call this to update the contract
    function setIsInBookMultiple(uint256[] calldata _tokenIds) external onlyOwner {
        for (uint256 i = 0; i < _tokenIds.length; i++) {
            setIsInBook(_tokenIds[i]);
        }
    }

    // returns uri with data for book and bot where appropriate
    function tokenURI(uint256 _tokenId) public view override returns (string memory) {
        require(_exists(_tokenId), "no_token");

        string memory append = ".json?var=1";
        string memory addBot = "";
        if (hasBot[_tokenId]) {
            addBot = "&bot=yes";
        }
        string memory addBook = "";
        if (inBook[_tokenId]) {
            addBook = "&book=yes";
        }

        return bytes(baseTokenURI).length > 0 ? string(abi.encodePacked(baseTokenURI, _tokenId.toString(), append, addBot, addBook)) : "";
    }

    // buy NFTs - version without signature required for public mint
    function shop(uint256 _amount) external override payable {
        require(nextToken() + _amount + reservedFromCabinet <= maxTokens, "sold_out");
        require(_amount <= maxMintPerTransaction, "max_mintable");
        require(!isClosed, "is_closed");
        require(isPublic, "not_public");
        require(price * _amount == msg.value, "incorrect_funds");
        for (uint256 i = 0; i < _amount; i++) {
            mint(_msgSender());
        }
    }

    // buy NFTs - signature required from Hashku for presale
    function shop(uint256 _amount, bytes memory _signature) external override payable {
        require(nextToken() + _amount + reservedFromCabinet <= maxTokens, "sold_out");
        require(tokensMinted[_msgSender()] + _amount <= maxMintPerAddress, "max_minted");
        require(!isClosed, "is_closed");
        require(verifySignature(_signature), "invalid_signature");
        require(price * _amount == msg.value, "incorrect_funds");

        tokensMinted[_msgSender()] += _amount;
        for (uint256 i = 0; i < _amount; i++) {
            mint(_msgSender());
        }
    }

    // owner sends NFTs to addresses
    function send(address[] calldata _addresses, uint256[] calldata _amounts) external override onlyOwner {
        require(_addresses.length == _amounts.length, "amount_mismatch");

        uint256 total;
        for (uint256 t = 0; t < _amounts.length; t++) {
            total += _amounts[t];
        }

        require(nextToken() + total + reservedFromCabinet <= maxTokens, "sold_out");

        delete total;

        for (uint256 i = 0; i < _addresses.length; i++) {
            for (uint256 a = 0; a < _amounts[i]; a++) {
                mint(_addresses[i]);
            }
        }
    }

    // cabinet collection address
    CabinetOfCuriosities public externalAccount;

    // redemption of initial cabinet items allowed when true
    bool public redemptionOpen;

    // redemption of secondary cabinet items allowed when true
    bool public secondRedemptionOpen;

    modifier cabinetMustBeSet() {
        require(address(externalAccount) != address(0), "no_cabinet");
        _;
    }

    // open or close initial cabinet item redemption
    function setRedemptionOpen(bool _open) public onlyOwner {
        redemptionOpen = _open;
    }

    // open or close secondary cabinet item redemption
    function setSecondRedemptionOpen(bool _open) public onlyOwner {
        secondRedemptionOpen = _open;
    }

    // set the cabinet account
    function setExternalAccount(address _contract) public onlyOwner {
        externalAccount = CabinetOfCuriosities(_contract);
    }