//SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

/**
 * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
 * deploying minimal proxy contracts, also known as "clones".
 *
 */
contract Cloneable {

    /**
        @dev Deploys and returns the address of a clone of address(this
        Created by DeFi Mark To Allow Clone Contract To Easily Create Clones Of Itself
        Without redundancy
     */
    function clone() external returns(address) {
        return _clone(address(this));
    }

    /**
     * @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) {
        /// @solidity memory-safe-assembly
        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");
    }

}
/**
 * @title Owner
 * @dev Set & change owner
 */
contract Ownable {

    address private owner;
    
    // event for EVM logging
    event OwnerSet(address indexed oldOwner, address indexed newOwner);
    
    // modifier to check if caller is owner
    modifier onlyOwner() {
        // If the first argument of 'require' evaluates to 'false', execution terminates and all
        // changes to the state and to Ether balances are reverted.
        // This used to consume all gas in old EVM versions, but not anymore.
        // It is often a good idea to use 'require' to check if functions are called correctly.
        // As a second argument, you can also provide an explanation about what went wrong.
        require(msg.sender == owner, "Caller is not owner");
        _;
    }
    
    /**
     * @dev Set contract deployer as owner
     */
    constructor() {
        owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
        emit OwnerSet(address(0), owner);
    }

    /**
     * @dev Change owner
     * @param newOwner address of new owner
     */
    function changeOwner(address newOwner) public onlyOwner {
        emit OwnerSet(owner, newOwner);
        owner = newOwner;
    }

    /**
     * @dev Return owner address 
     * @return address of owner
     */
    function getOwner() external view returns (address) {
        return owner;
    }
}

interface IBaggageClaim {
    function __init__(
        address BAG_,
        address rewardToken_,
        uint256 lockTime_,
        uint256 rewardsPerSecond_,
        string calldata name_,
        string calldata symbol_,
        address lockTimeSetter_
    ) external;
}

interface IHasTotalSupply {
    function totalSupply() external view returns (uint256);
}

contract BaggageClaimGenerator is Ownable {

    /**
        Proxy Implementation Through Which NFT Staking Contracts Are Generated
     */
    address public proxyImplementation;

    /**
        Project Structure
     */
    struct Project {
        string name;
        address nft;
        address stakingProxy;
    }

    /**
        Staking Proxy => Project Struct
     */
    mapping ( address => Project ) public stakingToProject;
    
    /**
        NFT => Project Struct
     */
    mapping ( address => Project ) public nftToStaking;

    /**
        List Of All Staking Proxies
     */
    address[] public allProjects;

    /**
        True If User Can Create Staking Proxies, False Otherwise
     */
    mapping ( address => bool ) public canCreateStakingContracts;
    modifier canCreate() {
        require(
            canCreateStakingContracts[msg.sender] || msg.sender == this.getOwner(),
            'Invalid Permissions'
        );
        _;
    }

    /** Creation Event */
    event StakingCreated(address staking, address nft);

    constructor(address implementation) {
        canCreateStakingContracts[msg.sender] = true;
        proxyImplementation = implementation;
    }

    function givePermissionToCreateStakingContracts(address user, bool isAllowed) external onlyOwner {
        canCreateStakingContracts[user] = isAllowed;
    }

    function setNewImplementation(address newProxyImplementation) external onlyOwner {
        proxyImplementation = newProxyImplementation;
    }

    function createWithImplementation(
        address implementation,
        address nft_,
        address rewardToken_,
        uint256 lockTime_,
        uint256 rewardsPerSecond_,
        string calldata name_,
        string calldata symbol_,
        address lockTimeSetter_
    ) external canCreate returns (address) {
        return _create(implementation, nft_, rewardToken_, lockTime_, rewardsPerSecond_, name_, symbol_, lockTimeSetter_);
    }

    function create(
        address nft_,
        address rewardToken_,
        uint256 lockTime_,
        uint256 rewardsPerSecond_,
        string calldata name_,
        string calldata symbol_,
        address lockTimeSetter_
    ) external canCreate returns (address) {
        return _create(proxyImplementation, nft_, rewardToken_, lockTime_, rewardsPerSecond_, name_, symbol_, lockTimeSetter_);
    }

    function _create(
        address implementation,
        address nft_,
        address rewardToken_,
        uint256 lockTime_,
        uint256 rewardsPerSecond_,
        string calldata name_,
        string calldata symbol_,
        address lockTimeSetter_
    ) internal returns (address project) {

        project = Cloneable(implementation).clone();

        IBaggageClaim(project).__init__(
            nft_,
            rewardToken_,
            lockTime_,
            rewardsPerSecond_,
            name_,
            symbol_,
            lockTimeSetter_
        );

        Project memory newProject = Project({
            name: name_,
            nft: nft_,
            stakingProxy: project
        });

        stakingToProject[project] = newProject;
        nftToStaking[nft_] = newProject;

        allProjects.push(project);

        emit StakingCreated(project, nft_);
    }
    

    function fetchAllProjects() external view returns (address[] memory) {
        return allProjects;
    }

    function fetchAllProjectsAndNames() external view returns (address[] memory, string[] memory) {

        uint len = allProjects.length;
        string[] memory names = new string[](len);
        for (uint i = 0; i < len;) {
            names[i] = stakingToProject[allProjects[i]].name;
            unchecked {
                ++i;
            }
        }

        return (allProjects, names);
    }

    function fetchAllProjectsAndNamesAndNFTsAndStakingContracts() external view returns (address[] memory, string[] memory, address[] memory, address[] memory) {

        uint len = allProjects.length;
        string[] memory names = new string[](len);
        address[] memory nfts = new address[](len);
        address[] memory staking = new address[](len);
        for (uint i = 0; i < len;) {
            names[i] = stakingToProject[allProjects[i]].name;
            nfts[i] = stakingToProject[allProjects[i]].nft;
            staking[i] = stakingToProject[allProjects[i]].stakingProxy;
            unchecked {
                ++i;
            }
        }

        return (allProjects, names, nfts, staking);
    }

}