// File: contracts/MetadataCoordinator.sol



pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
import "./mixin/MixinOwnable.sol";
import "./BaseMetadataRegistry.sol";
import "./interface/IMetadataRegistry.sol";

contract MetadataCoordinator is Ownable {
  uint256 constant internal TYPE_MASK = uint256(uint128(~0)) << 128;

  string public runnableOffchainScript;

  mapping(uint256 => mapping(uint256 => MetadataRegistryInfo)) public tokenTypeToMetadataRegistries;
  mapping(uint256 => uint256) public tokenTypeToMetadataRegistryMaxIndex;

	struct MetadataRegistryInfo {
		address registry;
    string key;
    string prefURI;
	}

  constructor(
  ) {
  }

  event UpdatedMetadataRegistryInfo(
      uint256 indexed tokenType,
      uint256 indexed index,
      address indexed registry,
      bool isBased
  );

  function setTokenTypeToMetadataRegistries(uint256 tokenType, uint256[] memory indexes, MetadataRegistryInfo[] memory infos) public onlyOwner {
    for (uint i = 0; i < indexes.length; ++i) {
      tokenTypeToMetadataRegistries[tokenType][indexes[i]] = infos[i];
    }
  }

  function setTokenTypeToMetadataRegistryMaxIndex(uint256 tokenType, uint256 maxIndex) public onlyOwner {
    tokenTypeToMetadataRegistryMaxIndex[tokenType] = maxIndex;
  }

  function setRunnableOffchainScript(string memory runnableOffchainScript_) public onlyOwner {
    runnableOffchainScript = runnableOffchainScript_;
  }

  function _getNonFungibleBaseType(uint256 id) pure internal returns (uint256) {
    return id & TYPE_MASK;
  }

  function tokenIdToDocuments(uint256 tokenId) public view returns (IMetadataRegistry.Document[] memory, MetadataRegistryInfo[] memory) {
    uint256 tokenType = _getNonFungibleBaseType(tokenId);
    uint256 maxIndex = tokenTypeToMetadataRegistryMaxIndex[tokenType];
    MetadataRegistryInfo[] memory infos = new MetadataRegistryInfo[](maxIndex);
    IMetadataRegistry.Document[] memory documents = new IMetadataRegistry.Document[](maxIndex);
    for (uint i = 0; i < maxIndex; ++i) {
      MetadataRegistryInfo memory info = tokenTypeToMetadataRegistries[tokenType][i];
      infos[i] = info;
      IMetadataRegistry registry = IMetadataRegistry(info.registry);
      IMetadataRegistry.Document memory doc = registry.tokenIdToDocument(tokenId, info.key);
      documents[i] = doc;
    }
    return (documents, infos);
  }
}

// File: contracts/mixin/MixinOwnable.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor () {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(_owner == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}
// File: contracts/BaseMetadataRegistry.sol



pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;

import "./mixin/MixinOwnable.sol";
import "./library/LibString.sol";
import "./interface/IMetadataRegistry.sol";

contract BaseMetadataRegistry is Ownable, IMetadataRegistry {
  uint256 constant internal TYPE_MASK = uint256(uint128(~0)) << 128;

  mapping(uint256 => mapping(string => IMetadataRegistry.Document)) public tokenTypeToBaseURIMap;
  mapping(address => bool) public permissedWriters;

  constructor(
  ) {
  }

  event UpdatedDocument(
      uint256 indexed tokenId,
      address indexed writer,
      string indexed key,
      string text
  );

  function updatePermissedWriterStatus(address _writer, bool status) public onlyOwner {
    permissedWriters[_writer] = status;
  }
  
  modifier onlyIfPermissed(address writer) {
    require(permissedWriters[writer] == true, "writer can't write to registry");
    _;
  }

  function writeDocuments(uint256 tokenType, string[] memory keys, string[] memory texts, address[] memory writers) public onlyIfPermissed(msg.sender) {
    require(keys.length == texts.length, "keys and txHashes size mismatch");
    require(writers.length == texts.length, "writers and texts size mismatch");
    for (uint256 i = 0; i < keys.length; ++i) {
      string memory key = keys[i];
      string memory text = texts[i];
      address writer = writers[i];
      tokenTypeToBaseURIMap[tokenType][key] = IMetadataRegistry.Document(writer, text, block.timestamp);
      emit UpdatedDocument(tokenType, writer, key, text); 
    }
  }

  function _getNonFungibleBaseType(uint256 id) pure internal returns (uint256) {
    return id & TYPE_MASK;
  }

  function tokenIdToDocument(uint256 tokenId, string memory key) override external view returns (IMetadataRegistry.Document memory) {
    IMetadataRegistry.Document memory doc = tokenTypeToBaseURIMap[_getNonFungibleBaseType(tokenId)][key];
    string memory text =  LibString.strConcat(
        doc.text,
        LibString.uint2hexstr(tokenId)
    );
    return IMetadataRegistry.Document(doc.writer, text, doc.creationTime); 
  }
}
// File: contracts/interface/IMetadataRegistry.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;

interface IMetadataRegistry {
  struct Document {
		address writer;
		string text;
		uint256 creationTime;
	}

  function tokenIdToDocument(uint256 tokenId, string memory key) external view returns (Document memory);
}
// File: contracts/utils/Context.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.7.0;

/*
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _txOrigin() internal view virtual returns (address) {
        return tx.origin;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
        return msg.data;
    }
}
// File: contracts/library/LibString.sol



pragma solidity ^0.7.0;

library LibString {
  // via https://github.com/oraclize/ethereum-api/blob/master/oraclizeAPI_0.5.sol
  function strConcat(string memory _a, string memory _b, string memory _c, string memory _d, string memory _e) internal pure returns (string memory) {
      bytes memory _ba = bytes(_a);
      bytes memory _bb = bytes(_b);
      bytes memory _bc = bytes(_c);
      bytes memory _bd = bytes(_d);
      bytes memory _be = bytes(_e);
      string memory abcde = new string(_ba.length + _bb.length + _bc.length + _bd.length + _be.length);
      bytes memory babcde = bytes(abcde);
      uint k = 0;
      for (uint i = 0; i < _ba.length; i++) babcde[k++] = _ba[i];
      for (uint i = 0; i < _bb.length; i++) babcde[k++] = _bb[i];
      for (uint i = 0; i < _bc.length; i++) babcde[k++] = _bc[i];
      for (uint i = 0; i < _bd.length; i++) babcde[k++] = _bd[i];
      for (uint i = 0; i < _be.length; i++) babcde[k++] = _be[i];
      return string(babcde);
    }

    function strConcat(string memory _a, string memory _b, string memory _c, string memory _d) internal pure returns (string memory) {
        return strConcat(_a, _b, _c, _d, "");
    }

    function strConcat(string memory _a, string memory _b, string memory _c) internal pure returns (string memory) {
        return strConcat(_a, _b, _c, "", "");
    }

    function strConcat(string memory _a, string memory _b) internal pure returns (string memory) {
        return strConcat(_a, _b, "", "", "");
    }

    function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
        if (_i == 0) {
            return "0";
        }
        uint j = _i;
        uint len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint k = len - 1;
        while (_i != 0) {
            bstr[k--] = byte(uint8(48 + _i % 10));
            _i /= 10;
        }
        return string(bstr);
    }

    function uint2hexstr(uint i) internal pure returns (string memory) {
        if (i == 0) {
            return "0";
        }
        uint j = i;
        uint len;
        while (j != 0) {
            len++;
            j = j >> 4;
        }
        uint mask = 15;
        bytes memory bstr = new bytes(len);
        uint k = len - 1;
        while (i != 0){
            uint curr = (i & mask);
            bstr[k--] = curr > 9 ? byte(uint8(55 + curr)) : byte(uint8(48 + curr));
            i = i >> 4;
        }
        return string(bstr);
    }
}