contract GunToken is NoMintERC721, NoMintERC721Enumerable, OveridableERC721Metadata, Ownable {
    using strings for *;
    
    address internal factory;
    
    uint16 public constant maxAllocation = 4000;
    uint256 public lastAllocation = 0;
    
    event BatchTransfer(address indexed from, address indexed to, uint256 indexed batchIndex);
    
    struct Batch {
        address owner;
        uint16 size;
        uint8 category;
        uint256 startId;
        uint256 startTokenId;
    }
    
    Batch[] public allBatches;
    mapping(address => uint256) unactivatedBalance;
    mapping(uint256 => bool) isActivated;
    
    //Used for enumeration
    mapping(address => Batch[]) public batchesOwned;
    //Batch index to owner batch index
    mapping(uint256 => uint256) public ownedBatchIndex;
    
    mapping(uint8 => uint256) internal totalGunsMintedByCategory;
    uint256 internal _totalSupply;

    modifier onlyFactory {
        require(msg.sender == factory, "Not authorized");
        _;
    }

    constructor(address factoryAddress) public OveridableERC721Metadata("WarRiders Gun", "WRG") {
        factory = factoryAddress;
    }
    
    function categoryTypeToId(uint8 category, uint256 categoryId) public view returns (uint256) {
        for (uint i = 0; i < allBatches.length; i++) {
            Batch memory a = allBatches[i];
            if (a.category != category)
                continue;
            
            uint256 endId = a.startId + a.size;
            if (categoryId >= a.startId && categoryId < endId) {
                uint256 dif = categoryId - a.startId;
                
                return a.startTokenId + dif;
            }
        }
        
        revert();
    }
    
    /**
     * @dev Gets the token ID at a given index of the tokens list of the requested owner.
     * @param owner address owning the tokens list to be accessed
     * @param index uint256 representing the index to be accessed of the requested tokens list
     * @return uint256 token ID at the given index of the tokens list owned by the requested address
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) {
        return tokenOfOwner(owner)[index];
    }
    
    function getBatchCount(address owner) public view returns(uint256) {
        return batchesOwned[owner].length;
    }
    
    function getTokensInBatch(address owner, uint256 index) public view returns (uint256[] memory) {
        Batch memory a = batchesOwned[owner][index];
        uint256[] memory result = new uint256[](a.size);
        
        uint256 pos = 0;
        uint end = a.startTokenId + a.size;
        for (uint i = a.startTokenId; i < end; i++) {
            if (isActivated[i] && super.ownerOf(i) != owner) {
                continue;
            }
            
            result[pos] = i;
            pos++;
        }
        
        require(pos > 0);
        
        uint256 subAmount = a.size - pos;
        
        assembly { mstore(result, sub(mload(result), subAmount)) }
        
        return result;
    }
    
    function tokenByIndex(uint256 index) public view returns (uint256) {
        return allTokens()[index];
    }
    
    function allTokens() public view returns (uint256[] memory) {
        uint256[] memory result = new uint256[](totalSupply());
        
        uint pos = 0;
        for (uint i = 0; i < allBatches.length; i++) {
            Batch memory a = allBatches[i];
            uint end = a.startTokenId + a.size;
            for (uint j = a.startTokenId; j < end; j++) {
                result[pos] = j;
                pos++;
            }
        }
        
        return result;
    }
    
    function tokenOfOwner(address owner) public view returns (uint256[] memory) {
        uint256[] memory result = new uint256[](balanceOf(owner));
        
        uint pos = 0;
        for (uint i = 0; i < batchesOwned[owner].length; i++) {
            Batch memory a = batchesOwned[owner][i];
            uint end = a.startTokenId + a.size;
            for (uint j = a.startTokenId; j < end; j++) {
                if (isActivated[j] && super.ownerOf(j) != owner) {
                    continue;
                }
                
                result[pos] = j;
                pos++;
            }
        }
        
        uint256[] memory fallbackOwned = _tokensOfOwner(owner);
        for (uint i = 0; i < fallbackOwned.length; i++) {
            result[pos] = fallbackOwned[i];
            pos++;
        }
        
        return result;
    }
    
    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");

        return super.balanceOf(owner) + unactivatedBalance[owner];
    }
    
     function ownerOf(uint256 tokenId) public view returns (address) {
         require(exists(tokenId), "Token doesn't exist!");
         
         if (isActivated[tokenId]) {
             return super.ownerOf(tokenId);
         }
         uint256 index = getBatchIndex(tokenId);
         require(index < allBatches.length, "Token batch doesn't exist");
         Batch memory a = allBatches[index];
         require(tokenId < a.startTokenId + a.size);
         return a.owner;
     }
    
    function exists(uint256 _tokenId) public view returns (bool) {
        if (isActivated[_tokenId]) {
            return super._exists(_tokenId);
        } else {
            uint256 index = getBatchIndex(_tokenId);
            if (index < allBatches.length) {
                Batch memory a = allBatches[index];
                uint end = a.startTokenId + a.size;
                
                return _tokenId < end;
            }
            return false;
        }
    }
    
    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }
    
    function claimAllocation(address to, uint16 size, uint8 category) public onlyFactory returns (uint) {
        require(size < maxAllocation, "Size must be smaller than maxAllocation");
        
        allBatches.push(Batch({
            owner: to,
            size: size,
            category: category,
            startId: totalGunsMintedByCategory[category],
            startTokenId: lastAllocation
        }));
        
        uint end = lastAllocation + size;
        for (uint i = lastAllocation; i < end; i++) {
            emit Transfer(address(0), to, i);
        }
        
        lastAllocation += maxAllocation;
        
        unactivatedBalance[to] += size;
        totalGunsMintedByCategory[category] += size;
        
        _addBatchToOwner(to, allBatches[allBatches.length - 1]);
        
        _totalSupply += size;
        return lastAllocation;
    }
    
    function transferFrom(address from, address to, uint256 tokenId) public {
        if (!isActivated[tokenId]) {
            activate(tokenId);
        }
        super.transferFrom(from, to, tokenId);
    }
    
    function activate(uint256 tokenId) public {
        require(!isActivated[tokenId], "Token already activated");
        uint256 index = getBatchIndex(tokenId);
        require(index < allBatches.length, "Token batch doesn't exist");
        Batch memory a = allBatches[index];
        require(tokenId < a.startTokenId + a.size);
        isActivated[tokenId] = true;
        addTokenTo(a.owner, tokenId);
        unactivatedBalance[a.owner]--;
    }
    
    function getBatchIndex(uint256 tokenId) public pure returns (uint256) {
        uint256 index = (tokenId / maxAllocation);
        
        return index;
    }
    
    function categoryForToken(uint256 tokenId) public view returns (uint8) {
        uint256 index = getBatchIndex(tokenId);
        require(index < allBatches.length, "Token batch doesn't exist");
        
        Batch memory a = allBatches[index];
        
        return a.category;
    }
    
    function categoryIdForToken(uint256 tokenId) public view returns (uint256) {
        uint256 index = getBatchIndex(tokenId);
        require(index < allBatches.length, "Token batch doesn't exist");
        
        Batch memory a = allBatches[index];
        
        uint256 categoryId = (tokenId % maxAllocation) + a.startId;
        
        return categoryId;
    }
    
    function uintToString(uint v) internal pure returns (string memory) {
        if (v == 0) {
            return "0";
        }
        uint j = v;
        uint len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint k = len - 1;
        while (v != 0) {
            bstr[k--] = byte(uint8(48 + v % 10));
            v /= 10;
        }
        
        return string(bstr);
    }
    
    function tokenURI(uint256 tokenId) public view returns (string memory) {
        require(exists(tokenId), "Token doesn't exist!");
        if (isActivated[tokenId]) {
            return super.tokenURI(tokenId);
        } else {
            //Predict the token URI
            uint8 category = categoryForToken(tokenId);
            uint256 _categoryId = categoryIdForToken(tokenId);
            
            string memory id = uintToString(category).toSlice().concat("/".toSlice()).toSlice().concat(uintToString(_categoryId).toSlice().concat(".json".toSlice()).toSlice());
            string memory _base = "https://vault.warriders.com/guns/";
            
            //Final URL: https://vault.warriders.com/guns/<category>/<category_id>.json
            string memory _metadata = _base.toSlice().concat(id.toSlice());
            
            return _metadata;
        }
    }
    
    function addTokenTo(address _to, uint256 _tokenId) internal {
        //Predict the token URI
        uint8 category = categoryForToken(_tokenId);
        uint256 _categoryId = categoryIdForToken(_tokenId);
            
        string memory id = uintToString(category).toSlice().concat("/".toSlice()).toSlice().concat(uintToString(_categoryId).toSlice().concat(".json".toSlice()).toSlice());
        string memory _base = "https://vault.warriders.com/guns/";
            
        //Final URL: https://vault.warriders.com/guns/<category>/<category_id>.json
        string memory _metadata = _base.toSlice().concat(id.toSlice());
        
        super._addTokenTo(_to, _tokenId);
        super._setTokenURI(_tokenId, _metadata);
    }
    
    function ceil(uint a, uint m) internal pure returns (uint ) {
        return ((a + m - 1) / m) * m;
    }
    
    function _removeBatchFromOwner(address from, Batch memory batch) private {
        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).
        
        uint256 globalIndex = getBatchIndex(batch.startTokenId);

        uint256 lastBatchIndex = batchesOwned[from].length.sub(1);
        uint256 batchIndex = ownedBatchIndex[globalIndex];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (batchIndex != lastBatchIndex) {
            Batch memory lastBatch = batchesOwned[from][lastBatchIndex];
            uint256 lastGlobalIndex = getBatchIndex(lastBatch.startTokenId);

            batchesOwned[from][batchIndex] = lastBatch; // Move the last batch to the slot of the to-delete batch
            ownedBatchIndex[lastGlobalIndex] = batchIndex; // Update the moved batch's index
        }

        // This also deletes the contents at the last position of the array
        batchesOwned[from].length--;

        // Note that ownedBatchIndex[batch] hasn't been cleared: it still points to the old slot (now occupied by
        // lastBatch, or just over the end of the array if the batch was the last one).
    }
    
    function _addBatchToOwner(address to, Batch memory batch) private {
        uint256 globalIndex = getBatchIndex(batch.startTokenId);
        
        ownedBatchIndex[globalIndex] = batchesOwned[to].length;
        batchesOwned[to].push(batch);
    }
    
    function batchTransfer(uint256 batchIndex, address to) public {
        Batch storage a = allBatches[batchIndex];
        
        address previousOwner = a.owner;
        
        require(a.owner == msg.sender);
        
        _removeBatchFromOwner(previousOwner, a);
        
        a.owner = to;
        
        _addBatchToOwner(to, a);
        
        emit BatchTransfer(previousOwner, to, batchIndex);
        
        //Now to need to emit a bunch of transfer events
        uint end = a.startTokenId + a.size;
        uint256 unActivated = 0;
        for (uint i = a.startTokenId; i < end; i++) {
            if (isActivated[i]) {
                if (ownerOf(i) != previousOwner)
                    continue; //The previous owner didn't own this token, don't emit an event
            } else {
                unActivated++;
            }
            emit Transfer(previousOwner, to, i);
        }
        
        unactivatedBalance[to] += unActivated;
        unactivatedBalance[previousOwner] -= unActivated;
    }
}
/**
 * ERC-721 implementation that allows 
 * tokens (of the same category) to be minted in batches. Each batch
 * contains enough data to generate all
 * token ids inside the batch, and to
 * generate the tokenURI in the batch
 */
function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) {
    return tokenOfOwner(owner)[index];
}

/**
 * @dev Gets the token ID at a given index of the tokens list of the requested owner.
 * @param owner address owning the tokens list to be accessed
 * @param index uint256 representing the index to be accessed of the requested tokens list
 * @return uint256 token ID at the given index of the tokens list owned by the requested address
 */