// Contract: PooperRenderer
// Target function: function formatStyle(Elements[] memory eltss, bytes32[] memory ids) external returns(bytes memory)
        
        ret[retIdx] = bytes1(uint8(FLAG_NONZERO));
        length = inputLength-zs.end;
        retIdx = _setLength(ret, retIdx, length);
        for (uint256 i = zs.end; i < inputLength; ++i) {
            ret[retIdx++] = input[i];
        }
        if (retIdx < input.length) { // compression was favorable
            ret[retIdx++] = bytes1(uint8(FLAG_IS_COMPRESSED));
            assembly {
                mstore(ret, retIdx)
            }
        } else { // compression was NOT an improvement
            ret = new bytes(inputLength+1);
            assembly {
                mstore(ret, 0)
            }
            _append(ret, input);
            assembly {
                mstore(ret, add(inputLength, 1))
            }
            ret[inputLength] = bytes1(uint8(FLAG_IS_NOT_COMPRESSED));
        }
    } 

    function decompressZeros(bytes memory input) internal pure returns(bytes memory ret) {
        // assumes input is output of `compressZeros`
        uint256 inputLength = input.length;
        require(inputLength > 0, "invalid input len.");
        uint256 isCompressed = uint256(uint8(input[inputLength-1]));
        assembly {
            mstore(input, sub(mload(input), 1))
        }
        if (isCompressed == FLAG_IS_NOT_COMPRESSED) return input;
        --inputLength;

        uint256 totalLength;
        uint256 length;
        uint256 idx;
        uint256 flag;
        
        while (idx < inputLength) {
            flag = uint256(uint8(input[idx])) & FLAG_ZERO_MASK;
            (length, idx) = _getLength(input, idx);
            if (length == 0) {
                continue;
            }
            totalLength += length; 
            if (flag == FLAG_NONZERO){
                idx += length;
            }
        }
        idx = 0;
        ret = new bytes(totalLength);
        uint256 retIdx;
        while (idx < inputLength) {
            flag = uint256(uint8(input[idx])) & FLAG_ZERO_MASK;
            (length, idx) = _getLength(input, idx);
            if (length == 0) {
                continue;
            }
            if (flag == FLAG_ZERO) {
                retIdx += length;
                continue;
            } else if (flag == FLAG_NONZERO){
                for (uint256 i; i < length; ++i) {
                    ret[retIdx++] = input[i+idx]; 
                }
                idx += length; 
            } else {
                revert("should not happen :(");
            }
        }
    }
    
    function _setLength(bytes memory arr, uint256 arrIdx, uint256 length) private pure returns(uint256) {
        uint256 bound;
        if (length < type(uint8).max) {
            arr[arrIdx++] |= bytes1(uint8(FLAG_LENGTH_UINT8));
            bound = 1;
        } else if (length < type(uint16).max) {
            arr[arrIdx++] |= bytes1(uint8(FLAG_LENGTH_UINT16));
            bound = 2;
        } else if (length < type(uint32).max) { 
            arr[arrIdx++] |= bytes1(uint8(FLAG_LENGTH_UINT32));
            bound = 4;
        } else if (length < type(uint64).max) { 
            arr[arrIdx++] |= bytes1(uint8(FLAG_LENGTH_UINT64));
            bound = 8;
        } else {
            revert("_setLength:unsupportedLength");
        }
        for (uint256 i; i < bound; ++i) {
            arr[arrIdx++] = bytes1(uint8(length >> (8*i)));
        }
        return arrIdx;
    }

    function _getLength(bytes memory arr, uint256 arrIdx) private pure returns(uint256, uint256) {
        uint256 flagLength = uint256(uint8(arr[arrIdx++])) & FLAG_LENGTH_MASK; 
        uint256 bound;
        if (flagLength == FLAG_LENGTH_UINT8) {
            bound = 1;
        } else if (flagLength == FLAG_LENGTH_UINT16) {
            bound = 2;
        } else if (flagLength == FLAG_LENGTH_UINT32) { 
            bound = 4;
        } else if (flagLength == FLAG_LENGTH_UINT64) { 
            bound = 8;
        } else {
            revert("_getLength:unsupportedLength");
        }
        uint256 length;
        for (uint256 i; i < bound; ++i) {
            length |= uint256(uint8(arr[arrIdx++])) << (8*i);
        }
        return (length, arrIdx);
    }
    
    // cheaper than bytes concat :)
    function _append(bytes memory dst, bytes memory src) private view {
      
        assembly {
            // resize

            let priorLength := mload(dst)
            
            mstore(dst, add(priorLength, mload(src)))
        
            // copy    

            pop(
                staticcall(
                  gas(), 4, 
                  add(src, 32), // src data start
                  mload(src), // src length 
                  add(dst, add(32, priorLength)), // dst write ptr
                  mload(dst)
                ) 
            )
        }
    }
}

contract PooperRenderer is IRenderer, ImageScripterSSTORE2, Ownable {

    mapping(address => bool) internal _hasCallerRole;
    mapping(address => bytes32) internal _contractLink;

    constructor() {
        _hasCallerRole[owner()] = true;
    }

    function _onlyOwner() internal view {
        require(msg.sender == owner(), "not owner.");
    }

    function _onlyHasCallerRole() private view {
        require(_hasCallerRole[msg.sender], "msg.sender !_hasCallerRole.");
    }

    function _onlyLinkedContract() private view {
        require(bytes(abi.encodePacked(_contractLink[msg.sender])).length > 0, "caller not linked.");
    }

    function grantCallerRole(address account) external {
        _onlyOwner();
        _grantCallerRole(account);
    }

    function _grantCallerRole(address account) private {
        _hasCallerRole[account] = true;
    }

    function linkContract(address contract_, bytes32 imageScriptPtrName) external {
        _onlyOwner();
        _linkContract(contract_, imageScriptPtrName);
    }
    
    function _linkContract(address contract_, bytes32 imageScriptPtrName) private {
        _contractLink[contract_] = imageScriptPtrName;
    }

    string constant SVG_HEADER = ""
"    <svg version=\"1.1\""
"     baseProfile=\"full\""
"     viewBox=\"0 0 100 100\""
"     xmlns=\"http://www.w3.org/2000/svg\""
"     >"
     "";

    string constant SVG_FOOTER = "</svg>";

    function _getSeed(uint256 tokenId) private view returns(uint256) {
        address caller = msg.sender;
        return uint256(keccak256(abi.encodePacked(tokenId, caller)));
    }

    function getMaxDistinctTokens() external returns(uint256) {
        _onlyHasCallerRole();
        _onlyLinkedContract();
        address caller = msg.sender;
        return _getMaxDistinctTokens(_contractLink[msg.sender]);
    }

    function getMaxDistinctTokens(bytes32 name) external returns(uint256) {
        _onlyOwner();
        return _getMaxDistinctTokens(name); 
    }

    function _getMaxDistinctTokens(bytes32 name) private returns(uint256 tally) {
        ImageScript memory script = abi.decode(_read(_imageScriptPtr[name]), (ImageScript));
        uint256 elementIterPtrsLength = script.styleIterPtrs.length;
        Elements memory elements;
        tally = 1;
        for (uint256 i; i < elementIterPtrsLength; ++i) {
            elements = abi.decode(_read(script.styleIterPtrs[i]), (Elements));
            tally *= _getMaxDistinctTokens(elements);
        }
        elementIterPtrsLength = script.elementIterPtrs.length;
        for (uint256 i; i < elementIterPtrsLength; ++i) {
            elements = abi.decode(_read(script.elementIterPtrs[i]), (Elements));
            tally *= _getMaxDistinctTokens(elements);
        }
    }

    function _getMaxDistinctTokens(Elements memory e) private returns(uint256 tally) {
        Element memory element;
        uint256 elementsLength = e.elements.length;
        tally = elementsLength; // this is the most crucial line in this count
        for (uint256 i; i < elementsLength; ++i) {
            element = e.elements[i];
            tally *= (e.fills.length > 0) ? e.fills.length : 1;

            tally *= (element.groupingOrPath) ? _getMaxDistinctTokens(element) : 1;
        }
    }

    function _getMaxDistinctTokens(Element memory e) private returns(uint256) {
        if (e.groupingOrPath) {
            address[] memory toGroupPtrs = abi.decode(e.d, (address[]));
            
            uint256 toGroupPtrsLength = toGroupPtrs.length;
            Elements memory elts;
            uint256 tally = 1;
            for (uint256 i; i < toGroupPtrsLength; ++i) {
                elts = abi.decode(_read(toGroupPtrs[i]), (Elements));
                tally *= _getMaxDistinctTokens(elts);
            }
            return tally;
        }
        revert("_getMaxDistinctTokens: only for groupings.");
    }

    function renderAttributes(uint256 tokenId) external view returns(string memory) {
        _onlyHasCallerRole();
        _onlyLinkedContract();
        
        ImageScript memory script = abi.decode(_read(_imageScriptPtr[_contractLink[msg.sender]]), (ImageScript));

        uint256 seed = _getSeed(tokenId);
        return _getAttributes(script, seed); // "csv", so whomever ingests may need to ignore last comma
    }

    // encoding is done by caller
    function renderSVG(uint256 tokenId) external view returns(string memory) {
        _onlyHasCallerRole();
        _onlyLinkedContract();
        return string(_renderSVG(tokenId)); 
    }

    function _renderSVG(uint256 tokenId) private view returns(bytes memory) {

        ImageScript memory script = abi.decode(_read(_imageScriptPtr[_contractLink[msg.sender]]), (ImageScript));

        uint256 seed = _getSeed(tokenId);

        return abi.encodePacked(
            SVG_HEADER,
            _getStyles(script, seed),
            _getPaths(script, seed),
            SVG_FOOTER 
        );
    }

    function _getStyles(ImageScript memory script, uint256 seed) private view returns(string memory) {
        require(script.motionPtr != address(0), "motionPtr not set.");
        string memory style;
        uint256 styleIterPtrsLength = script.styleIterPtrs.length;
        Elements memory e;
        Element[] memory elts;
        for (uint256 i; i < styleIterPtrsLength; ++i) {
            e = abi.decode(_read(script.styleIterPtrs[i]), (Elements));
            elts = e.elements;
            style = string(abi.encodePacked(style, _choose(elts, seed, e.id, "element").d));
        }
        return string(abi.encodePacked("<style>",
                      _read(script.motionPtr),
                      style, 
                      "</style>"));    
    }

    function _getPaths(ImageScript memory script, uint256 seed) private view returns(string memory ret) {
        uint256 elementIterPtrsLength = script.elementIterPtrs.length;
        for (uint256 i; i < elementIterPtrsLength; ++i) {
            Elements memory elements = abi.decode(_read(script.elementIterPtrs[i]), (Elements));
            ret = string(abi.encodePacked(ret, _toString(elements, seed)));
        }
    }

    function _toString(Elements memory e, uint256 seed) private view returns(string memory ret) {
      
        Element memory element = _choose(e.elements, seed, e.id, "element");
        bool groupingOrPath = element.groupingOrPath;
        (string memory prefix, 
         string memory prePostfix, 
         string memory postfix) = (groupingOrPath) ? ("<g ", ">", "</g>") : ("<path ", "", "/>"); 
        return string(abi.encodePacked(
            prefix,
            "id=\"", _toString(e.id), 
            "\" class=\"", e.class,
            "\" fill=\"", _toString(_choose(e.fills, seed, e.id, "fill")), "\" ",
            prePostfix,
            _toString(element, seed),
            postfix
        ));
    }

    // can this be more efficient??
    function _toString(bytes32 b) private pure returns(string memory ret) {
        if (b == 0x0) return "";
        ret = string(abi.encodePacked(b)); 
        unchecked{
        for (uint256 i = 1; i < 32; ++i) {
            if (bytes(ret)[i] == 0x0) {
                assembly {
                    mstore(ret, i)
                } 
                break;
            }
        } 
        }//uc
    }

    function _toString(Element memory e, uint256 seed) private view returns(string memory) {
        if (e.groupingOrPath) {
            address[] memory toGroupPtrs = abi.decode(e.d, (address[]));
            
            string memory ret;
            uint256 toGroupPtrsLength = toGroupPtrs.length;
            Elements memory elts;
            for (uint256 i; i < toGroupPtrsLength; ++i) {
                elts = abi.decode(_read(toGroupPtrs[i]), (Elements));
                ret = string(abi.encodePacked(ret, _toString(elts, seed))); 
            }
            return ret;
        }
        return string(abi.encodePacked("d=\"", e.d, "\" "));
    }

    // opensea compatibility
    // note: using _toString() for bytes32 elements is CRUCIAL for decoding in the browser
    function _traitTypeString(bytes32 traitType, bytes32 value) private pure returns(string memory) {
        return string(abi.encodePacked(
          "{\"trait_type\": \"", 
          _toString(traitType), 
          "\", \"value\": \"", 
          _toString(value),"\"}"
        )); 
    }
    
    function _traitTypeString(string memory traitType, bytes32 value) private pure returns(string memory) {
        return string(abi.encodePacked(
            "{\"trait_type\":\"", traitType, 
            "\",\"value\":\"", _toString(value), "\"}"
        )); 
    }

    function _toAttributeString(Elements memory e, uint256 seed) private view returns(string memory ret) {
        Element memory element = _choose(e.elements, seed, e.id, "element");
        if (element.name != 0x0)
            ret = string(abi.encodePacked(_traitTypeString(e.id, element.name), ","));
        if (e.fills.length > 1) 
            ret = string(abi.encodePacked(
                            ret,
                            _traitTypeString(
                            string(abi.encodePacked(_toString(e.id), "-fill")), 
                            _choose(e.fills, seed, e.id, "fill")), 
                            ","));

        bool groupingOrPath = element.groupingOrPath;
        if (groupingOrPath) 
            ret = string(abi.encodePacked(ret, _toAttributeString(element, seed)));

    }

    function _toAttributeString(Element memory e, uint256 seed) private view returns(string memory ret) {
        if (e.groupingOrPath) {
            address[] memory toGroupPtrs = abi.decode(e.d, (address[]));
            
            uint256 toGroupPtrsLength = toGroupPtrs.length;
            Elements memory elts;
            for (uint256 i; i < toGroupPtrsLength; ++i) {
                elts = abi.decode(_read(toGroupPtrs[i]), (Elements));
                ret = string(abi.encodePacked(ret, _toAttributeString(elts, seed))); 
            }
            return ret;
        }
        revert("_toAttributeString: only for groupings.");
    }

    function _getAttributes(ImageScript memory script, uint256 seed) private view returns(string memory ret) {
        uint256 elementIterPtrsLength = script.styleIterPtrs.length;
        Elements memory elements;
        for (uint256 i; i < elementIterPtrsLength; ++i) {
            elements = abi.decode(_read(script.styleIterPtrs[i]), (Elements));
            ret = string(abi.encodePacked(ret, _toAttributeString(elements, seed)));
        }
        elementIterPtrsLength = script.elementIterPtrs.length;
        for (uint256 i; i < elementIterPtrsLength; ++i) {
            elements = abi.decode(_read(script.elementIterPtrs[i]), (Elements));
            ret = string(abi.encodePacked(ret, _toAttributeString(elements, seed)));
        }
    }

    function _choose(Element[] memory e, uint256 seed, bytes32 id, string memory name) private pure returns(Element memory) {
        seed = uint256(keccak256(abi.encode(seed, id, name))); // not packed :)
        return e[seed % e.length]; // e should NEVER have length 0
    }

    function _choose(bytes32[] memory e, uint256 seed, bytes32 id, string memory name) private pure returns(bytes32) {
        if (e.length == 0) return ""; // this is used for "fill" and things that can be "null"
        seed = uint256(keccak256(abi.encode(seed, id, name))); // not packed :)
        return e[seed % e.length];
    }

    // hub stuff

    function _setImageScript(bytes memory compressedImageScript, bytes32 name) private {
        _imageScriptPtr[name] = SSTORE2.write(compressedImageScript);
    }

    // motion
    function setMotion(bytes calldata compressedBStyle, bytes32 id) external {
        _onlyOwner();
        _id2StringPtr[id] = SSTORE2.write(compressedBStyle);
    }

    function getStringPtr(bytes32 id) external view returns(address) {
        _onlyOwner();
        return _getStringPtr(id);
    }

    function _getStringPtr(bytes32 id) private view returns(address) {
        return _id2StringPtr[id];
    }