contract FabledFractals is ERC721("Flames by Fabled Fractals | Official Collection", "FLAME"), ERC721Enumerable, Ownable {
    using SafeMath for uint256;
    using Strings for uint256;

    uint256 private cur_phase = 0;
    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public constant MAX_GIVEAWAY = 100;
    uint256 public constant AIRDROP_MAX_SUPPLY = 5000;
    uint256 public constant SALE_SUPPLY = 5000;
    uint256 public constant WHITELIST_PURCHASE_LIMIT = 4;
    uint256 public constant SALE_PURCHASE_LIMIT = 10;
    uint256 public constant DEVELOPER_FRACTALS_EACH = 6;
    uint256 public constant BASE_PRICE = 100000000000000000; // 0.1 ETH

    address public constant DEV1_ADDRESS = 0xDa17AA88e60CEf4007Cf10aA94191EcF15bA26f3;
    address public constant DEV2_ADDRESS = 0x6e306a7d34860B0D45346a870f6110483fc5F280;
    address public constant DEV3_ADDRESS = 0x03D5a39CD7517bc568aA8B23C4302fA4c67DC204;

    string public constant BASE_URI = "https://storage.googleapis.com/flames/metadata/";

    //
    // https://ethereum.stackexchange.com/questions/55474/verification-of-externally-created-ecdsa-signatures-in-solidity
    //

    function recover(bytes32 hash, bytes memory sig)
        internal
        pure
        returns (address)
    {
        bytes32 r;
        bytes32 s;
        uint8 v;

        // Check the signature length
        if (sig.length != 65) {
            return (address(0));
        }

        // Divide the signature in r, s and v variables
        // ecrecover takes the signature parameters, and the only way to get them
        // currently is to use assembly.
        // solium-disable-next-line security/no-inline-assembly
        assembly {
            r := mload(add(sig, 32))
            s := mload(add(sig, 64))
            v := byte(0, mload(add(sig, 96)))
        }

        // Version of signature should be 27 or 28, but 0 and 1 are also possible versions
        if (v < 27) {
            v += 27;
        }

        // If the version is correct return the signer address
        if (v != 27 && v != 28) {
            return (address(0));
        } else {
            // solium-disable-next-line arg-overflow
            return ecrecover(hash, v, r, s);
        }
    }

    function verifySignature(bytes memory sig) public view returns (bool) {
        bytes32 message = getAddressHash();
        address addr = 0x9EB0F5610D94141c051E19A110FB725cc2493766; // public key
        return recover(message, sig) == addr;
    }

    function getAddressHash() public view returns (bytes32) {
        return keccak256(abi.encodePacked(msg.sender));
    }

    function getAddress() public view returns (address) {
        return msg.sender;
    }

    address[] private whitelist_addresses;
    mapping(address => uint256) private whitelist_limits;

    uint256 private TOTAL_GIVEAWAY = 0;
    bool private DEV_FRACTALS_MINTED = false;
    address[] private giveaway_addresses;
    mapping(address => uint256) private giveaway_limits;

    address[] private airdrop_addresses;
    mapping(address => uint256) private airdrop_limits;

    //
    // Helpers
    //

    // helper to check if value is in a list, O(n)
    function addrInArray(address a, address[] memory list) 
        internal
        pure
        returns (bool)
    {
        for (uint i=0; i < list.length; i++) {
            if (a == list[i]) {
                return true;
            }
        }
        return false;
    }

    // Step phase of the contract minting to ensure that there is no funny business from the developers
    // Cant go back in phases! 
    function stepPhase()        
        external 
        onlyOwner 
    {
        require(cur_phase <= 3, "No phase beyond phase 3");
        cur_phase += 1;        
    }

    // Get the current phase
    function getPhase() 
        view
        external
        returns (uint256)
    {
        return cur_phase;
    }

    function getGiveawayCount() 
        view
        external
        returns (uint256)
    {
        if (addrInArray(msg.sender, giveaway_addresses)) {
            return giveaway_limits[msg.sender];
        } else {
            return 0;
        }
    }

    function getRemainingPresaleCount() 
        view
        external
        returns (uint256)
    {
        // naive logic, if sender not in current WL minters, they have all left
        if (addrInArray(msg.sender, whitelist_addresses)) {
            return whitelist_limits[msg.sender];
        } else {
            return WHITELIST_PURCHASE_LIMIT;
        }
    }


    // Function to withdraw collected amount during minting by the owners
    function withdraw() 
        public 
        onlyOwner 
    {
        uint balance = address(this).balance;
        require(balance > 0, "Balance should be more then zero");
        payable(DEV1_ADDRESS).transfer(balance * 2 / 5);
        payable(DEV2_ADDRESS).transfer(balance * 2 / 5);
        payable(DEV3_ADDRESS).transfer(balance * 1 / 5);
    }

    // Function to get token URI of given token ID
    function tokenURI(uint256 _tokenId)
        public 
        view 
        virtual 
        override 
        returns (string memory) 
    {
        require(_exists(_tokenId), "ERC721Metadata: URI query for nonexistent token");
        return string(abi.encodePacked(BASE_URI, _tokenId.toString(), ".json"));
    }

    //
    // PHASE 0 METHODS
    //

    // set giveaway information 
    function setGiveaway(address[] calldata addresses, uint256[] calldata counts)        
        external 
        onlyOwner 
    {
        require(cur_phase == 0 || cur_phase == 1 || cur_phase == 2, "Giveaways can't be set in airdrop phase");
        require(addresses.length == counts.length, "Ensure input arrays are equal in size");

        uint256 total_giveaway = TOTAL_GIVEAWAY;

        for (uint i = 0; i < counts.length; i++) {
            total_giveaway += counts[i];
        }

        require(total_giveaway <= MAX_GIVEAWAY, "Transaction exceeds contract giveaway limits");

        for (uint i = 0; i < addresses.length; i++) {
            if (addrInArray(addresses[i], giveaway_addresses) == true) {
                giveaway_limits[addresses[i]] += counts[i];
            } else {
                giveaway_addresses.push(addresses[i]);
                giveaway_limits[addresses[i]] = counts[i];
            }
        }

        TOTAL_GIVEAWAY = total_giveaway;
    }

    function mintDevFractals() 
        external
        onlyOwner
    {
        require(cur_phase == 0, 'Can only mint dev fractals in presale');
        require(DEV_FRACTALS_MINTED == false, 'Dev fractals already minted');
        // mint dev allocation of fractals (18)
        for (uint i = 0; i < DEVELOPER_FRACTALS_EACH; i++) {
            _safeMint(DEV1_ADDRESS, totalSupply());
            _safeMint(DEV2_ADDRESS, totalSupply());
            _safeMint(DEV3_ADDRESS, totalSupply());
        }

        airdrop_addresses.push(DEV1_ADDRESS);
        airdrop_limits[msg.sender] = DEVELOPER_FRACTALS_EACH;

        airdrop_addresses.push(DEV2_ADDRESS);
        airdrop_limits[msg.sender] = DEVELOPER_FRACTALS_EACH;

        airdrop_addresses.push(DEV3_ADDRESS);
        airdrop_limits[msg.sender] = DEVELOPER_FRACTALS_EACH;

        DEV_FRACTALS_MINTED = true;
    }

    //
    // PHASE 1 METHODS
    //

    /*
     * Function to mint new NFTs during the presale
     * It is payable. Amount is calculated as per (NFTPrice.mul(_numOfTokens))
    */
    function presaleMintNFT(uint256 _numOfTokens, bytes memory sig)
        public
        payable
    {
        require(cur_phase == 1, 'Can only mint in presale');
        require(verifySignature(sig) == true, "You are not a member of the presale");

        // check if address in whitelist limits, if not add it and continue checks
        if (addrInArray(msg.sender, whitelist_addresses) == false) {
            whitelist_addresses.push(msg.sender);
            whitelist_limits[msg.sender] = WHITELIST_PURCHASE_LIMIT;
        }

        require(whitelist_limits[msg.sender] >= _numOfTokens, "Purchase exceeds your presale allocation");
        require(totalSupply().add(_numOfTokens) <= SALE_SUPPLY, "Purchase would exceed max public supply");
        require(BASE_PRICE.mul(_numOfTokens) == msg.value, "Ether value sent is not correct");

        // mint nfts
        for(uint i = 0; i < _numOfTokens; i++) {
            _safeMint(msg.sender, totalSupply());
            whitelist_limits[msg.sender] -= 1;
        }

        // add to tracker for airdrop
        if (addrInArray(msg.sender, airdrop_addresses) == true) {
            airdrop_limits[msg.sender] += _numOfTokens;
        } else {
            airdrop_addresses.push(msg.sender);
            airdrop_limits[msg.sender] = _numOfTokens;
        }
    }


    function giveawayMintNFT(uint256 _numOfTokens)
        public
        payable
    {
        require(cur_phase == 1 || cur_phase == 2, 'Can only mint giveaways in presale and sale');
        require(addrInArray(msg.sender, giveaway_addresses) == true, "You are not a member of the giveaway");
        require(giveaway_limits[msg.sender] >= _numOfTokens, "Purchase exceeds your giveaway allocation");
        require(totalSupply().add(_numOfTokens) <= SALE_SUPPLY, "Purchase would exceed max public supply");
        require(msg.value == 0, "Ether value should be 0");

        // mint nfts
        for(uint i = 0; i < _numOfTokens; i++) {
            _safeMint(msg.sender, totalSupply());
            giveaway_limits[msg.sender] -= 1;
        }

        // add to tracker for airdrop
        if (cur_phase != 3) {
            if (addrInArray(msg.sender, airdrop_addresses) == true) {
                airdrop_limits[msg.sender] += _numOfTokens;
            } else {
                airdrop_addresses.push(msg.sender);
                airdrop_limits[msg.sender] = _numOfTokens;
            }
        }
    }


    //
    // PHASE 2
    //

    function mintNFT(uint256 _numOfTokens)
        public
        payable
    {
        require(cur_phase == 2, 'Can only mint during sale');
        require(_numOfTokens <= SALE_PURCHASE_LIMIT, "Request amount of NFTs exceeds single transaction limit");
        require(totalSupply().add(_numOfTokens) <= SALE_SUPPLY, "Purchase would exceed max public supply");
        require(BASE_PRICE.mul(_numOfTokens) == msg.value, "Ether value sent is not correct");

        // mint nfts
        for(uint i = 0; i < _numOfTokens; i++) {
            _safeMint(msg.sender, totalSupply());
        }

        // add to tracker for airdrop
        if (cur_phase != 3) {
            if (addrInArray(msg.sender, airdrop_addresses) == true) {
                airdrop_limits[msg.sender] += _numOfTokens;
            } else {
                airdrop_addresses.push(msg.sender);
                airdrop_limits[msg.sender] = _numOfTokens;
            }
        }
    }

    //
    // PHASE 3
    //


    function airdropNFT(uint256 _numOfTokens)
        public
        payable
    {
        require(cur_phase == 3, 'Can only mint airdrops after drop');
        require(addrInArray(msg.sender, airdrop_addresses) == true, "You are not a member of the airdrop");
        require(airdrop_limits[msg.sender] >= _numOfTokens, "Purchase exceeds your airdrop allocation");
        require(totalSupply().add(_numOfTokens) <= MAX_SUPPLY, "Purchase would exceed max supply");
        require(msg.value == 0, "Ether value should be 0");

        // mint nfts
        for(uint i = 0; i < _numOfTokens; i++) {
            _safeMint(msg.sender, totalSupply());
            airdrop_limits[msg.sender] -= 1;
        }
    }

  
    // Standard functions to be overridden in ERC721Enumerable
    function supportsInterface(bytes4 _interfaceId) 
        public
        view 
        override (ERC721, ERC721Enumerable) 
        returns (bool) 
    {
        return super.supportsInterface(_interfaceId);
    }

   
    function _beforeTokenTransfer(address _from, address _to, uint256 _tokenId) 
        internal 
        override(ERC721, ERC721Enumerable) 
    {
        super._beforeTokenTransfer(_from, _to, _tokenId);
    }

}
function presaleMintNFT(uint256 _numOfTokens, bytes memory sig)
    public
    payable
{
    require(cur_phase == 1, 'Can only mint in presale');
    require(verifySignature(sig) == true, "You are not a member of the presale");

    // check if address in whitelist limits, if not add it and continue checks
    if (addrInArray(msg.sender, whitelist_addresses) == false) {
        whitelist_addresses.push(msg.sender);
        whitelist_limits[msg.sender] = WHITELIST_PURCHASE_LIMIT;
    }

    require(whitelist_limits[msg.sender] >= _numOfTokens, "Purchase exceeds your presale allocation");
    require(totalSupply().add(_numOfTokens) <= SALE_SUPPLY, "Purchase would exceed max public supply");
    require(BASE_PRICE.mul(_numOfTokens) == msg.value, "Ether value sent is not correct");

    // mint nfts
    for(uint i = 0; i < _numOfTokens; i++) {
        _safeMint(msg.sender, totalSupply());
        whitelist_limits[msg.sender] -= 1;
    }

    // add to tracker for airdrop
    if (addrInArray(msg.sender, airdrop_addresses) == true) {
        airdrop_limits[msg.sender] += _numOfTokens;
    } else {
        airdrop_addresses.push(msg.sender);
        airdrop_limits[msg.sender] = _numOfTokens;
    }
}

/*
 * Function to mint new NFTs during the presale
 * It is payable. Amount is calculated as per (NFTPrice.mul(_numOfTokens))
 */