// File: src/YomFactory.sol



/*----------------------------------------------------------*|
|*               ██╗   ██╗ ██████╗ ███╗   ███╗              *|
|*               ╚██╗ ██╔╝██╔═══██╗████╗ ████║              *|
|*                ╚████╔╝ ██║   ██║██╔████╔██║              *|
|*                 ╚██╔╝  ██║   ██║██║╚██╔╝██║              *|
|*                  ██║   ╚██████╔╝██║ ╚═╝ ██║              *|
|*                  ╚═╝    ╚═════╝ ╚═╝     ╚═╝              *|
|*----------------------------------------------------------*/

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

import "./proxy/Clones.sol";
import "./access/Ownable.sol";

/**
 *
 * @title YomFactory                                         *
 *                                                           *
 * @notice Clone factory pattern contract                    *
 *                                                           *
 * @custom:security-contact tech@ninfa.io                    *
 *
 */

contract YomFactory is Ownable {
    using Clones for address;

    uint256 public deploymentPrice;

    mapping(address => bool) private _instances; // a mapping that contains all

    mapping(address => bool) private _collectionsWhitelist;

    mapping(address => bool) private _authorizedCallers;

    event NewClone(address master, address instance, address owner); // owner is
        // needed in order to keep a local
        // database of owners to instance addresses; this avoids keeping track
        // of them on-chain via a mapping

    /**
     * @param _salt _salt is a random number of our choice. generated with
     * https://web3js.readthedocs.io/en/v1.2.11/web3-utils.html#randomhex
     * _salt could also be dynamically calculated in order to avoid duplicate
     * clones and for a way of finding
     * predictable clones if salt the parameters are known, for example:
     * `address _clone =
     * erc1155Minter.cloneDeterministic(†bytes32(keccak256(abi.encode(_name,
     * _symbol,
     * _msgSender))));`
     * @dev "Using the same implementation and salt multiple time will revert,
     * since the clones cannot be cloneed twice
     * at the same address." -
     * https://docs.openzeppelin.com/contracts/4.x/api/proxy#Clones-cloneDeterministic-address-bytes32-
     * @param _master MUST be one of this factory's whhitelisted collections
     *
     */
    function cloneCollection(
        address _master, 
        bytes32 _salt, 
        bytes calldata _data
    ) public payable returns (address clone_) {

        require(msg.value >= deploymentPrice || _authorizedCallers[msg.sender]);

        require(_collectionsWhitelist[_master] == true, "Collection not whitelisted");

        clone_ = _master.cloneDeterministic(_salt);

        (bool success,) = clone_.call(
            abi.encodeWithSelector(
                0x439fab91, // bytes4(keccak256('initialize(bytes)')) ==
                    // 0x439fab91
                _data
            )
        );
        require(success);

        _instances[clone_] = true;

        emit NewClone(_master, clone_, msg.sender);
    }

    function exists(address _instance) external view returns (bool) {
        return _instances[_instance];
    }

    function whitelistCollection(address _master, bool _isWhitelisted) external onlyOwner {
        _isContract(_master);
        _collectionsWhitelist[_master] = _isWhitelisted;
    }

    function setDeploymentPrice(uint256 _newPrice) external onlyOwner {
        deploymentPrice = _newPrice;
    }

    function setAuthorizedCaller(address _caller, bool _isAuthorized) external onlyOwner {
        _authorizedCallers[_caller] = _isAuthorized;
    }

    function withdraw() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }

    function predictDeterministicAddress(address _master, uint256 _salt) external view returns (address predicted) {
        predicted = Clones.predictDeterministicAddress(_master, bytes32(_salt));
    }

    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `_isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     * ====
     */
    function _isContract(address _account) private view {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.
        uint256 size;
        assembly {
            size := extcodesize(_account)
        }
        require(size > 0);
    }

    constructor(uint256 _deploymentPrice) {
        deploymentPrice = _deploymentPrice;
    }
}

// File: src/proxy/Clones.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
 * deploying minimal proxy contracts, also known as "clones".
 *
 * > To simply and cheaply clone contract functionality in an immutable way,
 * this standard specifies
 * > a minimal bytecode implementation that delegates all calls to a known,
 * fixed address.
 *
 * The library includes functions to deploy a proxy using either `create`
 * (traditional deployment) or `create2`
 * (salted deterministic deployment). It also includes functions to predict the
 * addresses of clones deployed using the
 * deterministic method.
 */
library Clones {
    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour
     * of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     */
    function clone(address implementation) internal returns (address instance) {
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(ptr, 0x14), shl(0x60, implementation))
            mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
            instance := create(0, ptr, 0x37)
        }
        require(instance != address(0), "ERC1167: create failed");
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour
     * of `implementation`.
     *
     * This function uses the create2 opcode and a `salt` to deterministically
     * deploy
     * the clone. Using the same `implementation` and `salt` multiple time will
     * revert, since
     * the clones cannot be deployed twice at the same address.
     */
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(ptr, 0x14), shl(0x60, implementation))
            mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
            instance := create2(0, ptr, 0x37, salt)
        }
        require(instance != address(0), "ERC1167: create2 failed");
    }

    /**
     * @dev Computes the address of a clone deployed using
     * {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt,
        address deployer
    )
        internal
        pure
        returns (address predicted)
    {
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(ptr, 0x14), shl(0x60, implementation))
            mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf3ff00000000000000000000000000000000)
            mstore(add(ptr, 0x38), shl(0x60, deployer))
            mstore(add(ptr, 0x4c), salt)
            mstore(add(ptr, 0x6c), keccak256(ptr, 0x37))
            predicted := keccak256(add(ptr, 0x37), 0x55)
        }
    }

    /**
     * @dev Computes the address of a clone deployed using
     * {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt
    )
        internal
        view
        returns (address predicted)
    {
        return predictDeterministicAddress(implementation, salt, address(this));
    }
}

// File: src/access/Ownable.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

/**
 * @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 {
    address private _owner;

    event OwnershipTransferred(address previousOwner, address newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(msg.sender);
    }

    /**
     * @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() == msg.sender);
        _;
    }

    /**
     * @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() external 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) external onlyOwner {
        require(newOwner != address(0));
        _transferOwnership(newOwner);
    }

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