contract PiRats is IPiRats, ERC721, Ownable, Pausable {

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

    Counters.Counter private totalMinted;
    Counters.Counter private totalBurned;

    uint256 public override maxTokens;  // 20,000
    uint256 public override paidTokens; // 5,000

    event CrewMinted(uint256 indexed tokenId);
    event CaptainMinted(uint256 indexed tokenId);
    event CrewBurned(uint256 indexed tokenId);

    mapping(uint256 => CrewCaptain) private tokenTraits;
    mapping(uint256 => uint256) private existingCombinations;
    mapping(address => uint16) private balanceCrew;
    mapping(address => uint16) private balanceCaptain;
    mapping(address => uint16) private totalLegendRank;

    mapping(address => bool) public admins;

    IPOTMTraits public traits;
    IBOOTY public booty;

    constructor(uint256 _maxTokens) ERC721("PiRats Of The Metaverse", "POTM") {
        _pause();
        maxTokens = _maxTokens;
        paidTokens = _maxTokens / 4;
    }
    /// MODIFIERS ///

    modifier requireContractsSet() {
        require(
            address(booty) != address(0) && 
            address(traits) != address(0), "Contracts not set");
      _;
    }

    /// MINTING ///

    function mintPiRat(address recipient, uint16 amount, uint256 seed) external override whenNotPaused {
        require(admins[_msgSender()], "Only admins - mintPiRat");
        _mintLoop(recipient, seed, amount);
        booty.initTimeStamp(recipient, block.timestamp);
    }

    function generate(address owner, uint256 tokenId, uint256 seed) internal returns (CrewCaptain memory t) {
        require(admins[_msgSender()], "Only admins - generate");
        t = traits.selectMintTraits(seed);        
        if (existingCombinations[structToHash(t)] == 0) {
            tokenTraits[tokenId] = t;
            existingCombinations[structToHash(t)] = tokenId;
            if(t.isCrew) {
                addToBalances(owner, tokenId); 
                emit CrewMinted(tokenId);
            }
            else {
                addToBalances(owner, tokenId); 
                emit CaptainMinted(tokenId);
            }
            return t;
        }
        return generate(owner, tokenId, seed);
    }
    /// WALK THE PLANK ///

    function plankPiRat(address recipient, uint16 amount, uint256 seed, uint256 _burnToken) external override whenNotPaused {
        require(admins[_msgSender()], "Only admins - mintPiRat");
        require(_isApprovedOrOwner(recipient, _burnToken), "Not your PiRat to burn!");
        _burn(_burnToken);
        totalBurned.increment();
        subToBalances(recipient, _burnToken);
        _mintLoop(recipient, seed, amount);
        emit CrewBurned(_burnToken);
    }

    function _mintLoop(address recipient, uint256 seed, uint256 amount) internal whenNotPaused {
        require(admins[_msgSender()], "Only admins - _mintLoop");
        uint16[] memory tokenIds = new uint16[](amount);
        for (uint i = 0; i < amount; i++) {
            totalMinted.increment();
            seed = random(seed);
            generatePlank(recipient, totalMinted.current(), random(seed));     
            tokenIds[i] = uint16(totalMinted.current());
            _safeMint(recipient, totalMinted.current());
        }
        booty.initTimeStamp(recipient, block.timestamp);
    }

    function generatePlank(address owner, uint256 tokenId, uint256 seed) internal returns (CrewCaptain memory t) {
        require(admins[_msgSender()], "Only admins - generatePlank");
        t = traits.selectPlankTraits(seed);
        if (existingCombinations[structToHash(t)] == 0) {
            tokenTraits[tokenId] = t;
            existingCombinations[structToHash(t)] = tokenId;
            if(t.isCrew) {
                addToBalances(owner, tokenId);
                emit CrewMinted(tokenId);
            }
            else {
                addToBalances(owner, tokenId);
                emit CaptainMinted(tokenId);
            }
            return t;
        }
        return generatePlank(owner, tokenId, random(seed));
    }

    /// TRAITS ///

    function getTokenTraits(uint256 tokenId) external view override  returns (CrewCaptain memory) {
        return tokenTraits[tokenId];
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "Token ID does not exist");
        return traits.tokenURI(tokenId);
    }

    /// SUPPLY ///

    function getTotalPiratsMinted() external view override returns(uint256 totalPiratsMinted) {
        totalPiratsMinted = totalMinted.current();
    }

    function getTotalPiratsBurned() external view override returns(uint256 totalPiratsBurned) {
        totalPiratsBurned = totalBurned.current();
    }

    function getTotalPirats() external view override returns(uint256 totalPirats) {
        totalPirats = (totalMinted.current() - totalBurned.current());
    }    

    function walletOfOwner(address owner) external view override returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(owner);
        uint256[] memory tokensId = new uint256[](tokenCount);
        uint256 currentTokenId = 1;
        uint256 ownedTokenIndex = 0;

        while (ownedTokenIndex < tokenCount && currentTokenId <= maxTokens) {
            address currentTokenOwner = ownerOf(currentTokenId);

            if (currentTokenOwner == owner) {
                tokensId[ownedTokenIndex] = currentTokenId;
                ownedTokenIndex++;
            }
            currentTokenId++;
        }
        return tokensId;
    }

    /// ADMIN ///

    function addAdmin(address addr) external onlyOwner {
        admins[addr] = true;
    }

    function removeAdmin(address addr) external onlyOwner {
        admins[addr] = false;
    }

    function setContracts(address _booty, address _traits) external onlyOwner {
        booty = IBOOTY(_booty);       
        traits = IPOTMTraits(_traits);
    }

    function withdraw() external onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }

    function setPaidTokens(uint256 _paidTokens) external onlyOwner {
        paidTokens = uint16(_paidTokens);
    }

    function setPaused(bool _paused) external onlyOwner {
        if (_paused) _pause();
        else _unpause();
    }

    /// INTERNAL ///

    function structToHash(CrewCaptain memory s) internal pure returns (uint256) {
        return uint256(keccak256(
            abi.encodePacked(
                s.isCrew,
                s.body,
                s.clothes,
                s.face,
                s.mouth,
                s.eyes,
                s.head,
                s.legendRank
            )
        ));
    }

    function random(uint256 seed) internal view returns (uint256) {
        require(admins[_msgSender()], "Only admins - random");
        return uint256(keccak256(abi.encodePacked(
        tx.origin,
        blockhash(block.number - 1),
        block.timestamp,
        seed
     )));
    }

    /// OVERRIDE ///

    function transferFrom(address from, address to, uint256 tokenId) public override(ERC721, IERC721) {
        IPiRats.CrewCaptain memory s = tokenTraits[tokenId];
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        if (s.isCrew) {
            booty.claimBooty(from);
            subToBalances(from, tokenId);
            booty.initTimeStamp(to, block.timestamp);
            addToBalances(to, tokenId);
        } else {
            booty.claimBooty(from);
            subToBalances(from, tokenId);
            booty.initTimeStamp(to, block.timestamp);
            addToBalances(to, tokenId);
        }
        ERC721.transferFrom(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override(ERC721, IERC721) {
        IPiRats.CrewCaptain memory s = tokenTraits[tokenId];
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        if (s.isCrew) {
            booty.claimBooty(from);
            subToBalances(from, tokenId);
            booty.initTimeStamp(to, block.timestamp);
            addToBalances(to, tokenId);
        } else {
            booty.claimBooty(from);
            subToBalances(from, tokenId);
            booty.initTimeStamp(to, block.timestamp);
            addToBalances(to, tokenId);
        }
        ERC721.safeTransferFrom(from, to, tokenId, data);
    }

    function isCrew(uint256 tokenId) external view override returns (bool) {
        IPiRats.CrewCaptain memory s = tokenTraits[tokenId];
        return s.isCrew;
    }

    function getBalanceCrew(address owner) external view override returns (uint16 _balanceCrew) {
        _balanceCrew = balanceCrew[owner];
    }

    function getBalanceCaptain(address owner) external view override returns (uint16 _balanceCaptain) {
        _balanceCaptain = balanceCaptain[owner];
    }

    function getTotalRank(address owner) external view override returns (uint256 _totalRank) {
        _totalRank = (totalLegendRank[owner]);
    }

    function getLegendRank(uint256 tokenId) public view returns (uint16 _legendRank) {
        IPiRats.CrewCaptain memory s = tokenTraits[tokenId];
        if (s.isCrew) {
            _legendRank = (uint16(s.legendRank) + 1);
        } else { 
            _legendRank = (uint16(s.legendRank) + 8);            
        }
    }

    function addToBalances(address owner, uint256 tokenId) internal {
        IPiRats.CrewCaptain memory s = tokenTraits[tokenId];
        uint16 _rank = getLegendRank(tokenId);
        totalLegendRank[owner] += _rank;
        if (s.isCrew) {
            balanceCrew[owner]++;
        } else { 
            balanceCaptain[owner]++;            
        }
    }

    function subToBalances(address owner, uint256 tokenId) internal {
        IPiRats.CrewCaptain memory s = tokenTraits[tokenId];
        uint16 _rank = getLegendRank(tokenId);
        totalLegendRank[owner] -= _rank;
        if (s.isCrew) {
            balanceCrew[owner]--;
        } else { 
            balanceCaptain[owner]--;            
        }
    }
}
////////////////////////////////
function getTotalPiratsMinted() external view override returns(uint256 totalPiratsMinted) {
    totalPiratsMinted = totalMinted.current();
}

/// SUPPLY ///