contract MetaScyra is Ownable, ERC721EnumerableB {

    using ECDSA for bytes32;
    using Strings for uint256;

    struct Stake {
        uint256 tokenId;
        uint256 timestamp;
    }

    uint256 public constant SCYRA_MAX = 6667;
    uint256 public constant SCYRA_GENESIS_MAX = 667;

    uint256 public constant SCYRA_GIFT = 53;

    uint256 public constant SCYRA_PER_MINT = 2;

    uint256 public constant SCYRA_PURCHASE_LIMIT = 2;
    uint256 public constant SCYRA_PRESALE_PURCHASE_LIMIT = 2;

    uint256 public constant SCYRA_GENESIS_PRICE = 0.15 ether;
    uint256 public constant SCYRA_PRICE = 0.088 ether;

    uint256 public giftedAmount;

    string public provenance;

    string private _tokenBaseURI;

    address private scyra1 = 0x5243891d7641C0b87695437494ae23A17aB582eB;
    address private scyra2 = 0x9DB6dDabe3d8b6181d36367a00D56666cf5c0e97;
    address private scyra3 = 0xb8fc44EB23fc325356198818D0Ef2fec7aC0b6D7;
    address private scyra4 = 0xE35ff894bDf9B92d02f118717385aB1337A4b6df;
    address private scyra5 = 0xea1c9C2152b77835B06f4aD5dF84f6964A1d2117;
    address private scyra6 = 0x39B4fb38391D3Df364894C3fc9166832Bf52ba81;
    address private scyra7 = 0xb550adB2e2d39D02e9f3A4B85237AB303666230d;
    address private scyra8 = 0x4A5056FC80494023E53b2238aE0DE60Cd106E9d9;
    address private scyra9 = 0xb032Cc4bD2295b51b07ea0AA43C3465c8F3ABe2b;
    address private scyra10 = 0x8a907097c89d76766EdEC06Bd3B38917B94Ef9dE;
    address private scyra11 = 0xB8A9021C9c054a0e7A1e7cC9ac8fD21bc8974Bd3;
    address private scyra12 = 0x5F69C83e97B4e410b2f5eAC172854900c72b7927;

    address private signerAddress = 0x6f65E7A2bd16fA95D5f99d3Bc82594304670a118;

    bool public presaleLive;
    bool public saleLive;
    bool public genesisPresaleLive;
    bool public genesisLive;

    mapping(address => uint256) private presalerListPurchases;
    mapping(address => uint256) private purchases;

    mapping(address => uint256) private genesisPresalePurchases;
    mapping(address => uint256) private genesisPurchases;

    mapping(uint256 => Stake) public stakes;
    mapping(uint256 => address) private isStaking;
    mapping(uint256 => uint256) public stakingTime;
    
    constructor(
        string memory baseUri
    ) ERC721B("MetaScyra", "SCYRA") { 
        _tokenBaseURI = baseUri;
        // zero id
        _mint( msg.sender, 0 );
        // auction
        _mint( msg.sender, 1 );
        _mint( msg.sender, 2 );
        _mint( msg.sender, 3 );
        _mint( msg.sender, 4 );
        _mint( msg.sender, 5 );
        _mint( msg.sender, 6 );
        // team genesis
        _mint( scyra1, 7 );
        _mint( scyra1, 8 );
        _mint( scyra2, 9 );
        _mint( scyra3, 10 );
        _mint( scyra4, 11 );
        _mint( scyra5, 12 );
        _mint( scyra6, 13 );
        _mint( scyra7, 14 );
        _mint( scyra8, 15 );
        _mint( scyra9, 16 );
        _mint( scyra12, 17 );
    }

    /* ---- Functions ---- */

    function stake(uint256 _tokenId) public {
        _transfer(msg.sender, address(this), _tokenId);
        stakingTime[_tokenId] = 0;
        stakes[_tokenId] = Stake(_tokenId, block.timestamp);
        _setStaking(msg.sender, _tokenId);
    }

    function unstake(uint256 _tokenId) public {
        require(isStaking[_tokenId] == msg.sender, "NO_PERMISSION");
        stakingTime[_tokenId] += (block.timestamp - stakes[_tokenId].timestamp);
        _unStake(_tokenId);
        _transfer(address(this), msg.sender, _tokenId);
        delete stakes[_tokenId];
    }

    function buy(bytes32 hash, bytes memory signature, uint256 tokenQuantity) external payable {
        require(saleLive, "SALE_CLOSED");
        require(!presaleLive, "ONLY_PRESALE");
        require(tokenQuantity <= SCYRA_PER_MINT, "EXCEED_SCYRA_PER_MINT");
        require(SCYRA_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        require(totalSupply() + tokenQuantity <= SCYRA_MAX, "EXCEED_MAX_SALE_SUPPLY");
        require(purchases[msg.sender] + tokenQuantity <= SCYRA_PURCHASE_LIMIT, "EXCEED_ALLOC");
        require(matchAddressSigner(hash, signature), "NO_DIRECT_MINT");

        purchases[msg.sender] += tokenQuantity;

        for(uint256 i = 0; i < tokenQuantity; i++) {
            uint mintIndex = totalSupply();
           _mint( msg.sender, mintIndex );
        }
    }

    function presaleBuy(bytes32 hash, bytes memory signature, uint256 tokenQuantity) external payable {
        require(!saleLive && presaleLive, "PRESALE_CLOSED");
        require(tokenQuantity <= SCYRA_PER_MINT, "EXCEED_SCYRA_PER_MINT");
        require(SCYRA_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        require(totalSupply() + tokenQuantity <= SCYRA_MAX, "EXCEED_MAX_SALE_SUPPLY");
        require(presalerListPurchases[msg.sender] + tokenQuantity <= SCYRA_PRESALE_PURCHASE_LIMIT, "EXCEED_ALLOC");
        require(matchAddressSigner(hash, signature), "NO_DIRECT_MINT");

        presalerListPurchases[msg.sender] += tokenQuantity;

        for (uint256 i = 0; i < tokenQuantity; i++) {
            uint mintIndex = totalSupply();
            _mint( msg.sender, mintIndex );
        }
    } 

    function buyGenesis(bytes32 hash, bytes memory signature, uint256 tokenQuantity) external payable {
        require(genesisLive, "SALE_CLOSED");
        require(!genesisPresaleLive, "ONLY_PRESALE");
        require(tokenQuantity <= SCYRA_PER_MINT, "EXCEED_SCYRA_PER_MINT");
        require(SCYRA_GENESIS_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        require(totalSupply() + tokenQuantity <= SCYRA_GENESIS_MAX, "EXCEED_MAX_SALE_SUPPLY");
        require(genesisPurchases[msg.sender] + tokenQuantity <= SCYRA_PURCHASE_LIMIT, "EXCEED_ALLOC");
        require(matchAddressSigner(hash, signature), "NO_DIRECT_MINT");

        genesisPurchases[msg.sender] += tokenQuantity;

        for(uint256 i = 0; i < tokenQuantity; i++) {
            uint mintIndex = totalSupply();
           _mint( msg.sender, mintIndex );
        }
    }

    function presaleBuyGenesis(bytes32 hash, bytes memory signature, uint256 tokenQuantity) external payable {
        require(!genesisLive && genesisPresaleLive, "PRESALE_CLOSED");
        require(tokenQuantity <= SCYRA_PER_MINT, "EXCEED_SCYRA_PER_MINT");
        require(SCYRA_GENESIS_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        require(totalSupply() + tokenQuantity <= SCYRA_GENESIS_MAX, "EXCEED_MAX_SALE_SUPPLY");
        require(genesisPresalePurchases[msg.sender] + tokenQuantity <= SCYRA_PRESALE_PURCHASE_LIMIT, "EXCEED_ALLOC");
        require(matchAddressSigner(hash, signature), "NO_DIRECT_MINT");

        genesisPresalePurchases[msg.sender] += tokenQuantity;

        for (uint256 i = 0; i < tokenQuantity; i++) {
            uint mintIndex = totalSupply();
            _mint( msg.sender, mintIndex );
        }
    } 

    function withdraw() external onlyOwner {
        uint balance = address(this).balance;

        _withdraw(scyra1, balance * 29 / 100);
        _withdraw(scyra2, balance * 15 / 100);
        _withdraw(scyra3, balance * 20 / 100);
        _withdraw(scyra4, balance * 5 / 100);
        _withdraw(scyra5, balance * 5 / 100);
        _withdraw(scyra6, balance * 1 / 100);
        _withdraw(scyra7, balance * 12 / 1000);
        _withdraw(scyra8, balance * 5 / 100);
        _withdraw(scyra9, balance * 8 / 1000);
        _withdraw(scyra10, balance * 3 / 100);
        _withdraw(scyra11, balance * 12 / 100);
        _withdraw(scyra12, balance * 3 / 100);
    }

    function _withdraw(address _address, uint256 _amount) private {
        (bool success, ) = _address.call{value: _amount}("");
        require(success, "Transfer failed.");
    }

    function gift(address _to, uint256 _reserveAmount) external onlyOwner {
        require(totalSupply() + _reserveAmount <= SCYRA_MAX, "MAX_MINT");
        require(giftedAmount + _reserveAmount <= SCYRA_GIFT, "NO_GIFTS");
        
        giftedAmount += _reserveAmount;

        for (uint256 i = 0; i < _reserveAmount; i++) {
            uint mintIndex = totalSupply();
            _safeMint( _to, mintIndex );
        }
    }
    
    /* ---- Setters ---- */

    function togglePresaleStatus() external onlyOwner {
        presaleLive = !presaleLive;
    } 

    function toggleSaleStatus() external onlyOwner {
        saleLive = !saleLive;
    }

    function toggleGenesisPresaleStatus() external onlyOwner {
        genesisPresaleLive = !genesisPresaleLive;
    } 

    function toggleGenesisSaleStatus() external onlyOwner {
        genesisLive = !genesisLive;
    }

    function setBaseURI(string calldata URI) external onlyOwner {
        _tokenBaseURI = URI;
    }
    
    function setProvenanceHash(string calldata hash) external onlyOwner {
        provenance = hash;
    } 

    function setSignerAddress(address _signerAddress) external onlyOwner {
        signerAddress = _signerAddress;
    } 

    /* ---- Misc ---- */

    function _setStaking(address _owner, uint256 _tokenId) internal {
        isStaking[_tokenId] = _owner;
    }

    function _unStake(uint256 _tokenId) internal {
        require(isStaking[_tokenId] == msg.sender, "NOT_AUTHORIZED");
        isStaking[_tokenId] = address(0);
    }

    function matchAddressSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
        return signerAddress == hash.recover(signature);
    }

    function tokenURI(uint256 tokenId) external view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return string(abi.encodePacked(_tokenBaseURI, tokenId.toString()));
    } 

    function tokensOfOwner(address addr) external view returns(uint256[] memory) {
        uint256 tokenCount = balanceOf(addr);
        uint256[] memory tokensId = new uint256[](tokenCount);
        for(uint256 i; i < tokenCount; i++){
            tokensId[i] = tokenOfOwnerByIndex(addr, i);
        }
        return tokensId;
    }  
}
function stake(uint256 _tokenId) public {
    _transfer(msg.sender, address(this), _tokenId);
    stakingTime[_tokenId] = 0;
    stakes[_tokenId] = Stake(_tokenId, block.timestamp);
    _setStaking(msg.sender, _tokenId);
}

/* ---- Functions ---- */