// File: /contracts/Platform/Meta/MetaSpaceController.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IMetaSpace.sol";
import {Pausable} from "../../Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MetaSpaceController is IMetaSpace, Pausable, ReentrancyGuard {
    address _platform_address;
    address _metaspace_token_address;

    MetaSpace[] public _metaSpaces;
    mapping(uint256 => Token[]) _tokens;
    mapping(uint256 => Curator[]) _curators;
    mapping(uint256 => Partner[]) _partners;
    uint256 tokenId;

    mapping(uint256 => uint256) _tokens_metaspace_uid; 
    mapping(uint256 => uint256) _global_to_local_token_uids;

    mapping(uint256 => mapping(address => mapping(uint256 => bool)))
        private _submited_tokens;
    mapping(uint256 => mapping(uint256 => bool))
        private _curator_approved_tokens;

    mapping(uint256 => mapping(address => bool)) private _is_curator_address;
    mapping(uint256 => mapping(address => bool)) private _is_partner_address;

    modifier onlyOwner(uint256 uid) {
        require(msg.sender == _metaSpaces[uid].owner_of, "Only for owner");
        _;
    }

    constructor(
        address platform_address_
    ) Pausable(msg.sender) {
        _platform_address = platform_address_;
    }

    function createSpace(string memory uri_) external {
        uint256 uid = _metaSpaces.length;
        _metaSpaces.push(MetaSpace(uid, msg.sender, 0, uri_, 0, 0, address(0)));
        _curators[uid].push(Curator(msg.sender, 0));
        IERC721MetaSpace(_metaspace_token_address).mint(msg.sender, uid);
    }

    function updateTerms(
        uint256 uid,
        Access memory access_,
        address[] memory partners_,
        uint256[] memory partners_fees_,
        OwnerMeta memory owner_meta_,
        address[] memory curators_,
        uint256[] memory curator_fees_
    ) external onlyOwner(uid) {
        MetaSpace memory _metaSpace = _metaSpaces[uid];
        require(
            curators_.length == curator_fees_.length,
            "Invalid inputs for curators"
        );
        require(
            partners_.length == partners_fees_.length,
            "Invalid inputs for partners"
        );
        require(curators_.length <= 2, "To much curators for Space");
        uint256 percentages = (owner_meta_.owner_fee_ * 10) + 25;
        for (uint256 i = 0; i < partners_fees_.length; i++) {
            percentages += partners_fees_[i] * 10;
        }
        for (uint256 i = 0; i < curator_fees_.length; i++) {
            percentages += curator_fees_[i] * 10;
        }
        require(percentages <= 1000, "Percentage too big");

        _metaSpace.owner_of = owner_meta_.owner_of_;
        _metaSpace.owner_fee = owner_meta_.owner_fee_;
        _metaSpace.access_token_address = access_.token_address;
        _metaSpace.access_fee = access_.access_fee;
        _metaSpace.submit_fee = access_.submit_fee;
        _metaSpaces[uid] = _metaSpace;

        for (uint256 i = 0; i < partners_.length; i++) {
            _partners[uid].push(Partner(partners_[i], partners_fees_[i]));
            _is_partner_address[uid][partners_[i]] = true;
        }
        for (uint256 i = 0; i < curators_.length; i++) {
            _curators[uid].push(Curator(curators_[i], curator_fees_[i]));
            _is_curator_address[uid][curators_[i]] = true;
        }
    }

    function submitToken(
        uint256 space_uid,
        address token_address_,
        uint256 token_id_,
        uint256 amount_,
        ContractType contract_type_,
        uint256 price_
    ) external {
        MetaSpace memory _metaSpace = _metaSpaces[space_uid];
        if (
            _metaSpace.access_token_address != address(0) &&
            _metaSpace.submit_fee != 0
        ) {
            require(
                IERC20MetaSpace(_metaSpace.access_token_address).balanceOf(
                    msg.sender
                ) >= _metaSpace.submit_fee,
                "Not enough tokens for submit your nft"
            );
        }
        require(
            !_submited_tokens[space_uid][token_address_][token_id_],
            "Token is submited to this Space"
        );
        if (contract_type_ == ContractType.single_token) {
            IERC721MetaSpace token = IERC721MetaSpace(token_address_);
            require(
                token.ownerOf(token_id_) == msg.sender,
                "You are not an owner of this token"
            );
            require(
                token.getApproved(token_id_) == address(this),
                "Not approved to this Space"
            );
        } else {
            IERC1155MetaSpace token = IERC1155MetaSpace(token_address_);
            require(
                token.balanceOf(msg.sender, token_id_) >= amount_,
                "Not enough tokens in your wallet"
            );
            require(
                token.isApprovedForAll(msg.sender, address(this)),
                "Not approved to this Space"
            );
        }
        uint256 newTokenIdGlobal = tokenId;
        uint256 newTokenIdLocal = _tokens[space_uid].length;
        _global_to_local_token_uids[newTokenIdGlobal] = newTokenIdLocal;
        _submited_tokens[space_uid][token_address_][token_id_] = true;
        _tokens_metaspace_uid[newTokenIdGlobal] = space_uid;
        tokenId++;
        _tokens[space_uid].push(
            Token(
                newTokenIdGlobal,
                space_uid,
                token_address_,
                token_id_,
                amount_,
                contract_type_,
                price_,
                false,
                false,
                false,
                msg.sender
            )
        );
    }

    function approvedToken(uint256 uid) external {
        uint256 _space_uid = _tokens_metaspace_uid[uid];
        uint256 _uid_local = _global_to_local_token_uids[uid];
        require(
            !_curator_approved_tokens[_space_uid][_uid_local],
            "Already approved by curator"
        );
        require(
            _is_curator_address[_space_uid][msg.sender],
            "You are not curator"
        );
        _curator_approved_tokens[_space_uid][_uid_local] = true;
        _tokens[_space_uid][_uid_local].approved = true;
    }

    function buy(uint256 uid) external payable nonReentrant {
        uint256 _space_uid = _tokens_metaspace_uid[uid];
        uint256 _uid_local = _global_to_local_token_uids[uid];
        require(_uid_local <= _tokens[_space_uid].length, "Order does not exist");
        Token memory order = _tokens[_space_uid][_uid_local];
        require(
            _curator_approved_tokens[_space_uid][_uid_local] && order.approved,
            "Not curator approved"
        );
        address _access_token_address = _metaSpaces[_space_uid]
            .access_token_address;
        uint256 _access_fee = _metaSpaces[_space_uid].access_fee;
        address _metaspace_owner = _metaSpaces[_space_uid].owner_of;
        uint256 _metaspace_owner_fee = _metaSpaces[_space_uid].owner_fee;
        if (_access_token_address != address(0) && _access_fee != 0) {
            require(
                IERC20MetaSpace(_access_token_address).balanceOf(msg.sender) >=
                    _access_fee,
                "Permission denied"
            );
        }
        require(msg.value >= order.price, "Not enough ETH sent");
        require(!order.is_sold, "Already resolved");
        if (order.contract_type == ContractType.single_token) {
            IERC721MetaSpace token = IERC721MetaSpace(order.token_address);
            require(
                token.ownerOf(order.token_id) == order.owner_of,
                "Not an owner of this token"
            );
            require(
                token.getApproved(order.token_id) == address(this),
                "Not approved to this Space"
            );
            token.safeTransferFrom(
                order.owner_of,
                msg.sender,
                order.token_id,
                ""
            );
        } else {
            IERC1155MetaSpace token = IERC1155MetaSpace(order.token_address);
            require(
                token.balanceOf(order.owner_of, order.token_id) >= order.amount,
                "Not enough tokens in wallet"
            );
            require(
                token.isApprovedForAll(_metaspace_owner, address(this)),
                "Not approved to this Space"
            );
            token.safeTransferFrom(
                order.owner_of,
                msg.sender,
                order.token_id,
                order.amount,
                ""
            );
        }

        uint256 summ = 0;
        payable(_platform_address).transfer((msg.value * 25) / 1000);
        summ += (msg.value * 25) / 1000;
        payable(_metaspace_owner).send(
            (msg.value * _metaspace_owner_fee) / 100
        );
        summ += (msg.value * _metaspace_owner_fee) / 100;
        for (uint256 i = 0; i < _partners[_space_uid].length; i++) {
            payable(_partners[_space_uid][i].eth_address).send(
                (msg.value * _partners[_space_uid][i].percentage) / 100
            );
            summ += (msg.value * _partners[_space_uid][i].percentage) / 100;
        }
        for (uint256 i = 0; i < _curators[_space_uid].length; i++) {
            payable(_curators[_space_uid][i].eth_address).send(
                (msg.value * _curators[_space_uid][i].percentage) / 100
            );
            summ += (msg.value * _curators[_space_uid][i].percentage) / 100;
        }
        payable(order.owner_of).send(msg.value - summ);
        _submited_tokens[_space_uid][order.token_address][
            order.token_id
        ] = false;
        _tokens[_space_uid][_uid_local].is_sold = true;
    }

    function setMetaspaceToken(address token) external {
        require(msg.sender == _owner_of, "Only for owner");
        _metaspace_token_address = token;
    }

    function updateOwner(uint256 uid, address new_owner) external {
        require(msg.sender == _metaspace_token_address, "Only for platform");
        _metaSpaces[uid].owner_of = new_owner;
    }

    function getSpace(
        uint256 uid
    )
        public
        view
        returns (
            address owner_of,
            string memory uri,
            uint256 access_fee,
            uint256 submition_fee,
            uint256 owner_fee,
            address access_token_address,
            Partner[] memory partners,
            Curator[] memory curators,
            Token[] memory tokens
        )
    {
        MetaSpace memory _metaSpace = _metaSpaces[uid];
        Partner[] memory partners = _partners[uid];
        Curator[] memory curators = _curators[uid];
        Token[] memory tokens = _tokens[uid];
        {
            if (
                msg.sender != _metaSpace.owner_of &&
                !_is_partner_address[uid][msg.sender] &&
                !_is_curator_address[uid][msg.sender] &&
                _metaSpace.access_token_address != address(0) &&
                _metaSpace.access_fee != 0
            ) {
                require(
                    IERC20MetaSpace(_metaSpace.access_token_address).balanceOf(
                        msg.sender
                    ) >= _metaSpace.access_fee,
                    "Permission denied"
                );
            }
        }
        return (
            _metaSpace.owner_of,
            _metaSpace.uri,
            _metaSpace.access_fee,
            _metaSpace.submit_fee,
            _metaSpace.owner_fee,
            _metaSpace.access_token_address,
            partners,
            curators,
            tokens
        );
    }

    function getSpaceSecure(
        uint256 uid
    )
        public
        view
        returns (
            address owner_of,
            string memory uri,
            uint256 access_fee,
            uint256 submition_fee,
            address access_token_address
        )
    {
        MetaSpace memory _metaSpace = _metaSpaces[uid];
        return (
            _metaSpace.owner_of,
            _metaSpace.uri,
            _metaSpace.access_fee,
            _metaSpace.submit_fee,
            _metaSpace.access_token_address
        );
    }
}

// File: /contracts/Platform/Meta/IMetaSpace.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IMetaSpace {
    struct Metadata {
        string title;
        string short_description;
        string description;
        string scene_uri;
    }

    struct Access {
        address token_address;
        uint256 access_fee;
        uint256 submit_fee;
    }

    struct Partner {
        address eth_address;
        uint256 percentage;
    }

    struct Curator {
        address eth_address;
        uint256 percentage;
    }

    struct OwnerMeta {
        address owner_of_;
        uint256 owner_fee_;
    }

    struct Token {
        uint256 uid;
        uint256 space_uid;
        address token_address;
        uint256 token_id;
        uint256 amount;
        ContractType contract_type;
        uint256 price;
        bool is_sold;
        bool approved;
        bool canceled;
        address owner_of;
    }

    struct MetaSpace {
        uint256 uid;
        address owner_of;
        uint256 owner_fee;
        string uri;
        uint256 submit_fee;
        uint256 access_fee;
        address access_token_address;
    }

    enum ContractType {
        single_token,
        multiple_token
    }
}

interface IERC20MetaSpace {
    function balanceOf(address account) external view returns (uint256);
}

interface IERC721MetaSpace {
    function mint(address to, uint256 tokenId) external;
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function getApproved(uint256 tokenId) external view returns (address operator);
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
}

interface IERC1155MetaSpace {
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function isApprovedForAll(address account, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
}
// File: /contracts/Pausable.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @author MetaPlayerOne DAO
 * @title Pausable
 */
contract Pausable {
    address internal _owner_of;
    bool internal _paused = false;

    /**
     * @dev setup owner of this contract with paused off state.
     */
    constructor(address owner_of_) {
        _owner_of = owner_of_;
        _paused = false;
    }

    /**
     * @dev modifier which can be used on child contract for checking if contract services are paused.
     */
    modifier notPaused() {
        require(!_paused, "Contract is paused");
        _;
    }

    /**
     * @dev function which setup paused variable.
     * @param paused_ new boolean value of paused condition.
     */
    function setPaused(bool paused_) external {
        require(_paused == paused_, "Param has been asigned already");
        require(_owner_of == msg.sender, "Permission address");
        _paused = paused_;
    }

    /**
     * @dev function which setup owner variable.
     * @param owner_of_ new owner of contract.
     */
    function setOwner(address owner_of_) external {
        require(_owner_of == msg.sender, "Permission address");
        _owner_of = owner_of_;
    }

    /**
     * @dev function returns owner of contract.
     */
    function owner() public view returns (address) {
        return _owner_of;
    }
}

// File: @openzeppelin/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}