// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.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() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

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

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        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 virtual onlyOwner {
        _transferOwnership(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 virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.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 _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: src/IDataHolder.sol



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

/// @title IVerified
/// @author @dievardump
interface IDataHolder {
	/// @notice returns the uniqid for an NFT
	/// @param collection the collection
	/// @param tokenId the id
	/// @return an uniq id
	function id(address collection, uint256 tokenId) external view returns (bytes32);

	/// @notice returns the data holder for the given format of a verified id
	/// @param verifiedId the verified token id
	/// @param format the format id
	function getVerifiedFormatHolder(uint256 verifiedId, uint256 format) external view returns (address);

	/// @notice returns the verified id linked to the token
	/// @param collection the collection
	/// @param tokenId the id
	/// @return the verified if
	function getOriginTokenVerifiedId(address collection, uint256 tokenId) external view returns (uint256);

	/// @notice inits verifiedId storage
	/// @param verifiedId the verified token id
	/// @param collection the collection
	/// @param tokenId the id
	/// @param format the format provided
	/// @param dataHolder the contact holding the data for this format
	function initVerifiedTokenData(
		uint256 verifiedId,
		address collection,
		uint256 tokenId,
		uint256 format,
		address dataHolder
	) external;

	/// @notice allows to add another format safely (throws if already exist)
	/// @param collection the collection
	/// @param tokenId the id
	/// @param format the format provided
	/// @param dataHolder the contact holding the data for this format
	function safeSetData(
		address collection,
		uint256 tokenId,
		uint256 format,
		address dataHolder
	) external;

	/// @notice allows to add another format (it overwrites existing data for this format)
	/// @param collection the collection
	/// @param tokenId the id
	/// @param format the format provided
	/// @param dataHolder the contact holding the data for this format
	function setData(
		address collection,
		uint256 tokenId,
		uint256 format,
		address dataHolder
	) external;
}

// File: src/VerifiedDataHolder.sol



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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IDataHolder} from "./IDataHolder.sol";

/// @title VerifiedDataHolder
/// @author @dievardump
contract VerifiedDataHolder is IDataHolder, Ownable {
	error NotAuthorized();
	error UnsafeOverwrite();
	error UnknownOriginToken();
	error TokenAlreadyVerified();

	struct VerifiedData {
		uint256 verifiedId;
		// format => dataHolder
		mapping(uint256 => address) formats;
	}

	/// @notice Origin NFT data (linked verifiedId and available formats)
	/// @dev id => format => dataHolder
	/// @dev format 1 == 8x10; format 2 == 24x24; format 3 == 36x36; maybe more in the future?
	mapping(bytes32 => VerifiedData) public originTokens;

	/// @notice link verifiedId => originalNFT
	mapping(uint256 => bytes32) public verifiedToData;

	/// @notice operators having the right to write on this contract; should be the "VerifiedMinter"
	mapping(address => bool) public operators;

	modifier onlyOperator() {
		if (!operators[msg.sender]) {
			revert NotAuthorized();
		}
		_;
	}

	// =============================================================
	//                   				Getters
	// =============================================================

	/// @inheritdoc IDataHolder
	function id(address collection, uint256 tokenId) public pure returns (bytes32) {
		return keccak256(abi.encode(collection, tokenId));
	}

	/// @inheritdoc IDataHolder
	function getOriginTokenVerifiedId(address collection, uint256 tokenId) public view returns (uint256) {
		bytes32 itemId = id(collection, tokenId);
		uint256 verifiedId = originTokens[itemId].verifiedId;

		if (verifiedId == 0) {
			revert UnknownOriginToken();
		}

		return verifiedId;
	}

	/// @inheritdoc IDataHolder
	function getVerifiedFormatHolder(uint256 verifiedId, uint256 format) external view override returns (address) {
		return originTokens[verifiedToData[verifiedId]].formats[format];
	}

	// =============================================================
	//                   		Gated Operators
	// =============================================================

	/// @inheritdoc IDataHolder
	function initVerifiedTokenData(
		uint256 verifiedId,
		address collection,
		uint256 tokenId,
		uint256 format,
		address dataHolder
	) external override onlyOperator {
		// we make sure this verified id doesn't already have a linked token
		if (verifiedToData[verifiedId] != 0x0) {
			revert NotAuthorized();
		}

		bytes32 itemId = id(collection, tokenId);
		// verify we don't have any token already linked to (collection, tokenId)
		if (originTokens[itemId].verifiedId != 0) {
			revert TokenAlreadyVerified();
		}

		// we link verifiedId => origin item
		verifiedToData[verifiedId] = itemId;

		// we link origin item with verified item
		originTokens[itemId].verifiedId = verifiedId;
		originTokens[itemId].formats[format] = dataHolder;
	}

	/// @inheritdoc IDataHolder
	function safeSetData(
		address collection,
		uint256 tokenId,
		uint256 format,
		address dataHolder
	) external override onlyOperator {
		bytes32 itemId = id(collection, tokenId);

		address holder = originTokens[itemId].formats[format];

		if (holder != address(0)) {
			revert UnsafeOverwrite();
		}

		originTokens[itemId].formats[format] = dataHolder;
	}

	/// @inheritdoc IDataHolder
	function setData(
		address collection,
		uint256 tokenId,
		uint256 format,
		address dataHolder
	) external override onlyOperator {
		originTokens[id(collection, tokenId)].formats[format] = dataHolder;
	}

	// =============================================================
	//                   				Gated Owner
	// =============================================================

	/// @notice allows owner to add/remove operators
	/// @param operators_ the operators to edit
	/// @param enabled if we enable or disable those operators
	function setOperators(address[] calldata operators_, bool enabled) external onlyOwner {
		for (uint256 i; i < operators_.length; i++) {
			operators[operators_[i]] = enabled;
		}
	}
}