abstract contract DimmCityV1Base is ERC721Enumerable {
    uint256 private _tokenIdCounter;
    uint8 public MaxPack;
    uint8 public ReleasePhase;
    bool EnforceRoyality;
    uint256 public MaxSupply;
    address public WithdrawalAddress;
    address public ProxyRegistryAddress;
    string public MetaDataUri;
    mapping(uint256 => int8) private _states;
    mapping(address => uint8) private _presaleList;
    mapping(int8 => mapping(int8 => uint256)) _costs;
    mapping(address => bool) public Admins;
    mapping(address => bool) public Markets;

    event ReleasePhaseChanged(uint8 indexed state);
    event StateChanged(
        address indexed sender,
        uint256 indexed tokenId,
        int8 indexed state
    );

    constructor(
        string memory releaseName,
        string memory releaseSymbol,
        string memory relativeUri,
        uint256 packCost,
        uint256 resurrectionCost,
        uint256 restorationCost
    ) ERC721(releaseName, releaseSymbol) {
        MetaDataUri = string(
            abi.encodePacked(
                "https://sporosnft.io/api/v1",
                relativeUri
            )
        );
        _tokenIdCounter = 1;
        _owners.push(address(0));

        MaxPack = 10;
        WithdrawalAddress = msg.sender;
        Admins[msg.sender] = true;
        MaxSupply = 3000;
        ReleasePhase = Phase.Registration;
        //EnforceRoyality = true;
        ProxyRegistryAddress = address(
            0x000a5409ec958c83c3f309868babaca7c86dcb077c1
        );

        _costs[States.Unminted][States.Unopened] = packCost;
        _costs[States.Lost][States.Alive] = restorationCost;
        _costs[States.Ethereal][States.Alive] = resurrectionCost;
    }

    modifier nonZero(address input) {
        require(input != address(0), "800");
        _;
    }

    //#region Admin

    modifier onlyAdmins() {
        require(isAdmin(msg.sender), "900");
        _;
    }

    function isAdmin(address admin) internal view returns (bool) {
        return Admins[admin];
    }

    function removeAdmin(address admin)
        external
        virtual
        nonZero(admin)
        onlyAdmins
    {
        require(admin != msg.sender, "940");
        delete Admins[admin];
    }

    function addAdmin(address newAdmin)
        external
        virtual
        nonZero(newAdmin)
        onlyAdmins
    {
        require(!isAdmin(newAdmin), "930");
        Admins[newAdmin] = true;
    }

    function owner() public view virtual returns (address) {
        return WithdrawalAddress;
    }

    function addMarket(address newMarket)
        external
        virtual
        nonZero(newMarket)
        onlyAdmins
    {
        Markets[newMarket] = true;
    }

    function removeMarket(address market)
        external
        virtual
        nonZero(market)
        onlyAdmins
    {
        delete Markets[market];
    }

    //#endregion

    function setWithdawalAddress(address newAddress)
        external
        nonZero(newAddress)
        onlyAdmins
    {
        WithdrawalAddress = newAddress;
    }

    function setMaxSupply(uint256 supply) external onlyAdmins {
        require(supply >= _owners.length, "510");
        MaxSupply = supply;
    }

    function setMaxPack(uint8 perPack) external onlyAdmins {
        MaxPack = perPack;
    }

    function setGuard(bool enabled) external onlyAdmins {
        EnforceRoyality = enabled;
    }

    function setMetaDataUri(string memory baseURI) external onlyAdmins {
        MetaDataUri = baseURI;
    }

    function setProxyRegistryAddress(address proxyAddress) external onlyAdmins {
        ProxyRegistryAddress = proxyAddress;
    }

    function getUserCredits(address wallet)
        external
        view
        returns (uint8 credits)
    {
        require(msg.sender == wallet || isAdmin(msg.sender), "900");
        return _presaleList[wallet];
    }

    //ToDo bulk set states

    function bulkUpdateCredits(
        address[] calldata list,
        uint8[] calldata credits
    ) external onlyAdmins {
        require(list.length == credits.length, "520");

        for (uint256 i = 0; i < list.length; i++) {
            _presaleList[list[i]] = credits[i];
        }
    }

    function getPackCost() external view returns (uint256) {
        return _costs[States.Unminted][States.Unopened];
    }

    function setStateCost(
        int8 from,
        int8 to,
        uint256 cost
    ) external onlyAdmins {
        _costs[from][to] = cost;
    }

    function setReleasePhase(uint8 state) external onlyAdmins {
        ReleasePhase = state;
        emit ReleasePhaseChanged(ReleasePhase);
    }

    function getState(uint256 tokenId) external view returns (int8) {
        require(_exists(tokenId), "404");
        return _states[tokenId];
    }

    function setState(uint256 tokenId, int8 state)
        public
        payable
        returns (int8)
    {
        require(state != States.Unopened, "915");
        if (!isAdmin(msg.sender)) {
            require(ownerOf(tokenId) == msg.sender, "910");
            require(msg.value >= _costs[_states[tokenId]][state], "200");
            require(
                _states[tokenId] >= States.Unopened && state > States.Unopened,
                "915"
            );
        }
        _states[tokenId] = state;
        emit StateChanged(msg.sender, tokenId, state);
        return state;
    }

    function burn(uint256 tokenId) external onlyAdmins {
        _burn(tokenId);
        setState(tokenId, States.Annihilated);
    }

    function buyPack(
        address to,
        uint8 numberOfTokens,
        bool openNow
    ) external payable virtual {
        require(
            ReleasePhase >= Phase.PrivatePresale && ReleasePhase < Phase.Closed,
            "000"
        );
        require(numberOfTokens > 0 && numberOfTokens <= MaxPack, "500");

        uint256 currentId = _owners.length;
        require((currentId + numberOfTokens) <= MaxSupply, "400");

        bool admin = isAdmin(msg.sender);
        require(
            admin ||
                msg.value >=
                (_costs[States.Unminted][States.Unopened] * numberOfTokens),
            "200"
        );
        require(
            admin ||
                ReleasePhase >= Phase.PublicPresale ||
                numberOfTokens <= _presaleList[msg.sender],
            "100"
        );

        if (to == address(0)) to = msg.sender;

        int8 initialState = States.Unopened;
        if (ReleasePhase >= Phase.Released && openNow)
            initialState = States.Alive;

        for (uint8 i = 0; i < numberOfTokens; i++) {
            _states[currentId] = initialState;

            if (!admin && _presaleList[msg.sender] > 0)
                _presaleList[msg.sender] = _presaleList[msg.sender] - 1;

            _safeMint(to, currentId);
            currentId++;
        }
        delete currentId;
        delete admin;
    }

    function openPack(uint256[] calldata tokens) external payable virtual {
        require(ReleasePhase >= Phase.Released, "001");
        require(
            msg.value >=
                (_costs[States.Unopened][States.Alive] * tokens.length),
            "200"
        );

        for (uint256 index = 0; index < tokens.length; index++) {
            uint256 tokenId = tokens[index];
            if (
                _states[tokenId] == States.Unopened &&
                msg.sender == ownerOf(tokenId)
            ) {
                _states[tokenId] = States.Alive;
            }
            delete tokenId;
        }
    }

    function sendPack(address to, uint8 numberOfTokens)
        external
        onlyAdmins
        nonZero(to)
    {
        require(numberOfTokens > 0, "410");

        uint256 currentId = _owners.length;
        require(currentId + numberOfTokens <= MaxSupply, "400");
        for (uint8 i = 0; i < numberOfTokens; i++) {
            _states[currentId] = States.Unopened;
            _safeMint(to, currentId);
            currentId++;
        }
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721Enumerable) {
        if (to != address(0) && from != address(0) && !allowTransfer(from)) {
            _states[tokenId] = States.Lost;
            emit StateChanged(msg.sender, tokenId, States.Lost);
        }
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function isApprovedForAll(address _owner, address operator)
        public
        view
        override
        returns (bool)
    {
        if (
            ProxyRegistryAddress != address(0) &&
            Utils.isOpenSeaProxy(ProxyRegistryAddress, _owner, operator)
        ) return true;

        return super.isApprovedForAll(_owner, operator);
    }

    function allowTransfer(address _owner) internal view returns (bool) {
        if (!EnforceRoyality) return true;

        bool result = (isAdmin(msg.sender) || Markets[msg.sender]);

        if (!result && ProxyRegistryAddress == address(0)) return false;

        if (
            !result &&
            Utils.isOpenSeaProxy(ProxyRegistryAddress, _owner, msg.sender)
        ) {
            return true;
        }
        return result;
    }

    function contractURI() public view returns (string memory) {
        return string(abi.encodePacked(MetaDataUri, "/contract.json"));
    }

    function tokenURI(uint256 tokenId)
        external
        view
        virtual
        override
        returns (string memory)
    {
        require(_exists(tokenId), "404");

        return
            string(
                abi.encodePacked(
                    MetaDataUri,
                    "/",
                    Utils.toString(tokenId),
                    ".json?s=",
                    Utils.toString(_states[tokenId])
                )
            );
    }

    function withdraw() external nonZero(WithdrawalAddress) onlyAdmins {
        require(isAdmin(WithdrawalAddress), "910");
        (bool sent, ) = WithdrawalAddress.call{value: address(this).balance}(
            ""
        );

        require(sent, "999");
        delete sent;
    }
}
function bulkUpdateCredits(
    address[] calldata list,
    uint8[] calldata credits
) external onlyAdmins {
    require(list.length == credits.length, "520");

    for (uint256 i = 0; i < list.length; i++) {
        _presaleList[list[i]] = credits[i];
    }
}

//ToDo bulk set states