// File: src/ethereum/NftCreatorVerifier.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import { INftCreatorVerifier, DeployLimitedEditionNftParams, DeployTimedEditionNftParams } from "src/ethereum/INftCreatorVerifier.sol";
import { CrossDomainOrigin } from "src/shared/CrossDomainOrigin.sol";
import { ICrossDomainMessenger } from "src/shared/ICrossDomainMessenger.sol";
import { IOpEditionNftFactory, DeployLimitedEditionNftParams as OpDeployLimitedEditionNftParams, DeployTimedEditionNftParams as OpDeployTimedEditionNftParams } from "src/op/IOpEditionNftFactory.sol";
import { ICreatorRegistry } from "src/ethereum/ICreatorRegistry.sol";
import { EditionType } from "src/shared/EditionType.sol";
import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol";

contract NftCreatorVerifier is INftCreatorVerifier, Ownable {
    address private _creatorRegistry;
    mapping(uint256 => address) private _opChainIdToOpEditionNftFactory;

    modifier onlyNftCreator(address nftAddress_, uint256 tokenId_) {
        if (ICreatorRegistry(_creatorRegistry).getCreatorOf(nftAddress_, tokenId_) != msg.sender) {
            revert CallerNotNftCreator(nftAddress_, tokenId_, msg.sender);
        }

        _;
    }

    modifier onlyRegisteredEditionNftChainId(uint256 editionNftChainId_) {
        if (_opChainIdToOpEditionNftFactory[editionNftChainId_] == address(0)) {
            revert UnregisteredOpChainId(editionNftChainId_);
        }

        _;
    }

    constructor(address owner_, address creatorRegistry_) {
        _transferOwnership(owner_);
        _creatorRegistry = creatorRegistry_;
    }

    function deployTimedEditionNft(
        DeployTimedEditionNftParams calldata params_
    )
        external
        onlyNftCreator(params_.originalNftAddress, params_.originalNftTokenId)
        onlyRegisteredEditionNftChainId(params_.editionNftChainId)
    {
        bytes memory message = abi.encodeCall(
            IOpEditionNftFactory(address(0)).deployTimedEditionNft,
            (
                OpDeployTimedEditionNftParams({
                    creator: msg.sender,
                    originalNftAddress: params_.originalNftAddress,
                    originalNftTokenId: params_.originalNftTokenId,
                    endMintingBlockNumber: params_.endMintingBlockNumber,
                    mintPrice: params_.mintPrice,
                    name: params_.name,
                    symbol: params_.symbol,
                    description: params_.description,
                    collectionImageUri: params_.collectionImageUri
                })
            )
        );

        ICrossDomainMessenger(CrossDomainOrigin.crossDomainMessenger(params_.editionNftChainId)).sendMessage(
            _opChainIdToOpEditionNftFactory[params_.editionNftChainId],
            message,
            // The first 1.92M gas is free
            // https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
            1920000
        );

        emit DeployEditionNftMessageSent(
            params_.editionNftChainId,
            params_.originalNftAddress,
            params_.originalNftTokenId,
            msg.sender,
            EditionType.Timed
        );
    }

    function deployLimitedEditionNft(
        DeployLimitedEditionNftParams calldata params_
    )
        external
        onlyNftCreator(params_.originalNftAddress, params_.originalNftTokenId)
        onlyRegisteredEditionNftChainId(params_.editionNftChainId)
    {
        bytes memory message = abi.encodeCall(
            IOpEditionNftFactory(address(0)).deployLimitedEditionNft,
            (
                OpDeployLimitedEditionNftParams({
                    creator: msg.sender,
                    originalNftAddress: params_.originalNftAddress,
                    originalNftTokenId: params_.originalNftTokenId,
                    supplyLimit: params_.supplyLimit,
                    mintPrice: params_.mintPrice,
                    name: params_.name,
                    symbol: params_.symbol,
                    description: params_.description,
                    collectionImageUri: params_.collectionImageUri
                })
            )
        );

        ICrossDomainMessenger(CrossDomainOrigin.crossDomainMessenger(params_.editionNftChainId)).sendMessage(
            _opChainIdToOpEditionNftFactory[params_.editionNftChainId],
            message,
            // The first 1.92M gas is free
            // https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
            1920000
        );

        emit DeployEditionNftMessageSent(
            params_.editionNftChainId,
            params_.originalNftAddress,
            params_.originalNftTokenId,
            msg.sender,
            EditionType.Limited
        );
    }

    function setOpEditionNftFactory(uint256 opChainId_, address opEditionNftFactory_) external override onlyOwner {
        _opChainIdToOpEditionNftFactory[opChainId_] = opEditionNftFactory_;
    }

    function creatorRegistry() external view returns (address) {
        return _creatorRegistry;
    }

    function opEditionNftFactory(uint256 opChainId_) external view returns (address) {
        return _opChainIdToOpEditionNftFactory[opChainId_];
    }
}

// File: src/ethereum/INftCreatorVerifier.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import { EditionType } from "src/shared/EditionType.sol";

struct DeployLimitedEditionNftParams {
    uint256 editionNftChainId;
    address originalNftAddress;
    uint256 originalNftTokenId;
    string name;
    string symbol;
    string description;
    string collectionImageUri;
    uint256 supplyLimit;
    uint256 mintPrice;
}

struct DeployTimedEditionNftParams {
    uint256 editionNftChainId;
    address originalNftAddress;
    uint256 originalNftTokenId;
    string name;
    string symbol;
    string description;
    string collectionImageUri;
    uint256 endMintingBlockNumber;
    uint256 mintPrice;
}

interface INftCreatorVerifier {
    error CallerNotNftCreator(address nftAddress_, uint256 tokenId_, address caller_);
    error UnregisteredOpChainId(uint256 opChainId_);

    event DeployEditionNftMessageSent(
        uint256 indexed editionNftChainId_,
        address indexed originalNftAddress_,
        uint256 originalNftTokenId_,
        address indexed creator_,
        EditionType editionType_
    );

    function deployTimedEditionNft(DeployTimedEditionNftParams calldata params_) external;

    function deployLimitedEditionNft(DeployLimitedEditionNftParams calldata params_) external;

    function creatorRegistry() external view returns (address);

    function opEditionNftFactory(uint256 opChainId_) external view returns (address);

    function setOpEditionNftFactory(uint256 opChainId_, address opEditionNftFactory_) external;
}

// File: src/shared/CrossDomainOrigin.sol



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

// Reference: https://github.com/ethereum-optimism/optimism-tutorial/blob/main/cross-dom-comm/contracts/Greeter.sol

import { ICrossDomainMessenger } from "src/shared/ICrossDomainMessenger.sol";

library CrossDomainOrigin {
    /**
     * Returns the CrossDomainMessenger for the given destinationOpChainId_
     **/
    function crossDomainMessenger(uint256 crossDomainChainId_) internal view returns (address cdmAddr) {
        // Get the cross domain messenger's address each time.
        // This is less resource intensive than writing to storage.

        // Mainnet -> Optimism
        if (block.chainid == 1 && crossDomainChainId_ == 10) cdmAddr = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1;

        // Mainnet -> Base
        if (block.chainid == 1 && crossDomainChainId_ == 8453) cdmAddr = 0x866E82a600A1414e583f7F13623F1aC5d58b0Afa;

        // Mainnet -> Zora
        if (block.chainid == 1 && crossDomainChainId_ == 7777777) cdmAddr = 0xdC40a14d9abd6F410226f1E6de71aE03441ca506;

        // Goerli -> Goerli Optimism
        if (block.chainid == 5 && crossDomainChainId_ == 420) cdmAddr = 0x5086d1eEF304eb5284A0f6720f79403b4e9bE294;

        // Goerli -> Goerli Base
        if (block.chainid == 5 && crossDomainChainId_ == 84531) cdmAddr = 0x8e5693140eA606bcEB98761d9beB1BC87383706D;

        // Goerli -> Goerli Zora
        if (block.chainid == 5 && crossDomainChainId_ == 999) cdmAddr = 0xD87342e16352D33170557A7dA1e5fB966a60FafC;

        // Op Stack
        if (
            // Optimism -> Mainnet
            (block.chainid == 10 && crossDomainChainId_ == 1) ||
            // Base -> Mainnet
            (block.chainid == 8453 && crossDomainChainId_ == 1) ||
            // Zora -> Mainnet
            (block.chainid == 7777777 && crossDomainChainId_ == 1) ||
            // Goerli Optimism -> Goerli
            (block.chainid == 420 && crossDomainChainId_ == 5) ||
            // Goerli Base -> Goerli
            (block.chainid == 84531 && crossDomainChainId_ == 5) ||
            // Goerli Zora -> Goerli
            (block.chainid == 999 && crossDomainChainId_ == 5)
        ) cdmAddr = 0x4200000000000000000000000000000000000007;
    }

    function getCrossDomainMessageSender(uint256 crossDomainChainId_) internal view returns (address) {
        // Get the cross domain messenger's address each time.
        // This is less resource intensive than writing to storage.
        address cdmAddr = crossDomainMessenger(crossDomainChainId_);

        // If this isn't a cross domain message
        if (msg.sender != cdmAddr) {
            revert("Not crosschain call");
        }

        // If it is a cross domain message, find out where it is from
        return ICrossDomainMessenger(cdmAddr).xDomainMessageSender();
    }
}

// File: src/shared/ICrossDomainMessenger.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface ICrossDomainMessenger {
    /**********
     * Events *
     **********/

    event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit);
    event RelayedMessage(bytes32 indexed msgHash);
    event FailedRelayedMessage(bytes32 indexed msgHash);

    /*************
     * Variables *
     *************/

    function xDomainMessageSender() external view returns (address);

    /********************
     * Public Functions *
     ********************/

    /**
     * Sends a cross domain message to the target messenger.
     * @param _target Target contract address.
     * @param _message Message to send to the target.
     * @param _gasLimit Gas limit for the provided message.
     */
    function sendMessage(address _target, bytes calldata _message, uint32 _gasLimit) external;
}

// File: src/op/IOpEditionNftFactory.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import { EditionType } from "src/shared/EditionType.sol";

struct DeployLimitedEditionNftParams {
    address creator;
    address originalNftAddress;
    uint256 originalNftTokenId;
    uint256 supplyLimit;
    uint256 mintPrice;
    string name;
    string symbol;
    string description;
    string collectionImageUri;
}

struct DeployTimedEditionNftParams {
    address creator;
    address originalNftAddress;
    uint256 originalNftTokenId;
    uint256 endMintingBlockNumber;
    uint256 mintPrice;
    string name;
    string symbol;
    string description;
    string collectionImageUri;
}

interface IOpEditionNftFactory {
    error CallerNotEthereumNftCreatorVerifier();
    error EditionNftAlreadyDeployed();

    event DeployEditionNft(
        uint256 indexed originalNftChainId_,
        address indexed originalNftAddress_,
        uint256 originalNftTokenId_,
        address indexed creator_,
        EditionType editionType_,
        address editionNftAddress_
    );

    function deployLimitedEditionNft(DeployLimitedEditionNftParams calldata params_) external returns (address);

    function deployTimedEditionNft(DeployTimedEditionNftParams calldata params_) external returns (address);

    function l1ChainId() external view returns (uint256);
}

// File: src/ethereum/ICreatorRegistry.sol



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

interface ICreatorRegistry {
    function getCreatorOf(address nftContract_, uint256 tokenId_) external view returns (address);
}

// File: src/shared/EditionType.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

enum EditionType {
    Limited,
    Timed
}

// File: lib/openzeppelin-contracts/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.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. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling 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: lib/openzeppelin-contracts/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;
    }
}