contract Editions is ERC721 {
    // ============ Constants ============

    string public constant name = "Mirror Editions V2";
    string public constant symbol = "EDITIONS_V2";

    uint256 internal constant REENTRANCY_NOT_ENTERED = 1;
    uint256 internal constant REENTRANCY_ENTERED = 2;

    // ============ Structs ============

    struct Edition {
        // The maximum number of tokens that can be sold.
        uint256 quantity;
        // The price at which each token will be sold, in ETH.
        uint256 price;
        // The account that will receive sales revenue.
        address payable fundingRecipient;
        // The number of tokens sold so far.
        uint256 numSold;
    }

    // A subset of Edition, for efficient production of multiple editions.
    struct EditionTier {
        // The maximum number of tokens that can be sold.
        uint256 quantity;
        // The price at which each token will be sold, in ETH.
        uint256 price;
        bytes32 contentHash;
    }

    mapping(address => uint256) public fundingBalance;

    // ============ Immutable Storage ============

    // Fee updates take 2 days to take place, giving creators time to withdraw.
    uint256 public immutable feeUpdateTimelock;

    // ============ Mutable Storage ============

    string internal baseURI;
    // Mapping of edition id to descriptive data.
    mapping(uint256 => Edition) public editions;
    // Mapping of token id to edition id.
    mapping(uint256 => uint256) public tokenToEdition;
    // The amount of funds that have already been withdrawn for a given edition.
    mapping(uint256 => uint256) public withdrawnForEdition;
    // `nextTokenId` increments with each token purchased, globally across all editions.
    uint256 private nextTokenId;
    // Editions start at 1, in order that unsold tokens don't map to the first edition.
    uint256 private nextEditionId = 1;
    // Withdrawals include a fee, specified as a percentage.
    uint16 public feePercent;
    // The address that holds fees.
    address payable public treasury;
    uint256 public feesAccrued;
    // Timelock information.
    uint256 public nextFeeUpdateTime;
    uint16 public nextFeePercent;
    // Reentrancy
    uint256 internal reentrancyStatus;

    address public owner;
    address public nextOwner;

    // ============ Events ============

    event EditionCreated(
        uint256 quantity,
        uint256 price,
        address fundingRecipient,
        uint256 indexed editionId,
        bytes32 contentHash
    );

    event EditionPurchased(
        uint256 indexed editionId,
        uint256 indexed tokenId,
        // `numSold` at time of purchase represents the "serial number" of the NFT.
        uint256 numSold,
        uint256 amountPaid,
        // The account that paid for and received the NFT.
        address indexed buyer
    );

    event FundsWithdrawn(
        address fundingRecipient,
        uint256 amountWithdrawn,
        uint256 feeAmount
    );

    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );

    event FeesWithdrawn(uint256 feesAccrued, address sender);

    event FeeUpdateQueued(uint256 newFee, address sender);

    event FeeUpdated(uint256 feePercent, address sender);

    // ============ Modifiers ============

    modifier nonReentrant() {
        // On the first call to nonReentrant, _notEntered will be true
        require(reentrancyStatus != REENTRANCY_ENTERED, "Reentrant call");

        // Any calls to nonReentrant after this point will fail
        reentrancyStatus = REENTRANCY_ENTERED;

        _;

        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        reentrancyStatus = REENTRANCY_NOT_ENTERED;
    }

    modifier onlyOwner() {
        require(isOwner(), "caller is not the owner.");
        _;
    }

    modifier onlyNextOwner() {
        require(isNextOwner(), "current owner must set caller as next owner.");
        _;
    }

    // ============ Constructor ============

    constructor(
        string memory baseURI_,
        address payable treasury_,
        uint16 initialFee,
        uint256 feeUpdateTimelock_,
        address owner_
    ) {
        baseURI = baseURI_;
        treasury = treasury_;
        feePercent = initialFee;
        feeUpdateTimelock = feeUpdateTimelock_;
        owner = owner_;
    }

    // ============ Edition Methods ============

    function createEditionTiers(
        EditionTier[] memory tiers,
        address payable fundingRecipient
    ) external nonReentrant {
        // Execute a loop that creates editions.
        for (uint8 i = 0; i < tiers.length; i++) {
            uint256 quantity = tiers[i].quantity;
            uint256 price = tiers[i].price;

            editions[nextEditionId] = Edition({
                quantity: quantity,
                price: price,
                fundingRecipient: fundingRecipient,
                numSold: 0
            });

            emit EditionCreated(
                quantity,
                price,
                fundingRecipient,
                nextEditionId,
                tiers[i].contentHash
            );

            nextEditionId++;
        }
    }

    function createEdition(
        // The number of tokens that can be minted and sold.
        uint256 quantity,
        // The price to purchase a token.
        uint256 price,
        // The account that should receive the revenue.
        address payable fundingRecipient,
        // Content hash is emitted in the event, for UI convenience.
        bytes32 contentHash
    ) external nonReentrant {
        editions[nextEditionId] = Edition({
            quantity: quantity,
            price: price,
            fundingRecipient: fundingRecipient,
            numSold: 0
        });

        emit EditionCreated(
            quantity,
            price,
            fundingRecipient,
            nextEditionId,
            contentHash
        );

        nextEditionId++;
    }

    function buyEdition(uint256 editionId) external payable nonReentrant {
        // Check that the edition exists. Note: this is redundant
        // with the next check, but it is useful for clearer error messaging.
        require(editions[editionId].quantity > 0, "Edition does not exist");
        // Check that there are still tokens available to purchase.
        require(
            editions[editionId].numSold < editions[editionId].quantity,
            "This edition is already sold out."
        );
        // Check that the sender is paying the correct amount.
        require(
            msg.value >= editions[editionId].price,
            "Must send enough to purchase the edition."
        );
        // Increment the number of tokens sold for this edition.
        editions[editionId].numSold++;
        fundingBalance[editions[editionId].fundingRecipient] += msg.value;
        // Mint a new token for the sender, using the `nextTokenId`.
        _mint(msg.sender, nextTokenId);
        // Store the mapping of token id to the edition being purchased.
        tokenToEdition[nextTokenId] = editionId;

        emit EditionPurchased(
            editionId,
            nextTokenId,
            editions[editionId].numSold,
            msg.value,
            msg.sender
        );

        nextTokenId++;
    }

    // ============ Operational Methods ============

    function withdrawFunds(address payable fundingRecipient)
        external
        nonReentrant
    {
        uint256 remaining = fundingBalance[fundingRecipient];
        fundingBalance[fundingRecipient] = 0;

        if (feePercent > 0) {
            // Send the amount that was remaining for the edition, to the funding recipient.
            uint256 fee = computeFee(remaining);
            // Allocate fee to the treasury.
            feesAccrued += fee;
            // Send the remainder to the funding recipient.
            _sendFunds(fundingRecipient, remaining - fee);
            emit FundsWithdrawn(fundingRecipient, remaining - fee, fee);
        } else {
            _sendFunds(fundingRecipient, remaining);
            emit FundsWithdrawn(fundingRecipient, remaining, 0);
        }
    }

    function computeFee(uint256 _amount) public view returns (uint256) {
        return (_amount * feePercent) / 100;
    }

    // ============ Admin Methods ============

    function withdrawFees() public {
        _sendFunds(treasury, feesAccrued);
        emit FeesWithdrawn(feesAccrued, msg.sender);
        feesAccrued = 0;
    }

    function updateTreasury(address payable newTreasury) public {
        require(msg.sender == treasury, "Only available to current treasury");
        treasury = newTreasury;
    }

    function queueFeeUpdate(uint16 newFee) public {
        require(msg.sender == treasury, "Only available to treasury");
        nextFeeUpdateTime = block.timestamp + feeUpdateTimelock;
        nextFeePercent = newFee;
        emit FeeUpdateQueued(newFee, msg.sender);
    }

    function executeFeeUpdate() public {
        require(msg.sender == treasury, "Only available to current treasury");
        require(
            block.timestamp >= nextFeeUpdateTime,
            "Timelock hasn't elapsed"
        );
        feePercent = nextFeePercent;
        nextFeePercent = 0;
        nextFeeUpdateTime = 0;
        emit FeeUpdated(feePercent, msg.sender);
    }

    function changeBaseURI(string memory baseURI_) public onlyOwner {
        baseURI = baseURI_;
    }

    function isOwner() public view returns (bool) {
        return msg.sender == owner;
    }

    function isNextOwner() public view returns (bool) {
        return msg.sender == nextOwner;
    }

    function transferOwnership(address nextOwner_) external onlyOwner {
        require(nextOwner_ != address(0), "Next owner is the zero address.");

        nextOwner = nextOwner_;
    }

    function cancelOwnershipTransfer() external onlyOwner {
        delete nextOwner;
    }

    function acceptOwnership() external onlyNextOwner {
        delete nextOwner;

        emit OwnershipTransferred(owner, msg.sender);

        owner = msg.sender;
    }

    function renounceOwnership() external onlyOwner {
        emit OwnershipTransferred(owner, address(0));
        owner = address(0);
    }

    // ============ NFT Methods ============

    // Returns e.g. https://mirror-api.com/editions/[editionId]/[tokenId]
    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        // If the token does not map to an edition, it'll be 0.
        require(tokenToEdition[tokenId] > 0, "Token has not been sold yet");
        // Concatenate the components, baseURI, editionId and tokenId, to create URI.
        return
            string(
                abi.encodePacked(
                    baseURI,
                    _toString(tokenToEdition[tokenId]),
                    "/",
                    _toString(tokenId)
                )
            );
    }

    // Returns e.g. https://mirror-api.com/editions/metadata
    function contractURI() public view returns (string memory) {
        // Concatenate the components, baseURI, editionId and tokenId, to create URI.
        return string(abi.encodePacked(baseURI, "metadata"));
    }

    function getRoyaltyRecipient(uint256 tokenId)
        public
        view
        returns (address)
    {
        require(tokenToEdition[tokenId] > 0, "Token has not been minted yet");
        return editions[tokenToEdition[tokenId]].fundingRecipient;
    }

    function setRoyaltyRecipient(
        uint256 editionId,
        address payable newFundingRecipient
    ) public {
        require(
            editions[editionId].fundingRecipient == msg.sender,
            "Only current fundingRecipient can modify its value"
        );

        editions[editionId].fundingRecipient = newFundingRecipient;
    }

    // ============ Private Methods ============

    function _sendFunds(address payable recipient, uint256 amount) private {
        require(
            address(this).balance >= amount,
            "Insufficient balance for send"
        );

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Unable to send value: recipient may have reverted");
    }

    // From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol
    function _toString(uint256 value) internal pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
}
/**
 * @title Editions
 * @author MirrorXYZ
 */
function _toString(uint256 value) internal pure returns (string memory) {
    // Inspired by OraclizeAPI's implementation - MIT licence
    // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

    if (value == 0) {
        return "0";
    }
    uint256 temp = value;
    uint256 digits;
    while (temp != 0) {
        digits++;
        temp /= 10;
    }
    bytes memory buffer = new bytes(digits);
    while (value != 0) {
        digits -= 1;
        buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
        value /= 10;
    }
    return string(buffer);
}

// From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol