contract KohiCityLights is Context, AccessControlEnumerable, ERC721, ERC721Enumerable, ERC721Pausable, ERC721Burnable, ReentrancyGuard {

    using Address for address;

    struct Collection {
        string name;
        string baseTokenUri;        
        string description;
        string license;
        uint priceInWei;
        int32 seed;
        uint minted;
        uint mintedMax;        
        uint mintedMaxPerOwner;
        uint lockAt;
        bool paused;
        bool active;        
        string creatorName;
        address payable creatorAddress;        
        address _renderer;
    }
    
    mapping(uint8 => Collection) internal collections;   
    
    event CollectionMinted (
        uint256 indexed tokenId,        
        address indexed recipient,        
        uint256 mintId,
        uint256 priceInWei,
        int32 seed
    );

    uint private lastTokenId;  
    mapping(address => uint) private ownerMints;
    mapping(uint => int32) internal tokenSeed;

    bool private locked;
    bool private presale;    

    uint8 private ownerRoyalty;
    address payable private ownerAddress;

    address private _admin;
    address private _ownership;

    uint presalePriceInWei;
    mapping(address => uint) presaleMints;
    mapping(uint => bool) presoldTokens;

    constructor(address ownership) ERC721("CityLights", "EEPCL") {        
        lastTokenId = 0;
        _admin = _msgSender();
        _contractUri = "https://kohi.art/metadata";
        _ownership = ownership;
        presalePriceInWei = 330000000000000000;
        _pause();      
    }

    string private _contractUri;

    /*
    * @dev See: https://docs.opensea.io/docs/contract-level-metadata
    */
    function contractURI() public view returns (string memory) {
        return _contractUri;
    }

    function updateContractUri(string memory contractUri) public {
        if(_msgSender() != _admin) revert AdminOnly();
        _contractUri = contractUri;
    }

    function updateAdmin(address newAdmin) public {
        if(_msgSender() != _admin) revert AdminOnly();
        if(newAdmin == address(0x0)) revert AddressMustBeSet();
        _admin = newAdmin;       
    }

    function updateOwnerData(uint8 royalty, address payable newAddress) public {
        if(_msgSender() != _admin) revert AdminOnly();
        if(newAddress == address(0x0)) revert AddressMustBeSet();
        if(royalty == 0 || royalty > 100) revert InvalidOwnerRoyalty();        
        ownerRoyalty = royalty;
        ownerAddress = newAddress;
    }

    function getPresale() external view returns (bool) {
        return presale;
    }

    function togglePresale() external {
        if(_msgSender() != _admin) revert AdminOnly();
        if(presale) {
            presale = false;
        } else {
            presale = true;
        }        
    }

    function toggleLocked() external {
        if(_msgSender() != _admin) revert AdminOnly();
        if(locked) {
            locked = false;
        } else {
            locked = true;
        } 
    }

    function togglePaused() external {
        if(_msgSender() != _admin) revert AdminOnly();
        if(paused()) {
            _unpause();
        }
        else {
            _pause();
        }
    }

    function setPresalePrice(uint priceInWei) public {
        if(_msgSender() != _admin) revert AdminOnly();
        presalePriceInWei = priceInWei;
    }

    function setPrice(uint priceInWei) public {
        if(_msgSender() != _admin) revert AdminOnly();
        collections[0].priceInWei = priceInWei;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(AccessControlEnumerable, ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning.
     *
     * Calling conditions:
     *
     * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
     * transferred to `to`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override (ERC721, ERC721Enumerable, ERC721Pausable) {
        super._beforeTokenTransfer(from, to, tokenId);   
        if(collections[0].paused) revert CollectionPaused();                    
    }

    function getCollection() public view returns (Collection memory collection) {        
        return collections[0];
    }

    function updateCollection(Collection memory collection) public {
        if(_msgSender() != _admin) revert AdminOnly();
        if(collection._renderer == address(0x0) || !collection._renderer.isContract()) revert InvalidRenderer();
        collections[0] = collection;
    }
        
    /**
     * @notice Sets the collection's unique seed. It cannot be modified once set.
     * @dev This is a source of external entropy, by the contract owner, to avoid determinism on PRNG that could exploit the mint's parameters.
     */
    function setSeed(int32 seed) external {  
        if(_msgSender() != _admin) revert AdminOnly();
        if(seed == 0) revert InvalidSeed();
        if(collections[0].seed != 0) revert SeedAlreadySet();
        collections[0].seed = seed;
    }

    function getSeed() external view returns (int32) {  
        if(_msgSender() != _admin) revert AdminOnly();
        if(collections[0].seed == 0) revert SeedNotSet();
        return collections[0].seed;
    }

    function getOwnership() external view returns(address) {
        if(_msgSender() != _admin) revert AdminOnly();
        return _ownership;
    }

    function setOwnership(address newOwnership) external {
        if(_msgSender() != _admin) revert AdminOnly();
        _ownership = newOwnership;
    }

    function releaseControl() external {
        if(_msgSender() != _admin) revert AdminOnly();
        IBloomList(_ownership).updateAdmin(_msgSender());
    }       

    function purchasePresale(uint count) external payable {
        purchasePresaleFor(_msgSender(), count);
    }   

    function purchasePresaleFor(address recipient, uint count) public payable nonReentrant {
        if(!presale) revert PresaleNotOpen();

        address minter = _msgSender();
        uint balance = IERC721(_ownership).balanceOf(minter);        
        bool inBloomList = IBloomList(_ownership).isInBloomList(minter);
        
        // start with simple eligibility check
        if(!inBloomList && balance < 1) revert NotEligibleForPresale();

        // get count of all owned kintsugi tokens not already presold
        uint presaleBalance = 0;
        for(uint i = 0; i < balance; i++) {  
            uint tokenId = IERC721Enumerable(_ownership).tokenOfOwnerByIndex(minter, i);
            if(!presoldTokens[tokenId]) {
                presaleBalance++;
                presoldTokens[tokenId] = true;
                if(presaleBalance >= count) {
                    break; // have enough
                }
            }
        }
        
        // if in bloom list, with no entitlements, give one if they haven't minted one
        if(inBloomList && presaleBalance == 0 && presaleMints[minter] == 0) {
            presaleBalance = 1;
        }

        // if minter's count is over their balance, adjust it down
        count = uint(MathV1.min(int(count), int(presaleBalance)));

        // final balance check before minting
        if(count == 0) revert NoRemainingPresaleMints();

        // edge case: minter wants to buy multiple but we have less supply than their count,
        //            but don't want them to get nothing
        int remaining = MathV1.max(0, int(collections[0].mintedMax) - MathV1.min(int(collections[0].lockAt), int(collections[0].minted)));
        count = uint(MathV1.min(int(count), remaining));
        if(count == 0) revert CollectionSoldOut();

        presaleMints[minter] = presaleMints[minter] + count;
        mint(minter, recipient, presalePriceInWei, count);
    }

    function purchase(uint count) external payable {
        purchaseFor(_msgSender(), count);
    }

    function purchaseFor(address recipient, uint count) public payable nonReentrant {        
        if(presale) revert PublicSaleNotOpen();

        address minter = _msgSender();

        uint mintedMaxPerOwner = collections[0].mintedMaxPerOwner;
        if(mintedMaxPerOwner != 0) {            
            // reset for presale minters
            uint totalMintsAllowed = mintedMaxPerOwner + presaleMints[minter];
            if(ownerMints[minter] >= totalMintsAllowed) revert AccountExceedsMaxMints();
        }

        // edge case: minter wants to buy multiple but we have less supply than their count,
        //            but don't want them to get nothing
        int remaining = MathV1.max(0, int(collections[0].mintedMax) - MathV1.min(int(collections[0].lockAt), int(collections[0].minted)));
        count = uint(MathV1.min(int(count), remaining));
        if(count == 0) revert CollectionSoldOut();

        mint(_msgSender(), recipient, collections[0].priceInWei, count);        
    }

    function mint(address minter, address recipient, uint priceInWei, uint count) private {        
        if(ownerAddress == address(0x0)) revert NoOwnerAddress();          
        if(minter.isContract()) revert CannotPurchaseFromContract();
        
        if(locked && minter != _admin) revert CollectionLocked();

        uint totalPrice = priceInWei * count;  
        if(msg.value < totalPrice) revert InsufficientFundsForPurchase();      

        Collection memory collection = collections[0];        
        if(collection.creatorAddress == address(0x0)) revert NoCreatorAddress();      
        if(!collection.active && minter != _admin) revert CollectionInactive(); 
        if(collection.seed == 0) revert SeedNotSet();

        for(uint i = 0; i < count; i++) {
            uint256 nextTokenId = lastTokenId + 1;
            int32 seed = int32(int(uint(keccak256(abi.encodePacked(collection.seed, block.number, minter, recipient, nextTokenId)))));        
            lastTokenId = nextTokenId;        
            tokenSeed[lastTokenId] = seed;        
            collections[0].minted = collection.minted + 1;
            ownerMints[minter] = ownerMints[minter] + 1;

            _safeMint(recipient, nextTokenId);
            emit CollectionMinted(nextTokenId, recipient, collection.minted, collection.priceInWei, seed);

            if(collection.lockAt > 0) {
                if(lastTokenId >= collection.lockAt)
                    locked = true;     
            }
        }

        distributeFunds(totalPrice, collection.creatorAddress);
    }

    function distributeFunds(uint priceInWei, address payable creatorAddress) private {
        if (msg.value > 0) {
            uint overpaid = msg.value - priceInWei;
            if (overpaid > 0) {
                payable(_msgSender()).transfer(overpaid);
            }

            uint dueToOwner = ownerRoyalty * priceInWei / 100;
            uint paidToOwner;  
            if (dueToOwner > 0) {                
                ownerAddress.transfer(dueToOwner);
                paidToOwner = dueToOwner;
            }

            uint dueToCreator = priceInWei - paidToOwner;
            uint paidToCreator;
            if (dueToCreator > 0) {                
                creatorAddress.transfer(dueToCreator);
                paidToCreator = dueToCreator;
            }

            if(priceInWei - paidToOwner - paidToCreator != 0) {
                revert InvalidFundsDistribution();           
            }
        }
    }   
    
    function ownsToken(address owner, uint tokenId) public view returns (bool) {
        for(uint i = 0; i < balanceOf(owner); i++) {
            if(tokenId == tokenOfOwnerByIndex(owner, i)) {
                return true;
            }            
        }
        return false;
    }  

    /**
    * @dev See {IERC721Metadata-tokenURI}.
    */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        Collection memory collection = collections[0];
        string memory baseURI = collection.baseTokenUri;
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, Strings.toString(tokenId))) : "";
    }

    function setRenderer(address renderer) external {
        if(_msgSender() != _admin) revert AdminOnly();
        collections[0]._renderer = renderer;
    }

    /**
     * @notice Retrieve's an artwork's attributes, given a token ID.
     */
    function getAttributes(uint tokenId) external view returns (string memory attributes) {
        if(_msgSender() != _admin && !ownsToken(_msgSender(), tokenId)) revert RenderDisallowed();
        return IAttributes(collections[0]._renderer).getAttributes(tokenSeed[tokenId]);
    }

    /**
     * @notice Begins rendering an artwork given a token ID, and continuation arguments, which must be owned by the caller.
     */
    function _render(uint tokenId, IRenderer.RenderArgs memory args) private view returns (IRenderer.RenderArgs memory results) {
        if(_msgSender() != _admin && !ownsToken(_msgSender(), tokenId)) revert RenderDisallowed();
        if(args.seed != tokenSeed[tokenId]) revert InvalidSeed();  
        return IRenderer(collections[0]._renderer).render(args);
    }

    /**
     * @notice Continues rendering an artwork given a token ID and previous arguments. Token must be owned by the caller.
     */
    function render(uint tokenId, IRenderer.RenderArgs memory args) external view returns (IRenderer.RenderArgs memory results) {        
        return _render(tokenId, args);
    }

    /**
     * @notice Begins rendering an artwork given a token ID. Token must be owned by the caller.
     */
    function beginRender(uint tokenId) external view returns (IRenderer.RenderArgs memory results) {        
        uint32[20480] memory buffer;
        RandomV1.PRNG memory prng;
        return _render(tokenId, IRenderer.RenderArgs(0, 0, tokenSeed[tokenId], buffer, prng));
    }
}
function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId
) internal virtual override (ERC721, ERC721Enumerable, ERC721Pausable) {
    super._beforeTokenTransfer(from, to, tokenId);   
    if(collections[0].paused) revert CollectionPaused();                    
}

/**
 * @dev Hook that is called before any token transfer. This includes minting
 * and burning.
 *
 * Calling conditions:
 *
 * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
 * transferred to `to`.
 * - When `from` is zero, `tokenId` will be minted for `to`.
 * - When `to` is zero, ``from``'s `tokenId` will be burned.
 * - `from` and `to` are never both zero.
 *
 * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
 */