contract ethercards is ERC721 , Ownable, Pausable{
    using Strings for uint256;
    using SafeMath for uint256;

    IRNG rng;

    enum CardType { OG, Alpha, Random, Common, Founder,  Unresolved } 

    uint256   constant oStart = 10;
    uint256   constant aStart = 100;
    uint256   constant cStart = 1000;
    uint256   constant oMax = 99;
    uint256   constant aMax = 999;
    uint256   constant cMax = 9999;

    uint256   constant tr_ass_order_length = 14;
    uint256   constant tr_ass_order_mask   = 0x3ff;

    uint256   extra_trait_offset ;



    // sale conditions
    uint256   immutable sale_start;
    uint256   immutable sale_end;

    
    bool      curve_set;


    // sold AND resolved
    uint256   public oSold;
    uint256   public aSold;
    uint256   public cSold;
    // pending resolution
    uint256   public oPending;
    uint256   public aPending;
    uint256   public cPending;
    
    uint256   public nextTokenId = 10;
    // Random Stuff
    mapping (uint256 => bytes32) public randomRequests;
    uint256                      public lastRandomRequested;
    uint256                      public lastRandomProcessed;
    uint256                      public randomOneOfEight;

    // pricing stuff
    uint256[]                   og_stop;
    uint256[]                   og_price;
    uint256[]                   alpha_stop;
    uint256[]                   alpha_price;
    uint256[]                   common_stop;
    uint256[]                   common_price;
    uint256                     og_pointer;
    uint256                     alpha_pointer;
    uint256                     common_pointer;

    address payable             wallet;


    // traits stuff
    bytes32[50] public           traitHashes;
    mapping (uint256 => uint256)  traitAssignmentOrder;
    // Validation
    uint256                     startPos;
    bytes32                     tokenIdHash;


    //mapping(uint256 => uint256) serialToTokenId;
    //mapping(uint256 => uint256) tokenIdToSerial;
    mapping(uint256 => uint256) cardTraits;
    

    bytes32      public fullTokenIDHash;
    bytes32[50]  public allTokenIDHashes;


    
    bool    presale_closed;
    bool    founders_done;
    address oracle;
    address controller;

    event OG_Ordered(address buyer, uint256 price_paid, uint256 tokenID);
    event ALPHA_Ordered(address buyer, uint256 price_paid, uint256 tokenID);
    event COMMON_Ordered(address buyer, uint256 price_paid, uint256 tokenID);

    event Resolution(uint256 tokenId,uint256 chance);

    event PresaleClosed();
    event OracleSet( address oracle);
    event ControllerSet( address oracle);
    event SaleSet(uint256 start, uint256 end);
    event RandomSet(address random);
    event HashesSet();
    
    event WheresWallet(address wallet);
    event Upgrade(uint256 tokenID, uint256 position);

    event UpgradeToOG(uint256 tokenId,uint256 pos);
    event UpgradeToAlpha(uint256 tokenId,uint256 pos);

    event TraitsClaimed(uint tokenID,uint traits);
    event TraitsAlreadyClaimed(uint tokenID);
 
    
    modifier onlyOracle() {
        require(msg.sender == oracle,"Not Authorised");
        _;
    }

    modifier onlyAllowed() {
        require(
            msg.sender == owner() ||
            msg.sender == controller,"Not Authorised");
        _;
    }

    // SECTIONS IN CAPS TO RETAIN SANITY

    // CONSTRUCTOR
    // traitHashes : hashes of 50 x 200 elements

    constructor(
        IRNG _rng, 
        uint256 _start, uint256 _end,
        address payable _wallet, address _oracle
        ) ERC721("Ether Cards Founder","ECF") {

        
        rng = _rng;
        sale_start = _start;
        sale_end = _end;
        wallet = _wallet;
        oracle = _oracle;
// need events
        emit OracleSet(_oracle);
        emit SaleSet(_start,_end);
        emit RandomSet(address(_rng));
        emit WheresWallet(_wallet);
    }

    function setTraitHashes(bytes32[50] memory _traitHashes) external onlyOwner {
        traitHashes = _traitHashes;
        emit HashesSet();
    }

    function setCurve(
        uint256[] memory _og_stop, uint256[] memory _og_price,
        uint256[] memory _alpha_stop, uint256[] memory _alpha_price,
        uint256[] memory _random_stop, uint256[] memory _random_price) external onlyOwner {
        og_stop = _og_stop;
        og_price = _og_price;
        alpha_stop = _alpha_stop;
        alpha_price = _alpha_price;
        common_stop = _random_stop;
        common_price = _random_price;
        curve_set = true;
        _setBaseURI("temp.ether.cards/metadata");
    }


    // ENTRY POINT TO SALE CONTRACT
    // 0 = OG
    // 1 = ALPHA
    // 2 = RANDOM

    event Refund(address buyer, uint sent, uint purchased, uint refund);

    function buyCard(uint card_type) external payable sale_active whenNotPaused {
        buyCardInternal(card_type);
    }


    function buyCardInternal(uint card_type) internal {
        require(curve_set,"price curve not set");
        uint balance = msg.value;
        uint price;
        require(card_type < 3, "Invalid card type");
        for (uint j = 0; j < 100; j++) {
            if (card_type == 0) {
                price = OG_price();
            }  else if (card_type == 1) {
                price =  ALPHA_price();
            } else {
                price = COMMON_price();
            }
            if (balance < price) {
                if (j == 0) require(false,"Not enough sent");
                payable(wallet).transfer(msg.value.sub(balance));
                payable(msg.sender).transfer(balance);
                emit Refund(msg.sender,msg.value, j,balance);
                return;
            }
            assignCard(msg.sender,card_type);
            balance = balance.sub(price);
        }
        payable(wallet).transfer(msg.value.sub(balance));
        payable(msg.sender).transfer(balance);
        emit Refund(msg.sender,msg.value, 100,balance);
}

    // PRESALE FUNCTIONS
    // 0 - OG
    // 1 - ALPHA
    // 2 - COMMON

    function allocateManyCards(address[] memory buyers, uint256 card_type) external onlyOwner {
        require(curve_set,"price curve not set");
         require(founders_done, "mint founders first");
        require(card_type < 3 , "Invalid Card Type");
        require(!presale_closed,"Presale is over");
        for (uint j = 0; j < buyers.length; j++) {
            assignCard(buyers[j],card_type);
        }
    }
    
    function allocateCard(address buyer, uint256 card_type) external onlyOwner {
        require(curve_set,"price curve not set");
         require(founders_done, "mint founders first");
        require(card_type < 3, "Invalid Card Type");
        require((!presale_closed) || sale_is_over() ,"Presale is over");
        assignCard(buyer,card_type);
    }

    function closePresalePartOne() external onlyOwner {
        if (randomOneOfEight % 16 > 7) {
            request_random();
        }
        if (randomOneOfEight % 16 > 0) {
            request_random();
        }
    }

    function closePresalePartTwo() external onlyOwner {
        processRand();
        presale_closed = true;
        emit PresaleClosed();
    }


    // FOUNDERS CARDS

    function mintFounders(address[10] memory founders) external onlyOwner {
        require(!founders_done, "Founders already minted");
        for (uint j = 0; j < 10; j++) {
            _mint(founders[j],j);
            traitAssignmentOrder[j] = 1;
        }
        founders_done = true;
    }

    // Extra Traits

    function setExtraTraits(uint256 tokenId, uint256 bitNumber) external onlyAllowed {
        require((bitNumber >= extra_trait_offset) && (bitNumber < 256), "illegal bit number");
        cardTraits[tokenId] |=   (1 << bitNumber);
    }

    function setExtraTraitOffset(uint256 _offset) external onlyOwner {
        require(extra_trait_offset == 0, "Extra Trait offset already set");
        extra_trait_offset = _offset;
    }

    // ORACLE ACTIVATION

    function numberPending() public view returns (uint256) {
        return oPending + cPending  +aPending;
    }

    function needProcessing() public view returns (bool) {
        uint count = 15;
        
        return (oPending + cPending  +aPending > count || nextTokenId > cMax) && randomAvailable();
    }

    event ProcessRandom();
    function processRandom() external onlyOracle {
        processRand();
    }

    function processRand() internal {
        emit ProcessRandom();
        uint random = nextRandom();
    
        uint count = 16;
        uint mask = 0xffff;
        uint shift = 16;
        
        uint pending = oPending + cPending +aPending;
        for (uint i = 0; i < count; i++) {
            if (pending-- == 0) {
                return;
            }
            resolve(random & mask);
            random = random >> shift;
        }
    }

    function setOracle(address _oracle) external onlyOwner {
        oracle = _oracle;
        emit OracleSet(_oracle);
    }

   function setController(address _controller) external onlyOwner {
        controller = _controller;
        emit ControllerSet(_controller);
    }

    // WEB3 SALE SUPPORT


    function OG_remaining() public view returns (uint256) {
        return oMax - (oStart + oSold + oPending)+1;
    }

    function ALPHA_remaining() public view returns (uint256) {
        return aMax - (aStart + aSold + aPending)+1;
    }

    function COMMON_remaining() public view returns (uint256) {
        return cMax - (cStart + cSold + cPending)+1;
    }

    function OG_price() public view returns (uint256) {
        require(OG_remaining() > 0,"OG Cards sold out"); 
        return og_price[og_pointer];
    }

    function ALPHA_price() public view returns (uint256) {
        require(ALPHA_remaining() > 0,"Alpha Cards sold out"); 
        return alpha_price[alpha_pointer];        
    }

    function COMMON_price() public view returns (uint256) {
        require(COMMON_remaining() > 0,"Random Cards sold out"); 
        return common_price[common_pointer];
    }

    modifier sale_active() {
        require(block.timestamp >= sale_start,"Sale not started");
        require(block.timestamp <= sale_end,"Sale ended");
        require(nextTokenId <= cMax, "Sorry. Sold out");
        _;
    }


    function request_random_if_needed() internal {

        if (randomOneOfEight++ % 16 == 15) {
            request_random();
        }

    }

 
    function assignCard(address buyer, uint256 card_type) internal {
        require(curve_set,"price curve not set");
        
        uint common_remaining = COMMON_remaining();
        uint alpha_remaining = ALPHA_remaining();
        request_random_if_needed();
        if (card_type == 2) {
            require(common_remaining > 0, "Sorry no random tickets available");
            uint cSum = cStart+cSold+cPending;
            _mint(buyer,cSum);
            cPending++;
            common_pointer = bump(cSold , cPending , common_stop,common_pointer);
            emit COMMON_Ordered(msg.sender, msg.value,cSum);
            return;
        } else if (card_type == 1)  {
            require (alpha_remaining > 0,"Sorry - no Alpha tickets available");
            uint aSum =aStart+aSold+aPending;
            _mint(buyer,aSum);
            emit ALPHA_Ordered(msg.sender, msg.value,aSum);
            aPending++;
            alpha_pointer = bump(aSold , aPending , alpha_stop,alpha_pointer);
            return;
        }
        require (OG_remaining() > 0, "Sorry, no OG cards available");
        uint oSum = oStart + oSold+oPending;
        _mint(buyer,oSum);
        emit OG_Ordered(msg.sender, msg.value,oSum);
        oPending++;
        og_pointer = bump(oSold,oPending,og_stop,og_pointer);
        return;
    }
 
   function resolve(uint256 random) internal {
        //bool upgrade;
        uint256 card_pos;
        //uint256 draw_pos;  // let's not get them confused
        uint256 r = random;
        if (cPending > 0) {
            card_pos = cStart + cSold++;
            cPending--;
        } else if (aPending > 0) {
            card_pos = aStart + aSold++;
            aPending--;
        } else if (oPending > 0) {
            card_pos = oStart+oSold++;
            oPending--;
        }   else {
            return; // NOTHING TO DO
        }
        uint256 chance = (r & tr_ass_order_mask)+1;       
        traitAssignmentOrder[card_pos] = chance;
        emit Resolution(card_pos,chance);
    }

    function bump(uint sold, uint pending, uint[] memory stop, uint pointer) internal pure returns (uint256) {
        if (pointer == stop.length - 1) return pointer; 
        if (sold + pending > stop[pointer]) {
            return pointer + 1;
        }
        return pointer;
    }
    
    function mintTheRest(uint card_type, address target) external onlyOwner {
        require(sale_is_over(), "not until it's over");
        require(card_type < 3,"invalid card type");
        uint remaining;
        uint toMint = 50;
        if (card_type == 0) remaining = OG_remaining();
        else if (card_type == 1) remaining = ALPHA_remaining();
        else if (card_type == 2) {
            remaining = COMMON_remaining();
            toMint = 30;
        }
        remaining = Math.min(remaining,toMint);
        for (uint j = 0; j < remaining; j++) {
            assignCard(target,card_type);
        }
    }


  
    function setSimpleHash200(uint pos,bytes32 hashX) external onlyAllowed {        
        allTokenIDHashes[pos] = hashX;
    }

    function hash10k(uint256[10000] memory data) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(data));
    }

    function hash200(uint256[200] memory data) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(data));
    }

    // ensures that the hashes are all correct
    function checkSimpleHash200( uint[10000] calldata input) public view returns (bool) {
        uint256[200] memory data;
        for (uint pos = 0; pos < 50; pos++){  
            for (uint j = 0; j < 200; j++) {
                data[j] = input[j+pos*200];
            }
            bytes32 h32 = hash200(data);
            if(allTokenIDHashes[pos]!= h32) {
                return (false);
            }
        }
        return (true);
    }

    function getTokenIDPosition(uint tokenID, uint[10000] calldata tokenIdArray) external pure returns (uint blockOf200, uint position) {
        for (uint j = 0; j < 100000; j++) {
            if (tokenID == tokenIdArray[j]) 
                return (j/200,j%200);
        }
        require(false,"Not Found");
    }

    function verifyTokenAt(uint256 position, uint[10000] calldata tokenIdArray) public view returns (bool) {
        require (position < 10000,"invalid position") ;
        if (position < 11) return true; // founder or first OG
        if (position == 100) return true; // First Alpha
        if (position == 1000) return true; // First Common
        uint tokenId = tokenIdArray[position];
        uint prevToken = tokenIdArray[position-1];
        require(_exists(tokenId),"Token does not exist");
        require(_exists(prevToken),"Prev Token does not exist");
        require(cardType(tokenId) == cardType(prevToken),"different types of card");
        if (traitAssignmentOrder[tokenId] > traitAssignmentOrder[prevToken]) return true;
        if (traitAssignmentOrder[tokenId] < traitAssignmentOrder[prevToken]) return false;
        return (tokenId > prevToken );
    }

    function revealTokenAt(uint256 hashBlock, uint256 hashBlockPos,uint256[200] memory _tokenIds, uint256[200] memory _traits) external {
        require(hash200(_tokenIds)==allTokenIDHashes[hashBlock],"IDs in wrong order");
        require(hash200(_traits)==traitHashes[hashBlock],"Traits in wrong order");
 
        uint tokenID = _tokenIds[hashBlockPos];
        require(ownerOf(tokenID) == msg.sender,"Not your token");
        if (cardTraits[tokenID] == 0) {
            cardTraits[tokenID] = _traits[hashBlockPos];
            emit TraitsClaimed(tokenID,_traits[hashBlockPos]);
        } else {
            emit TraitsAlreadyClaimed(tokenID);
        }       
    }

    event TraitSet(uint pos,uint256 tokenId, uint256 traits);

    function randomAvailable() public view returns (bool) {
        return (lastRandomRequested > lastRandomProcessed) && rng.isRequestComplete(randomRequests[lastRandomProcessed]);
    }

    function nextRandom() internal returns (uint256) {
        require(randomAvailable(),"Nothing to process");
        return rng.randomNumber(randomRequests[lastRandomProcessed++]);
    }

    function request_random() internal {
        randomRequests[lastRandomRequested++] = rng.requestRandomNumber();
    }

    function request_another_random() external onlyOwner {
        request_random();
    }

    // View Function to get graphic properties

    function isCardResolved(uint256 tokenId) public view returns (bool) {
        return traitAssignmentOrder[tokenId] > 0;
    }


    function fullTrait(uint256 tokenId) external view returns (uint256) {
        return cardTraits[tokenId];
    }

    function cardType(uint256 serial) public view returns(CardType) {
        if (!isCardResolved(serial)) return CardType.Unresolved;
        if (serial < oStart) return CardType.Founder;
        if (serial < aStart) return CardType.OG;
        if (serial < cStart) return CardType.Alpha;
        return CardType.Common;
    }

     function traitAssignment(uint256 tokenId) external view returns (uint256) {
        return traitAssignmentOrder[tokenId];
    }

  
    function OG_next() external view returns (uint256 left, uint256 nextPrice) {
        return CARD_next(og_stop, og_price, oSold,oPending,og_pointer);
    }

    function ALPHA_next() external view returns (uint256 left, uint256 nextPrice) {
            return CARD_next(alpha_stop, alpha_price, aSold,aPending,alpha_pointer);
    }
    function RANDOM_next() external view returns (uint256 left, uint256 nextPrice) {
            return CARD_next(common_stop, common_price, cSold,cPending,common_pointer);
    }

    function CARD_next(uint256[] storage stop, uint256[] memory price, uint256 sold, uint256 pending, uint256 pointer) internal view returns (uint256 left, uint256 nextPrice) {
        left = stop[pointer] - (sold + pending);
        if (pointer < stop.length - 1)
            nextPrice = price[pointer+1];
        else
            nextPrice = price[pointer];
    }

        function drain(xERC20 token) external onlyOwner {
        if (address(token) == 0x0000000000000000000000000000000000000000) {
            payable(owner()).transfer(address(this).balance);
        } else {
            token.transfer(owner(),token.balanceOf(address(this)));
        }
    }

    bool _FuzeBlown;
    // after finalization the images will be assigned to match the trait data
    // but due to onboarding more artists we will have a late assignment.
    // when it is proven OK we burn

    // should be of the format ipfs://<hash>/path
    function setDataFolder(string memory _baseURI) external onlyAllowed {
        require(!_FuzeBlown,"This data can no longer be changed");
        _setBaseURI(_baseURI);
    }

    function burnDataFolder() external onlyAllowed {
        _FuzeBlown = true;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        CardType ct = cardType(tokenId);
        if (ct == CardType.Unresolved) {
            return "https://temp.ether.cards/metadata/Unresolved";
        }
        // reformat to directory structure as below
        string memory folder = (tokenId % 100).toString(); 
        string memory file = tokenId.toString();
        string memory slash = "/";
        return string(abi.encodePacked(baseURI(),folder,slash,file,".json"));
    }

    /// 1 ... 10000
    /// 1 - 100 / 1 - 100

    function is_sale_on() external view returns (bool) {
        if (sale_is_over()) return false;
        if (block.timestamp < sale_start) return false;
        if (nextTokenId > cMax) return false;
        return true;
    }

    function TokenExists(uint tokenId) external view returns (bool) {
        return _exists(tokenId);
    }
    
    uint launch_date = 1616072400;

    function sale_is_over() public view returns (bool) {
        return (block.timestamp > sale_end);
    }
    
    function how_long_more() public view returns (uint Days, uint Hours, uint Minutes, uint Seconds) {
        require(block.timestamp < launch_date,"Missed It");
        uint gap = launch_date - block.timestamp;
        Days = gap / (24 * 60 * 60);
        gap = gap %  (24 * 60 * 60);
        Hours = gap / (60 * 60);
        gap = gap % (60 * 60);
        Minutes = gap / 60;
        Seconds = gap % 60;
        return (Days,Hours,Minutes,Seconds);
    }

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

        require(!paused(), "ERC721Pausable: token transfer while paused");
    }

    function pause() external onlyAllowed {
        _pause();
    }

    function unpause() external onlyAllowed {
        _unpause();
    }

}
function isCardResolved(uint256 tokenId) public view returns (bool) {
    return traitAssignmentOrder[tokenId] > 0;
}

// View Function to get graphic properties