contract Form is ERC721Enumerable, ReentrancyGuard, Ownable {
    
    struct Canvas {
        string[17] line;
        string metaDesc;
        bool isPainted;
    }
    
    Canvas[] private form;
    uint256 private maxLineLen = 35;
    uint256 private maxMetaDescLen = 280;
    
    function addForm(Canvas memory inputCanvas) private {
        require(getNumForm() < 10000, "Form is final");
        form.push(inputCanvas);
    }
    
    function getFormLine(uint256 tokenId, uint256 lineNum) public view returns (string memory) {
        require(tokenId > 0 && tokenId <= form.length, "Form has yet to enter the collective consciousness");
        require(lineNum > 0 && lineNum <= 17, "An ideal form has a max of 17 lines");
        string memory formLine = form[tokenId - 1].line[lineNum - 1];
        return formLine;
    }
    
    function getForm(uint256 tokenId) public view returns (string memory) {
        require(tokenId > 0 && tokenId <= form.length, "Form has yet to enter the collective consciousness");
        
        string memory fullForm = string(abi.encodePacked(getFormLine(tokenId,1), getFormLine(tokenId,2), getFormLine(tokenId,3), getFormLine(tokenId,4), getFormLine(tokenId,5), getFormLine(tokenId,6), getFormLine(tokenId,7)));
        fullForm = string(abi.encodePacked(fullForm, getFormLine(tokenId,8), getFormLine(tokenId,9), getFormLine(tokenId,10), getFormLine(tokenId,11), getFormLine(tokenId,12), getFormLine(tokenId,13)));
        fullForm = string(abi.encodePacked(fullForm, getFormLine(tokenId,14), getFormLine(tokenId,15), getFormLine(tokenId,16), getFormLine(tokenId,17)));
        
        return fullForm;
    }
    
    function getMetaDesc(uint256 tokenId) public view returns (string memory) {
        require(tokenId > 0 && tokenId <= form.length, "Form has yet to enter the collective consciousness");
        return form[tokenId - 1].metaDesc;
    }
    
    // the current Form owner may always wield the metadata
    function setMetaDesc(uint256 tokenId, string memory metadataDescription) public {
        require(_msgSender() == super.ownerOf(tokenId), "It is another's impression to make");
        require(bytes(metadataDescription).length <= maxMetaDescLen, string(abi.encodePacked("Metadata description may only contain ", toString(maxMetaDescLen), " bytes")));
        form[tokenId - 1].metaDesc = metadataDescription;
    }
    
    function getNumForm() public view returns (uint256 numForms) {
        return form.length;
    }
    
    function isPainted(uint256 tokenId) public view returns (bool painted) {
        return form[tokenId - 1].isPainted;
    }
    
    // a subsequently immutable Form
    function mintForm(string[17] memory line, string memory metadataDescription) public nonReentrant onlyOwner {
        require(bytes(line[0]).length <= maxLineLen && bytes(line[1]).length <= maxLineLen && bytes(line[2]).length <= maxLineLen && bytes(line[3]).length <= maxLineLen && 
        bytes(line[4]).length <= maxLineLen && bytes(line[5]).length <= maxLineLen && bytes(line[6]).length <= maxLineLen && bytes(line[7]).length <= maxLineLen && 
        bytes(line[8]).length <= maxLineLen && bytes(line[9]).length <= maxLineLen && bytes(line[10]).length <= maxLineLen && bytes(line[11]).length <= maxLineLen &&
        bytes(line[12]).length <= maxLineLen && bytes(line[13]).length <= maxLineLen && bytes(line[14]).length <= maxLineLen && bytes(line[15]).length <= maxLineLen && 
        bytes(line[16]).length <= maxLineLen, string(abi.encodePacked("Each line may only contain ", toString(maxLineLen), " bytes")));
        require(bytes(metadataDescription).length <= maxMetaDescLen, string(abi.encodePacked("Metadata description may only contain ", toString(maxMetaDescLen), " bytes")));
        
        _safeMint(_msgSender(), getNumForm() + 1);
        addForm(Canvas(line, metadataDescription, true));
    }
    
    // a blank Canvas
    function mintCanvas(string memory metadataDescription) public nonReentrant onlyOwner {
        _safeMint(_msgSender(), form.length + 1);
        string[17] memory lines;
        Canvas memory inputCanvas = Canvas(lines, metadataDescription, false);
        addForm(inputCanvas);
    }
    
    /** one may paint a blank Canvas
    * with immutable ink
    * for history and the evolution of the collective consciousness */
    function paintCanvas(uint256 tokenId, string[17] memory line, string memory metadataDescription) public nonReentrant {
        require(bytes(line[0]).length <= maxLineLen && bytes(line[1]).length <= maxLineLen && bytes(line[2]).length <= maxLineLen && bytes(line[3]).length <= maxLineLen && 
        bytes(line[4]).length <= maxLineLen && bytes(line[5]).length <= maxLineLen && bytes(line[6]).length <= maxLineLen && bytes(line[7]).length <= maxLineLen && 
        bytes(line[8]).length <= maxLineLen && bytes(line[9]).length <= maxLineLen && bytes(line[10]).length <= maxLineLen && bytes(line[11]).length <= maxLineLen &&
        bytes(line[12]).length <= maxLineLen && bytes(line[13]).length <= maxLineLen && bytes(line[14]).length <= maxLineLen && bytes(line[15]).length <= maxLineLen && 
        bytes(line[16]).length <= maxLineLen, string(abi.encodePacked("Each line may only contain ", toString(maxLineLen), " bytes")));
        require(bytes(metadataDescription).length <= maxMetaDescLen, string(abi.encodePacked("Metadata description may only contain ", toString(maxMetaDescLen), " bytes")));
        require(form[tokenId - 1].isPainted == false, "Form is no longer canvas");
        require(_msgSender() == super.ownerOf(tokenId), "It is another's impression to make");
        
        form[tokenId - 1].line = line;
        form[tokenId - 1].metaDesc = metadataDescription; 
        form[tokenId - 1].isPainted = true;
    }

    // full Form and metadata exist composably on chain
    function tokenURI(uint256 tokenId) override public view returns (string memory) {
        string[35] memory parts;
        parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';

        parts[1] = getFormLine(tokenId, 1);
        
        parts[2] = '</text><text x="10" y="40" class="base">';
        
        parts[3] = getFormLine(tokenId, 2);
        
        parts[4] = '</text><text x="10" y="60" class="base">';
        
        parts[5] = getFormLine(tokenId, 3);
        
        parts[6] = '</text><text x="10" y="80" class="base">';
        
        parts[7] = getFormLine(tokenId, 4);
        
        parts[8] = '</text><text x="10" y="100" class="base">';
        
        parts[9] = getFormLine(tokenId, 5);
        
        parts[10] = '</text><text x="10" y="120" class="base">';
        
        parts[11] = getFormLine(tokenId, 6);
        
        parts[12] = '</text><text x="10" y="140" class="base">';
        
        parts[13] = getFormLine(tokenId, 7);
        
        parts[14] = '</text><text x="10" y="160" class="base">';
        
        parts[15] = getFormLine(tokenId, 8);
        
        parts[16] = '</text><text x="10" y="180" class="base">';
        
        parts[17] = getFormLine(tokenId, 9);
        
        parts[18] = '</text><text x="10" y="200" class="base">';
        
        parts[19] = getFormLine(tokenId, 10);
        
        parts[20] = '</text><text x="10" y="220" class="base">';
        
        parts[21] = getFormLine(tokenId, 11);
        
        parts[22] = '</text><text x="10" y="240" class="base">';
        
        parts[23] = getFormLine(tokenId, 12);
        
        parts[24] = '</text><text x="10" y="260" class="base">';
        
        parts[25] = getFormLine(tokenId, 13);
        
        parts[26] = '</text><text x="10" y="280" class="base">';
        
        parts[27] = getFormLine(tokenId, 14);
        
        parts[28] = '</text><text x="10" y="300" class="base">';
        
        parts[29] = getFormLine(tokenId, 15);
        
        parts[30] = '</text><text x="10" y="320" class="base">';
        
        parts[31] = getFormLine(tokenId, 16);
        
        parts[32] = '</text><text x="10" y="340" class="base">';
        
        parts[33] = getFormLine(tokenId, 17);

        parts[34] = '</text></svg>';

        string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8]));
        output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13], parts[14], parts[15], parts[16]));
        output = string(abi.encodePacked(output, parts[17], parts[18], parts[19], parts[20], parts[21], parts[22], parts[23], parts[24]));
        output = string(abi.encodePacked(output, parts[25], parts[26], parts[27], parts[28], parts[29], parts[30], parts[31], parts[32]));
        output = string(abi.encodePacked(output, parts[33], parts[34]));
        
        string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Form #', toString(tokenId), '", "description": "', getMetaDesc(tokenId), '", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
        output = string(abi.encodePacked('data:application/json;base64,', json));

        return output;
    }
    
    function toString(uint256 value) internal pure returns (string memory) {
    // Inspired by OraclizeAPI's implementation - MIT license
    // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
    
    constructor() ERC721("Form", "FORM") Ownable() {}
}
/**
 * Full Form and metadata transparently queryable and composable on chain
 */
function setMetaDesc(uint256 tokenId, string memory metadataDescription) public {
    require(_msgSender() == super.ownerOf(tokenId), "It is another's impression to make");
    require(bytes(metadataDescription).length <= maxMetaDescLen, string(abi.encodePacked("Metadata description may only contain ", toString(maxMetaDescLen), " bytes")));
    form[tokenId - 1].metaDesc = metadataDescription;
}

// the current Form owner may always wield the metadata