contract GoldMaskApe is ERC721Enumerable, Ownable {
    event GoldMaskApeMint(address to, uint256 nftId, uint256 itemData);
    event SaleTimeChanged(
        uint256 newPresaleStart,
        uint256 newPresaleEnd,
        uint256 newPublicSaleEnd
    );

    using Counters for Counters.Counter;
    using Strings for uint256;

    //=======ERC721 BASIC SECTION ======
    Counters.Counter private _tokenIdCounter;
    string private theBaseUrl;
    //=======ADMIN ADDRESS
    address private theAdmin;

    //=======OUR LOGIC SECTION======
    mapping(uint256 => uint256) private theItemDataMapping;
    mapping(address => bool) private theOgList;
    mapping(address => bool) private theWhiteList;
    mapping(address => uint256) private theUserBoughtInPresale;

    uint256 public thePreSaleStartTime;
    uint256 public thePreSaleEndTime;
    uint256 public thePublicSaleEndTime;

    uint256 public constant MAX_SUPPLY = 8192;

    uint256 public PRESALE_MAX = 2000;
    uint256 public MARKETING_RESERVE_MAX = 150;

    uint256 public thePresaleCount;
    uint256 public theMarketingCount;
    uint256 public thePublicSaleCount;

    uint256 public theOgBuyLimit;
    uint256 public theWhiteListBuyLimit;

    uint256 public theOgBuyPrice;
    uint256 public theWhiteListBuyPrice;
    uint256 public thePublicSalePrice;

    //MODIFIERS
    modifier onlyAdminOrOwner() {
        require(
            msg.sender == owner() || msg.sender == theAdmin,
            "ONLY ADMIN OR OWNER CAN CALL THIS METHOD"
        );
        _;
    }

    //=======METHODS=============

    //base url should end with /
    constructor(
        string memory nftBaseUrl,
        address adminAddr,
        uint256 presaleStartTime,
        uint256 presaleEndTime,
        uint256 publicSaleEndTime
    ) payable ERC721("Gold Mask Ape", "GMA") {
        theBaseUrl = nftBaseUrl;
        thePreSaleStartTime = presaleStartTime;
        thePreSaleEndTime = presaleEndTime;
        thePublicSaleEndTime = publicSaleEndTime;
        theAdmin = adminAddr;
        theOgBuyPrice = 0.06 ether;
        theWhiteListBuyPrice = 0.06 ether;
        thePublicSalePrice = 0.08 ether;

        theOgBuyLimit = 4;
        theWhiteListBuyLimit = 2;
    }

    //======= MINT SECTION ==========

    function marketingMint(address[] calldata to) public onlyAdminOrOwner {
        require(
            (theMarketingCount + to.length) <= MARKETING_RESERVE_MAX,
            "MARKETING_RESERVE_MAX EXCEEDED"
        );
        theMarketingCount += to.length;
        for (uint256 i = 0; i < to.length; i++) {
            _mintOne(to[i], i);
        }
    }

    function buy(uint256 count) public payable {
        require(inSalePeriod(), "NOT IN SALE");
        require(count > 0, "CAN'T BUY 0");
        require(count <= queryLeft(), "SOLD OUT");

        if (inPresale()) {
            //IN PRESALE PERIOD
            if (theOgList[msg.sender]) {
                require(
                    theUserBoughtInPresale[msg.sender] + count <= theOgBuyLimit,
                    "EXCEEDED"
                );
            } else if (theWhiteList[msg.sender]) {
                require(
                    theUserBoughtInPresale[msg.sender] + count <=
                        theWhiteListBuyLimit,
                    "EXCEEDED"
                );
            } else {
                revert("PLEASE WAIT FOR PUBLIC SALE");
            }

            thePresaleCount += count;
            theUserBoughtInPresale[msg.sender] += count;
            _buy(count);
        }
        if (inPublicSale()) {
            thePublicSaleCount += count;
            _buy(count);
            return;
        }
    }

    function _buy(uint256 count) private {
        uint256 finalPrice = queryPrice(count);
        require(
            msg.value == finalPrice,
            "MSG.VALUE DOESN'T MATCH REQUIRED VALUE"
        );
        for (uint256 i = 0; i < count; i++) {
            _mintOne(msg.sender, i);
        }
    }

    function _mintOne(address to, uint256 seed) private {
        //avoid hash salt collision
        uint256 scaledSeed = seed * 10;
        uint256 data = genData(scaledSeed);
        _mintWithData(to, data);
    }

    function genData(uint256 seed) public view returns (uint256) {
        uint256 data = 0;
        //7 parts
        for (uint256 i = 0; i < 7; i++) {
            data |= genPart(i, seed + i);
        }
        return data;
    }

    function _mintWithData(address to, uint256 itemData) private {
        uint256 tokenIdCurrent = _tokenIdCounter.current();

        _safeMint(to, _tokenIdCounter.current());
        theItemDataMapping[_tokenIdCounter.current()] = itemData;

        _tokenIdCounter.increment();
        emit GoldMaskApeMint(to, tokenIdCurrent, itemData);
    }

    //======= MINT SECTION ENDS==========

    //=======ADMIN OP=================

    function addWhiteList(address[] calldata newWhiteList)
        public
        onlyAdminOrOwner
    {
        for (uint256 i = 0; i < newWhiteList.length; i++) {
            theWhiteList[newWhiteList[i]] = true;
        }
    }

    function addOGList(address[] calldata newOGList) public onlyAdminOrOwner {
        for (uint256 i = 0; i < newOGList.length; i++) {
            theOgList[newOGList[i]] = true;
        }
    }

    function setBaseUrl(string calldata newBaseUrl) public onlyAdminOrOwner {
        theBaseUrl = newBaseUrl;
    }

    function setOGPrice(uint256 newPrice) public onlyAdminOrOwner {
        require(newPrice >= 10**10);
        theOgBuyPrice = newPrice;
    }

    function setWhiteListPrice(uint256 newPrice) public onlyAdminOrOwner {
        require(newPrice >= 10**10);
        theWhiteListBuyPrice = newPrice;
    }

    function setPublicSalePrice(uint256 newPrice) public onlyAdminOrOwner {
        require(newPrice >= 10**10);
        thePublicSalePrice = newPrice;
    }

    function setSaleTime(
        uint256 presaleStart,
        uint256 presaleEnd,
        uint256 publicSaleEnd
    ) public onlyAdminOrOwner {
        thePreSaleStartTime = presaleStart;
        thePreSaleEndTime = presaleEnd;
        thePublicSaleEndTime = publicSaleEnd;
        emit SaleTimeChanged(presaleStart, presaleEnd, publicSaleEnd);
    }

    function setPresaleMax(uint256 newPreSaleMax) public onlyAdminOrOwner {
        require(
            newPreSaleMax >= thePresaleCount,
            "NEW PRESALE MAX SHOULD BE MORE THAN SOLD COUNT"
        );
        PRESALE_MAX = newPreSaleMax;
    }

    function takeOutNativeToken(address payable to, uint256 amount)
        public
        onlyOwner
    {
        require(amount <= address(this).balance, "INSUFFICIENT BALANCE");
        (bool sent, bytes memory data) = to.call{value: amount}("");
        require(sent, "TRANSFER OUT NATIVE TOKEN FAILED");
    }

    function setAdminAddress(address newAdmin) public onlyOwner {
        theAdmin = newAdmin;
    }

    //=======ADMIN OP ENDS=================

    //=========VIEWS==================

    function inPresale() private view returns (bool) {
        return
            block.timestamp >= thePreSaleStartTime &&
            block.timestamp < thePreSaleEndTime;
    }

    function inPublicSale() private view returns (bool) {
        return
            block.timestamp >= thePreSaleEndTime &&
            block.timestamp < thePublicSaleEndTime;
    }

    function inSalePeriod() private view returns (bool) {
        return
            block.timestamp >= thePreSaleStartTime &&
            block.timestamp < thePublicSaleEndTime;
    }

    function queryBuyableCount() public view returns (uint256) {
        if (inPresale()) {
            if (theOgList[msg.sender]) {
                return
                    theOgBuyLimit > theUserBoughtInPresale[msg.sender]
                        ? theOgBuyLimit - theUserBoughtInPresale[msg.sender]
                        : 0;
            }
            if (theWhiteList[msg.sender]) {
                return
                    theWhiteListBuyLimit > theUserBoughtInPresale[msg.sender]
                        ? theWhiteListBuyLimit -
                            theUserBoughtInPresale[msg.sender]
                        : 0;
            }
        }
        if (inPublicSale()) {
            return queryLeft();
        }
        return 0;
    }

    function queryUserTyp(address user) public view returns (uint256) {
        // 0 FOR NORMAL
        // 1 FOR OG
        // 2 FOR WHITE LIST
        if (theOgList[user]) {
            return 1;
        }

        if (theWhiteList[user]) {
            return 2;
        }

        return 0;
    }

    function queryLeft() public view returns (uint256) {
        if (block.timestamp < thePreSaleStartTime) {
            return MAX_SUPPLY;
        }
        if (inPresale()) {
            return PRESALE_MAX - thePresaleCount;
        }
        if (inPublicSale()) {
            return
                MAX_SUPPLY -
                MARKETING_RESERVE_MAX -
                thePresaleCount -
                thePublicSaleCount;
        }

        return 0;
    }

    function queryPrice(uint256 count)
        public
        view
        returns (uint256 finalPrice)
    {
        uint256 thePrice;

        if (block.timestamp <= thePreSaleEndTime) {
            //before Presale period ends
            if (theOgList[msg.sender]) {
                thePrice = theOgBuyPrice;
            } else if (theWhiteList[msg.sender]) {
                thePrice = theWhiteListBuyPrice;
            } else {
                thePrice = thePublicSalePrice;
            }
        } else {
            thePrice = thePublicSalePrice;
        }
        finalPrice = thePrice * count;
    }

    function _baseURI() internal view override returns (string memory) {
        return theBaseUrl;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        string memory baseURI = _baseURI();

        return
            string(
                abi.encodePacked(
                    baseURI,
                    tokenId.toString(),
                    "/",
                    theItemDataMapping[tokenId].toString()
                )
            );
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override(ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

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

    function getAllTokenIdAndDataByAddress(address user)
        public
        view
        returns (uint256[] memory)
    {
        uint256 userBalance = balanceOf(user);
        uint256[] memory result = new uint256[](userBalance * 2);
        for (uint256 i = 0; i < userBalance; i++) {
            result[i * 2] = tokenOfOwnerByIndex(user, i);
            result[i * 2 + 1] = theItemDataMapping[
                tokenOfOwnerByIndex(user, i)
            ];
        }
        return result;
    }

    function getDataByNftId(uint256 nftId) public view returns (uint256) {
        return theItemDataMapping[nftId];
    }

    function genPart(uint256 partPos, uint256 rndSeed)
        public
        view
        returns (uint256)
    {
        uint256 r1;
        uint256 r2;
        (r1, r2) = Randoms.rnd2(rndSeed, 100);
        uint256 partTyp = 0;
        uint256 partColor = 0;

        if (partPos == 0) {
            if (r1 >= 0 && r1 < 100) {
                partTyp = 0;
            }
            if (r2 >= 0 && r2 < 60) {
                partColor = 0;
            } else if (r2 >= 60 && r2 < 80) {
                partColor = 1;
            } else if (r2 >= 80 && r2 < 90) {
                partColor = 2;
            } else if (r2 >= 90 && r2 < 94) {
                partColor = 3;
            } else if (r2 >= 94 && r2 < 97) {
                partColor = 4;
            } else if (r2 >= 97 && r2 < 99) {
                partColor = 5;
            } else if (r2 >= 99 && r2 < 100) {
                partColor = 6;
            }
        } else if (
            partPos == 1 ||
            partPos == 2 ||
            partPos == 3 ||
            partPos == 5 ||
            partPos == 6
        ) {
            if (r1 >= 0 && r1 < 25) {
                partTyp = 0;
            } else if (r1 >= 25 && r1 < 50) {
                partTyp = 1;
            } else if (r1 >= 50 && r1 < 70) {
                partTyp = 2;
            } else if (r1 >= 70 && r1 < 85) {
                partTyp = 3;
            } else if (r1 >= 85 && r1 < 95) {
                partTyp = 4;
            } else if (r1 >= 95 && r1 < 100) {
                partTyp = 5;
            }
            if (r2 >= 0 && r2 < 25) {
                partColor = 0;
            } else if (r2 >= 25 && r2 < 50) {
                partColor = 1;
            } else if (r2 >= 50 && r2 < 70) {
                partColor = 2;
            } else if (r2 >= 70 && r2 < 85) {
                partColor = 3;
            } else if (r2 >= 85 && r2 < 95) {
                partColor = 4;
            } else if (r2 >= 95 && r2 < 99) {
                partColor = 5;
            } else if (r2 >= 99 && r2 < 100) {
                partColor = 6;
            }
        } else if (partPos == 4) {
            if (r1 >= 0 && r1 < 100) {
                partTyp = 0;
            }
            if (r2 >= 0 && r2 < 25) {
                partColor = 0;
            } else if (r2 >= 25 && r2 < 50) {
                partColor = 1;
            } else if (r2 >= 50 && r2 < 70) {
                partColor = 2;
            } else if (r2 >= 70 && r2 < 85) {
                partColor = 3;
            } else if (r2 >= 85 && r2 < 95) {
                partColor = 4;
            } else if (r2 >= 95 && r2 < 99) {
                partColor = 5;
            } else if (r2 >= 99 && r2 < 100) {
                partColor = 6;
            }
        } else {
            revert("UNKNOW BODY PART");
        }

        return (partTyp << (partPos * 8 + 4)) | (partColor << (partPos * 8));
    }
}
/**
 Gold Mask APE
 */
function inPresale() private view returns (bool) {
    return
        block.timestamp >= thePreSaleStartTime &&
        block.timestamp < thePreSaleEndTime;
}

//=======ADMIN OP ENDS=================
//=========VIEWS==================