contract PunkPrintRegistry is CustomERC721Metadata, WhitelistedRole {
    using SafeMath for uint256;

    ///////////////
    // Variables //
    ///////////////
   

    PunkInterface public punkContract;
    
    string public tokenBaseURI;
    string public tokenBaseIpfsURI = "https://ipfs.infura.io/ipfs/";

    address payable public printerAddress;
    uint256 public pricePerPrintInWei;
    uint256 public pricePerNFCInWei;
    uint256 public pricePerMiscInWei;
    uint256 public pricePerPrintIntlShipInWei;
    uint256 public pricePerNFCIntlShipInWei;
    uint256 public pricePerMiscIntlShipInWei;
    
    string public descriptionMiscItem;
    
    uint256 public invocations = 0;

    mapping(bytes32 => uint256) public serialToTokenId;
    mapping(uint256 => bytes32) public tokenIdToSerial;
    mapping(uint256 => uint256) public tokenIdToPunkId;
    mapping(uint256 => string) public tokenIdToNFC;
    mapping(uint256 => string) public tokenIdToContactMethod;
    
    mapping(uint256 => string) public staticIpfsImageLink;


    ///////////////
    // Modifiers //
    ///////////////

    modifier onlyValidTokenId(uint256 _tokenId) {
        require(_exists(_tokenId), "Token ID does not exist");
        _;
    }

    /////////////////
    // Constructor //
    /////////////////

    constructor(address payable _printerAddress, uint256 _pricePerPrintInWei, uint256 _pricePerNFCInWei, uint256 _pricePerMiscInWei, uint256 _pricePerPrintIntlShipInWei, uint256 _pricePerNFCIntlShipInWei, uint256 _pricePerMiscIntlShipInWei, string memory _tokenBaseURI, string memory _tokenName, string memory _tokenSymbol) CustomERC721Metadata(_tokenName, _tokenSymbol) public {
        super.addWhitelisted(msg.sender);
        super.addWhitelisted(_printerAddress);

        printerAddress = _printerAddress;

        pricePerPrintInWei = _pricePerPrintInWei;
        pricePerNFCInWei = _pricePerNFCInWei;
        pricePerMiscInWei = _pricePerMiscInWei;
        
        pricePerPrintIntlShipInWei = _pricePerPrintIntlShipInWei;
        pricePerNFCIntlShipInWei = _pricePerNFCIntlShipInWei;
        pricePerMiscIntlShipInWei = _pricePerMiscIntlShipInWei;
        

        tokenBaseURI = _tokenBaseURI;
        punkContract = PunkInterface(0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB);
        
    }



    //////////////////////////////
    // Token Creation Functions //
    //////////////////////////////

  

    function purchasePrint(uint256 _punkId, string memory _contactMethod) public payable returns (uint256 _tokenId) {
        require(msg.value == pricePerPrintInWei || msg.value == pricePerPrintIntlShipInWei, "Must send correct amount for a print. Please check price @ pricePerPrintInWei or pricePerPrintIntlShipInWei for international shipments.");
        require(msg.sender == punkContract.punkIndexToAddress(_punkId), "You must own punk");
        
        uint256 tokenId = _mintToken(msg.sender);
        
        tokenIdToPunkId[tokenId] = _punkId;
        tokenIdToContactMethod[tokenId] = _contactMethod;
        printerAddress.transfer(msg.value);
        
        return tokenId;
    }
    
    
    function purchaseNFCOnly(uint256 _punkId, string memory _contactMethod) public payable returns (uint256 _tokenId) {
        require(msg.value == pricePerNFCInWei || msg.value == pricePerNFCIntlShipInWei, "Must send correct amount for an NFC. Please check price @ pricePerNFCInWei or pricePerNFCIntlShipInWei for international shipments.");
        require(msg.sender == punkContract.punkIndexToAddress(_punkId), "You must own punk");
        
        uint256 tokenId = _mintToken(msg.sender);
        
        tokenIdToPunkId[tokenId] = _punkId;
        tokenIdToContactMethod[tokenId] = _contactMethod;
        printerAddress.transfer(msg.value);
        
        return tokenId;
    }
    
    function purchaseMisc(uint256 _punkId, string memory _contactMethod) public payable returns (uint256 _tokenId) {
        bytes memory tempDescriptionMiscItem = bytes(descriptionMiscItem); // Uses memory
        require(msg.value == pricePerMiscInWei || msg.value == pricePerMiscIntlShipInWei, "Must send correct amount. Please check price @ pricePerMiscInWei or pricePerMiscIntlShipInWei for international shipments.");
        require(tempDescriptionMiscItem.length > 0, "Misc item not available.");
        require(msg.sender == punkContract.punkIndexToAddress(_punkId), "You must own punk");
        
        uint256 tokenId = _mintToken(msg.sender);
        
        tokenIdToPunkId[tokenId] = _punkId;
        tokenIdToContactMethod[tokenId] = _contactMethod;
        printerAddress.transfer(msg.value);
        
        return tokenId;
    }
    
    function mint(uint256 _punkId, string memory _nfc) public onlyWhitelisted returns (uint256 _tokenId) {
        uint256 tokenId = _mintToken(punkContract.punkIndexToAddress(_punkId));
        tokenIdToPunkId[tokenId] = _punkId;
        tokenIdToNFC[tokenId]= _nfc;

        return tokenId;
    }

    function _mintToken(address _to) internal returns (uint256 _tokenId) {

        invocations = invocations.add(1); // next token ID
        bytes32 serial = keccak256(abi.encodePacked(invocations, block.number, msg.sender));

        _mint(_to, invocations);

        serialToTokenId[serial] = invocations;
        tokenIdToSerial[invocations] = serial;

        return invocations;
    }

  

    //////////////////////////
    // Management functions //
    //////////////////////////

    
    function updatePrinterAddress(address payable _printerAddress) public onlyWhitelisted returns (bool) {
        printerAddress = _printerAddress;
        return true;
    }
    

    function updatePricePerPrintInWei(uint256 _pricePerPrintInWei) public onlyWhitelisted returns (bool) {
        pricePerPrintInWei = _pricePerPrintInWei;
        return true;
    }
    
    function updatePricePerNFCInWei(uint256 _pricePerNFCInWei) public onlyWhitelisted returns (bool) {
        pricePerNFCInWei = _pricePerNFCInWei;
        return true;
    }
    
    function updatePricePerMiscInWei(uint256 _pricePerMiscInWei) public onlyWhitelisted returns (bool) {
        pricePerMiscInWei = _pricePerMiscInWei;
        return true;
    }
    
    function updatePricePerPrintIntlShipInWei(uint256 _pricePerPrintIntlShipInWei) public onlyWhitelisted returns (bool) {
        pricePerPrintIntlShipInWei = _pricePerPrintIntlShipInWei;
        return true;
    }
    
    function updatePricePerNFCIntlShipInWei(uint256 _pricePerNFCIntlShipInWei) public onlyWhitelisted returns (bool) {
        pricePerNFCIntlShipInWei = _pricePerNFCIntlShipInWei;
        return true;
    }
    
    function updatePricePerMiscIntlShipInWei(uint256 _pricePerMiscIntlShipInWei) public onlyWhitelisted returns (bool) {
        pricePerMiscIntlShipInWei = _pricePerMiscIntlShipInWei;
        return true;
    }
    
    function updateDescriptionMiscItem(string memory _descriptionMiscItem) public onlyWhitelisted returns (bool) {
        descriptionMiscItem = _descriptionMiscItem;
        return true;
    }
   
    function updateTokenBaseURI(string memory _newBaseURI) public onlyWhitelisted returns (bool) {
        tokenBaseURI = _newBaseURI;
        return true;
    }

    function updateTokenBaseIpfsURI(string memory _tokenBaseIpfsURI) public onlyWhitelisted returns (bool) {
        tokenBaseIpfsURI = _tokenBaseIpfsURI;
        return true;
    }

    function overrideDynamicImageWithIpfsLink(uint256 _tokenId, string memory _ipfsHash) public onlyWhitelisted returns (bool) {
        staticIpfsImageLink[_tokenId] = _ipfsHash;
        return true;
    }

    function clearIpfsImageUri(uint256 _tokenId) public onlyWhitelisted returns (bool) {
        delete staticIpfsImageLink[_tokenId];
        return true;
    }

    ////////////////////////
    // Accessor functions //
    ////////////////////////

    function updateTokenPunkId(uint256 _tokenId, uint256 _punkId) public onlyWhitelisted returns (bool) {
        require(_exists(_tokenId), "Token must exist");

        tokenIdToPunkId[_tokenId] = _punkId;
        return true;
    }
    
    function updateTokenContactMethod(uint256 _tokenId, string memory _contactMethod) public returns (bool) {
        require(_exists(_tokenId), "Token must exist");
        require(msg.sender == ownerOf(_tokenId) || isWhitelisted(msg.sender), "You must have permission.");
        tokenIdToContactMethod[_tokenId] = _contactMethod;
        return true;
    }
    
    function updateTokenNFC(uint256 _tokenId, string memory _NFCUID) public onlyWhitelisted returns (bool) {
        require(_exists(_tokenId), "Token must exist");
        tokenIdToNFC[_tokenId] = _NFCUID;
        return true;
    }
    

    
    function tokensOfOwner(address owner) external view returns (uint256[] memory) {
        return _tokensOfOwner(owner);
    }
    

    function tokenURI(uint256 _tokenId) external view onlyValidTokenId(_tokenId) returns (string memory) {
        // If we have an override then use it
        if (bytes(staticIpfsImageLink[_tokenId]).length > 0) {
            return Strings.strConcat(tokenBaseIpfsURI, staticIpfsImageLink[_tokenId]);
        }

        return Strings.strConcat(tokenBaseURI, Strings.uint2str(_tokenId));
    }
}
function updateTokenPunkId(uint256 _tokenId, uint256 _punkId) public onlyWhitelisted returns (bool) {
    require(_exists(_tokenId), "Token must exist");

    tokenIdToPunkId[_tokenId] = _punkId;
    return true;
}

////////////////////////