contract Encantos is ERC1155 {
    using Strings for uint256;

    struct PricingStrategy {
        uint256 price;
        uint256 totalForDiscount;
        uint256 discountedPrice;
    }

    struct Range {
        uint256 low;
        uint256 high;
        string uri;
    }

    struct CollectionId {
        uint256 id;
        bool exists;
    }

    address payable public owner;
    bool public locked;
    uint256[] private _uriKeys;
    mapping(uint256 => uint256) private _tokenPrices;
    mapping(uint256 => uint256) private _nftMaxTokens;
    mapping(uint256 => uint256) private _totalSupply;
    mapping(uint256 => bytes32) public _merkleRootPerCollection;
    mapping(uint256 => Range[]) private _baseMetadaUris;
    mapping(uint256 => CollectionId) private _collectionForIds;
    mapping(uint256 => mapping(uint256 => bool)) private _mintingStatusByCollectionAndGroup;
    mapping(uint256 => mapping(uint256 => uint256)) private _mintMaxByCollectionAndGroup;
    mapping(uint256 => mapping(uint256 => PricingStrategy)) private pricingStrategy;
    
    

    constructor(bytes32 root, string memory _url) payable ERC1155(_url) {
        owner = payable(msg.sender);
        setMintMaxForGroup(0 , 0, 3);
        setMintMaxForGroup(0, 1, 5);
        setMintMaxForGroup(0 ,2, 10);
        setMintMaxForGroup(0 , 3, 1);
        setPricingStrategy(0, 3, 0.04 ether, 1, 0.04 ether);
        setPricingStrategy(0, 2, 0.04 ether, 10, 0.03 ether);
        setPricingStrategy(0, 1, 0.05 ether, 5, 0.04 ether);
        _tokenPrices[0] = 0.06 ether;
        _nftMaxTokens[0] = 500;
        setMerkleRootPerCollection(0, root);
        setBaseMetadataURI(0,_url, 500);
    }

    function _onlyOwner() private view {
        require(owner == msg.sender, "caller not owner");
    }
    /**
    Modifiers
     */
    modifier ownerOnly() {
        _onlyOwner();
        _;
    }

    modifier noReentrancy() {
        require(!locked, "No reentrancy");
        locked = true;
        _;
        locked = false;
    }

    modifier validAddress(address _addr) {
        require(_addr != address(0), "Not valid address");
        _;
    }

    function transferOwnership(address payable newOwner) public ownerOnly validAddress(newOwner) {
        owner = newOwner;
    }

    function totalSupply(uint256 collection) public view virtual returns (uint256) {
        return _totalSupply[collection];
    }

    function tokenPrice(uint256 collection) public view virtual returns (uint256) {
        return _tokenPrices[collection];
    }

    function nftMaxTokens(uint256 collection) public view virtual returns (uint256) {
        return _nftMaxTokens[collection];
    }

    function collectionForTokenId(uint256 id) public view virtual returns (uint256) {
        require(_collectionForIds[id].exists, "id does not exist");
        return _collectionForIds[id].id;
    }

    function statusForCollectionAndMintGroup(uint256 collection, uint256 mintGroup) public view virtual returns (bool) {
        return _mintingStatusByCollectionAndGroup[collection][mintGroup];
    }

    function mintMaxByCollectionAndGroup(uint256 collection, uint256 mintGroup) public view virtual returns (uint256) {
        return _mintMaxByCollectionAndGroup[collection][mintGroup];
    }

    function getPricingStrategy(uint256 collection, uint256 mintGroup) public view virtual returns (PricingStrategy memory) {
        return pricingStrategy[collection][mintGroup];
    }

    function setTokenPrices(uint256[] memory ids, uint256[] memory prices) public ownerOnly {
        require(ids.length == prices.length, "lengths don't match");
        for (uint256 i = 0; i < ids.length; ++i) {
            _tokenPrices[ids[i]] = prices[i];
        }
    }

    function setCollectionTokenMaximums(uint256[] memory collections, uint256[] memory maxAmounts) public ownerOnly {
        require(collections.length == maxAmounts.length, "lengths don't match");
        for(uint256 i = 0; i < collections.length; ++i) {
            _nftMaxTokens[collections[i]] = maxAmounts[i];
        }
    }

    function setBaseMetadataURI(uint256 collection, string memory _uri, uint256 max) public ownerOnly {
        setNextUri(collection, max, _uri);
    }

    function setMerkleRootPerCollection(uint256 collection, bytes32 merkleRoot) public ownerOnly {
        _merkleRootPerCollection[collection] = merkleRoot;
    }

    function setMintingStatus(uint256 collection, uint256 mintGroup, bool status) public ownerOnly {
        _mintingStatusByCollectionAndGroup[collection][mintGroup] = status;
    }

    function setMintMaxForGroup(uint256 collection, uint256 mintGroup, uint256 maxMints) public ownerOnly {
        _mintMaxByCollectionAndGroup[collection][mintGroup] = maxMints;
    }

    function setPricingStrategy(uint256 collection, uint256 mintGroup, uint256 price, uint256 total, uint256 discountedPrice) public ownerOnly {
        pricingStrategy[collection][mintGroup] = PricingStrategy({totalForDiscount: total, price: price, discountedPrice: discountedPrice });
    }

    function privilegedMint(uint256 collection, uint256 mintGroup, uint256 numToMint,
            address acct, bytes32[] calldata merkleProof) public payable {
        bytes32 node = keccak256(abi.encodePacked(acct, mintGroup, collection));
        require(MerkleProof.verify(merkleProof, _merkleRootPerCollection[collection], node), "not on list");
        require(numToMint > 0, "no tokens requested");
        require(_mintingStatusByCollectionAndGroup[collection][mintGroup] == true, "mint group not active");

        if( numToMint == (pricingStrategy[collection][mintGroup]).totalForDiscount) {
            require(msg.value >= (pricingStrategy[collection][mintGroup]).discountedPrice, "insufficient fund to mint");
        } else {
            require(msg.value >= (pricingStrategy[collection][mintGroup]).price,"insufficient funds to mint");
        }
        require(numToMint < _mintMaxByCollectionAndGroup[collection][mintGroup] + 1,"to many mints requested");
        

        for (uint256 i = _totalSupply[collection]; i < (_totalSupply[collection] + numToMint); i++) {
            uint256 token = collection + i + 1;
            _mint(msg.sender, token, 1,"");
            _collectionForIds[token] = CollectionId({ id: collection, exists: true});
        }
        
        _totalSupply[collection] += numToMint;
    }
    function mint(uint256 collection, uint256 numToMint) public payable {
        require(numToMint > 0, "no tokens requested");
        require(_mintingStatusByCollectionAndGroup[collection][0] == true, "mint group not active");
        require(numToMint < _mintMaxByCollectionAndGroup[collection][0] + 1,"to many mints requested");
        require(msg.value >= (_tokenPrices[collection] * numToMint),"insufficient funds to mint");

        for (uint256 i = _totalSupply[collection]; i < (_totalSupply[collection] + numToMint); i++) {
            uint256 token = collection+ i + 1;
            _mint(msg.sender, token, 1,"");
            _collectionForIds[token] = CollectionId({ id: collection, exists: true});
        }
        
        _totalSupply[collection] += numToMint;
    }

    function airdropNFTs(uint256 collection, address[] memory to) public payable ownerOnly {
        uint256[] memory nftIds = new uint256[](to.length);
        uint256[] memory amounts = new uint256[](to.length);
        uint256 count = 0;
        for(uint256 i = _totalSupply[collection]; i < (_totalSupply[collection] + to.length); ++i) {
            uint256 token = collection + i + 1;
            _mint(msg.sender, token, 1, "");
            _collectionForIds[token] = CollectionId({ id: collection, exists: true});
            nftIds[count] = token;
            amounts[count] = 1;
            count++;
        }

        _totalSupply[collection] += to.length;

        for (uint256 i = 0; i < to.length; ++i) {
            require(to[i] != address(0), "invalid address input");
            safeTransferFrom(msg.sender, to[i], nftIds[i], amounts[i], "");
        }
    }
    
    function uri(uint256 _id) public view override returns (string memory) {   
        uint256 collectionId = collectionForTokenId(_id); 
        string memory paddedId = paddedString(_id, 64);
        string memory _uri = getUri(collectionId, _id);
        return string(abi.encodePacked(_uri, paddedId, ".json"));
    }

    function _beforeTokenTransfer(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual override {
        super._beforeTokenTransfer(operator,from,to,ids, amounts,data);

        if (from == address(0)) {
            for (uint256 i = 0; i < ids.length; ++i) {
                _totalSupply[ids[i]] += amounts[i];
            }
        }

        if ( to == address(0)) {
            for (uint256 i = 0; i < ids.length; ++i) {
                _totalSupply[ids[i]] -= amounts[i];
            }
        }
    }

    function exists(uint256 id) public view virtual returns (bool) {
        require(_collectionForIds[id].exists, "id does not exist");
        return true;
    }

    function paddedString(uint256 value, uint256 length) internal pure returns (string memory) {
        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(length);
        uint256 counter = length;
        while (counter != 0) {
            if (digits != 0){ 
                digits -= 1;
                counter -= 1;
                buffer[counter] = bytes1(uint8(48 + uint256(value % 10)));
                value /= 10;
                
            } else {
                counter -= 1;
                buffer[counter] = "0";
            }
        }
        return string(buffer);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return ERC1155.supportsInterface(interfaceId);
    }

    function deposit() public payable {}

    function withdraw(address payable _to) public payable ownerOnly noReentrancy {
        // get the amount of Ether stored in this contract
        uint amount = address(this).balance;

        // send all Ether to owner
        // Owner can receive Ether since the address of owner is payable
        (bool success, ) = _to.call{value: amount}("");
        require(success, "Failed to send Ether");
    }

    function transfer(address payable _to) public payable ownerOnly noReentrancy {
        // Note that "to" is declared as payable
        (bool success, ) = _to.call{value: msg.value}("");
        require(success, "Failed to send Ether");
    }

    // Function to receive Ether. msg.data must be empty
    receive() external payable {}

    // Fallback function is called when msg.data is not empty
    fallback() external payable {}

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }

    function getUriKeyAtIndex(uint collection, uint index) internal view returns (Range memory) {
        return _baseMetadaUris[collection][index];
    }

    function getUri(uint256 collection, uint256 id) internal view returns (string memory) {
        if (_baseMetadaUris[collection].length == 0 ){
            return _baseMetadaUris[collection][0].uri;
        }
        for (uint256 i = 0; i < _baseMetadaUris[collection].length; ++i) {
            Range memory rng = getUriKeyAtIndex(collection, i);
            if (id + 1 > rng.low && id < rng.high + 1){
                return _baseMetadaUris[collection][i].uri;
            }
        }
        return "";
    }

    function setNextUri(uint256 collection, uint256 high, string memory _uri) internal{
        if (_baseMetadaUris[collection].length > 0) {
            uint256 index = _baseMetadaUris[collection].length - 1;
            Range memory rng = _baseMetadaUris[collection][index];
            if (high + 1 > rng.high){
                _baseMetadaUris[collection].push(Range({uri: _uri, low: rng.high, high:  high}));
            }
            else{
                // can only replace last entry
                _baseMetadaUris[collection][index].high = high;
            }
        } else {
            _baseMetadaUris[collection].push(Range({uri: _uri, low: collection, high: high}));
        }
    }
}
////import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
////import "@openzeppelin/contracts/utils/Strings.sol";
////import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
// Fallback function is called when msg.data is not empty