contract Immutables is ImmutablesAdminPostPage, ImmutablesOptionalMetadataServer, ERC721, IERC2981 {
    using Strings for uint256;
    using Address for address payable;

    /// @dev GLOBAL VARIABLES
    /// @dev The total suppliy of tokens (Pages).
    uint256 public maxTotalSupply;
    /// @dev The last tokenId minted.
    uint256 public currentTokenId;

    /// @dev The metadata description provided by the contract for all tokens.
    string public globalMetadataDescription;

    /// @dev Template Cloneable Royalty Manager Contract
    ImmutablesPageRoyaltyManager public implementation;

    /// @dev Mappings between the page string and tokenId.
    mapping(string => uint256) public pageToTokenId;
    mapping(uint256 => string) public tokenIdToPage;

    mapping(uint256 => address) public tokenIdToRoyaltyAddress;
    mapping(uint256 => uint16) public tokenIdToSecondaryRoyaltyPercent;

    /// @dev EVENTS

    event PaymentReceived(address from, uint256 amount);

    event AddressReservedPage(address indexed owner, uint256 tokenId, string page, address manager);

    // @dev CONSTRUCTOR

    constructor() ERC721("Immutables", "][") ImmutablesOptionalMetadataServer() {
      postingFee = 0 ether;
      pageFee = 0 ether;

      maxTotalSupply = ~uint256(0);
      currentTokenId = 0;

      curator = address(0);
      curatorPercent = 0;

      beneficiary = address(0);
      beneficiaryPercent = 0;

      contractTipPercentage = 250; //  2.50%
      posterTipPercentage = 1000;  // 10.00%
      contractSecondaryRoyaltyPercentage = 1000; // 10.00%

      globalMetadataDescription = "Immutables are NFTs stored completely on the Ethereum Blockchain.";

      isTeammember[msg.sender] = true;
      emit AdminModifiedTeammembers(msg.sender, true);

      implementation = new ImmutablesPageRoyaltyManager();
      implementation.initialize(address(this), 1, "I", address(this), 10000, address(0), 0);
    }

    /// @dev FINANCIAL

    receive() external payable virtual {
        emit PaymentReceived(_msgSender(), msg.value);
    }

    // @dev HELPER FUNCTIONS

    function _baseURI() internal view override returns (string memory) {
      return immutablesURI;
    }

    // @dev DATA INSERTION: TEXT

    /** @dev Allows anyone to log data into the contract with a page _page, optional citation _tag, and content.
      * @param _page A page value (like an account)
      * @param _tag A citation to other data (like a parent post, another transaction, etc.)
      * @param _content A content string to log into the page with the optional citation.
      */
    function anyonePostPageTagContent(string calldata _page, string calldata _tag, string calldata _content) external payable {
        require(msg.value >= postingFee, "posting fee");
        require(bytes(_page).length > 0, "page name");
        if(userScreeningEnabled) {
          // If user screening is enabled,
          //    the user must be an allowed user, or the owner of the page, and
          //    the user must be posting in a public page, or the page they own.
          require(
            (isAllowedUser[msg.sender] || ownerOf(pageToTokenId[_page]) == msg.sender) &&
            (!this.isValidPageName(_page) || ownerOf(pageToTokenId[_page]) == msg.sender)
          , "auth");
        } else {
          // If user screening is not enabled,
          //    Anyone can post in a public page, or
          //    a page that they own.
          require(!this.isValidPageName(_page) || ownerOf(pageToTokenId[_page]) == msg.sender, "auth");
        }
        bytes32 _pageHash = keccak256(abi.encodePacked(_page));
        bytes32 _tagHash = keccak256(abi.encodePacked(_tag));

        emit AddressPostedPageTagContent(msg.sender, _pageHash, _tagHash, _page, _tag, _content);
    }

    // @dev DATA INSERTION: VALUE

    /** @dev Allows anyone to pay into the contract with a page _page, optional post transaction Hash, and poster address.
      * @dev Payment is split between page owner, poster, and contract.
      * @param _page An optional page value.
      * @param _postTransactionHash An optional citation to a post transaction hash.
      * @param _poster The address of the poster of the post to tip.
      */
    function anyonePayPagePostPosterValue(string calldata _page, string calldata _postTransactionHash, address _poster) external payable nonReentrant() {
        // checks
        require(msg.value > 0, "value");
        require(bytes(_postTransactionHash).length == 66, "txhash");

        // effects
        bytes32 _pageHash = "";
        bytes32 _tagHash = keccak256(abi.encodePacked('V',_postTransactionHash));
        string memory _tag = string(abi.encodePacked('V',_postTransactionHash));

        if(bytes(_page).length == 0) {
          // If there is no page specified
          uint256 _contractValue = msg.value * contractTipPercentage / 10000;
          uint256 _posterValue = msg.value - _contractValue;
          // interaction: pay the poster
          payable(_poster).sendValue(_posterValue);
        } else {
          // If a page is specified
          address _pageOwner = ownerOf(pageToTokenId[_page]);
          _pageHash = keccak256(abi.encodePacked(_page));

          uint256 _contractValue = msg.value * contractTipPercentage / 10000;
          uint256 _posterValue = msg.value * posterTipPercentage / 10000;
          uint256 _pageOwnerValue = msg.value - _contractValue - _posterValue;

          // interaction: pay the page owner, and pay the poster
          payable(_pageOwner).sendValue(_pageOwnerValue);
          payable(_poster).sendValue(_posterValue);
        }
        emit AddressPaidPageTagValue(msg.sender, _pageHash, _tagHash, _page, _tag, msg.value);
    }

    // @dev RESERVE PAGE

    /** @dev Allows anyone to reserve an unclaimed reservable page.
      * @param _page A page name to reserve.
      */
    function anyoneReserveUnclaimedPage(string calldata _page) external payable onlyAllowedUser() {
      require(msg.value >= pageFee, "page fee");
      require(currentTokenId < maxTotalSupply, "sold out");
      require(isValidPageName(_page), "invalid name");
      require(pageToTokenId[_page] == 0, "page taken");

      currentTokenId++;
      uint256 _newTokenId = currentTokenId;
      _mint(msg.sender, _newTokenId);

      pageToTokenId[_page] = _newTokenId;
      tokenIdToPage[_newTokenId] = _page;

      setupImmutablesPageRoyaltyManagerForTokenId(_newTokenId);

      emit AddressReservedPage(msg.sender, _newTokenId, _page, tokenIdToRoyaltyAddress[_newTokenId]);
    }

    /** @dev Clones a Royalty Manager Contract for a new Token ID
      * @param _tokenId the TokenId.
      */
    function setupImmutablesPageRoyaltyManagerForTokenId(uint256 _tokenId) internal {
        // checks
        require(tokenIdToRoyaltyAddress[_tokenId] == address(0), "royalty manager already exists for _tokenId");

        // effects
        address _newManager = Clones.clone(address(implementation));
        tokenIdToRoyaltyAddress[_tokenId] = address(_newManager);
        tokenIdToSecondaryRoyaltyPercent[_tokenId] = 1000;  // 10.00%

        // interactions
        ImmutablesPageRoyaltyManager(payable(_newManager)).initialize(
            address(this),
            _tokenId, tokenIdToPage[_tokenId],
            ownerOf(_tokenId), 10000-contractSecondaryRoyaltyPercentage,
            address(0), 0
        );
    }

    // @dev ROYALTIES - IERC2981

    /** @dev IERC2981 royaltyInfo function
      * @param _tokenId The tokenId for which royaltyInfo is being requested.
      * @param _salePrice The sales price to calculate the royalty from.
      * @return receiver the royalty recipient.
      * @return royaltyAmount the royalty amount.
      */
    function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view override returns (address receiver, uint256 royaltyAmount) {
      return (tokenIdToRoyaltyAddress[_tokenId], _salePrice * tokenIdToSecondaryRoyaltyPercent[_tokenId] / 10000);
    }

    /** @dev Allows contract owner or the current royalty recipient to change
      * @dev the receiver or percentage for a specified tokenId.
      * @param _tokenId The tokenId.
      * @param _newRoyaltyRecipient The new royalty receipient.
      * @param _royaltyPercent The new royalty percent.
      */
    function royaltyRecipientUpdateRoyaltyInfo(uint256 _tokenId, address _newRoyaltyRecipient, uint16 _royaltyPercent) external {
      require(ImmutablesPageRoyaltyManager(payable(tokenIdToRoyaltyAddress[_tokenId])).royaltyRecipient() == msg.sender, "auth");
      require(_royaltyPercent >= 0 && _royaltyPercent <= 1000, "percent"); // 0% to 10.00%
      ImmutablesPageRoyaltyManager(payable(tokenIdToRoyaltyAddress[_tokenId])).royaltyRecipientUpdateAddress(_newRoyaltyRecipient);
      tokenIdToSecondaryRoyaltyPercent[_tokenId] = _royaltyPercent;
    }

    /** @dev Allows the royaltyRecipient to update additional payee info for a
      * @dev spedified tokenId.
      * @param _tokenId the tokenId.
      * @param _additionalPayee the additional payee address.
      * @param _additionalPayeePercent the basis point (1/10,000th) share for the _additionalPayee up to artistPercent (e.g., 5000 = 50.0%).
      */
    function royaltyRecipientUpdateTokenAdditionalPayeeInfo(uint256 _tokenId, address _additionalPayee, uint16 _additionalPayeePercent) external  {
        // checks
        require(ImmutablesPageRoyaltyManager(payable(tokenIdToRoyaltyAddress[_tokenId])).royaltyRecipient() == msg.sender, "auth");
        // effects
        // interactions
        ImmutablesPageRoyaltyManager(payable(tokenIdToRoyaltyAddress[_tokenId])).royaltyRecipientUpdateAdditionalPayeeInfo(_additionalPayee, _additionalPayeePercent);
    }

    /** @dev Releases funds from a Royalty Manager for a Token Id
      * @param _tokenId the tokenId.
      */
    function releaseRoyaltiesForTokenId(uint256 _tokenId) external {
        ImmutablesPageRoyaltyManager(payable(tokenIdToRoyaltyAddress[_tokenId])).release();
    }

    // @dev METADATA

    /** @dev Returns a string from a uint256
      * @param value uint256 data type string
      * @return _ string data type.
      */
    function toString(uint256 value) internal pure returns (string memory) {
            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);
    }

    /** @dev Returns a simple SVG for a tokenId
      * @param _tokenId uint256 tokenId to get the SVG for
      * @return _ string SVG for image of tokenId
      */
    function getSVGForTokenId(uint256 _tokenId) public view returns (string memory) {
      string memory output = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style> .edition { fill: #F5F5F5; font-family: Open Sans; font-size: 12px; } .base { fill: #F5F5F5; font-family: Open Sans; font-size: 180px; } </style> <rect width="100%" height="100%" fill="#708090" /> <text class="edition" x="50%" y="5%" dominant-baseline="middle" text-anchor="middle">';
      output = string(abi.encodePacked(output, tokenIdToPage[_tokenId]));
      output = string(abi.encodePacked(output,'</text><text class="edition" x="50%" y="10%" dominant-baseline="middle" text-anchor="middle">][ # ', toString(_tokenId)));
      output = string(abi.encodePacked(output,'</text><text class="base" x="50%" y = "50%" dominant-baseline="middle" text-anchor="middle">][</text></svg>'));
      return output;
    }

    /** @dev Allows the contract owner to update the globalMetadataDescription.
      * @param _newDescription The new desciption text
      */
    function contractOwnerUpdateGlobalMetadataDescription(string calldata _newDescription) external onlyOwner() {
      globalMetadataDescription = _newDescription;
    }

    function getMetadataStringForTokenId(uint256 _tokenId) internal view returns (string memory) {
      string memory _url = string(abi.encodePacked(immutablesWEB, '#/', tokenIdToPage[_tokenId]));
      string memory output = string(
        abi.encodePacked(
          '{"name": "][ ',
          tokenIdToPage[_tokenId],
          '", "description": "', globalMetadataDescription, ' ( ', _url, ' ).", "external_url": "', _url
        )
      );
      return output;
    }

    /** @dev Returns a tokenURI URL or Metadata string depending on useMetadataServer
      * @param _tokenId The _token to return the URI or Metadata for.
      * @return _ String of a URI or Base64 encoded metadata and image string.
      */
    function tokenURI(uint256 _tokenId) public view override returns (string memory) {
      require(_exists(_tokenId), "ERC721Metadata: URI query for nonexistent token");
      if(useMetadataServer) { // IF THE METADATA SERVER IS IN USE RETURN A URL
        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, _tokenId.toString())) : "";
      } else { // ELSE WE ARE SERVERLESS AND RETURN METADATA DIRECTLY h/t DEAFBEEF FIRST NFT
        string memory json = Base64.encode(
          bytes(
            string(
              abi.encodePacked(
                getMetadataStringForTokenId(_tokenId),
                '", "image": "data:image/svg+xml;base64,',
                Base64.encode(bytes(getSVGForTokenId(_tokenId))),
                '"}'
              )
            )
          )
        );
        json = string(abi.encodePacked('data:application/json;base64,', json));
        return json;
      }
    }

    /** @dev Determines if a string is a valid page that is not taken.
      * @param _str The proposed page name.
      * @return _ Whether the page is valid and available to purchase.
      */
    function isValidAndAvailablePageName(string calldata _str) public view returns (bool) {
      bool vaild = isValidPageName(_str);
      return ((pageToTokenId[_str] == 0) && vaild);
    }

    /** @dev Allows the contract owner to update the max number of pages.
      * @param _newMaxTotalSupply The new max total supply.
      */
    function contractOwnerUpdateMaxTotalSupply(uint256 _newMaxTotalSupply) external onlyOwner() {
      require(_newMaxTotalSupply >= currentTokenId, "Can not set maxTotalSupply to a value lower than currentTokenId.");
      maxTotalSupply = _newMaxTotalSupply;
    }
}
/// @author Gutenblock.eth
/// @title Immutables - Ownable Pages
function releaseRoyaltiesForTokenId(uint256 _tokenId) external {
    ImmutablesPageRoyaltyManager(payable(tokenIdToRoyaltyAddress[_tokenId])).release();
}

/** @dev Releases funds from a Royalty Manager for a Token Id
 * @param _tokenId the tokenId.
 */