// File: AlgocracyCollection.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8;

import "./iAlgocracyCollection.sol";
import "./AlgocracyCollectionNFT.sol";
import "./AlgocracyCollectionRegistry.sol";

/// @title Algocracy Collection
/// @author jolan.eth

contract AlgocracyCollection is AlgocracyCollectionNFT, AlgocracyCollectionRegistry {
    iAlgocracyCollection public DAO;

    constructor(
        address _DAO
    ) {
        DAO = iAlgocracyCollection(_DAO);
    }

    function name()
    public view returns (string memory) {
        return string(abi.encodePacked(DAO.name(), ' Collection'));
    }

    function symbol()
    public view returns (string memory) {
        return DAO.symbol();
    }

    function mintCollectionNFT(
        address _NFT, address _Prime,
        address _Owner, address _Provider,
        string memory _name, string memory _cover, string memory _description,
        uint256 _maxSupply
    ) public {
        require (
            DAO.Deployer() == msg.sender, 
            "AlgocracyCollection::mintCollectionNFT() - msg.sender is not DAO.Deployer()"
        );

        uint256 REGISTRY_IDENTIFIER = AlgocracyCollectionRegistry.collectionIndex++;
        AlgocracyCollectionRegistry.setCollectionRegistration(
            _NFT, _Prime, _Provider,
            _name, _cover, _description,
            _maxSupply, REGISTRY_IDENTIFIER
        );
        
        AlgocracyCollectionNFT._mint(_Owner);
    }

    function setCollectionState(
        uint256 id, bool isActive, uint256 maxQuantity, uint256 price
    ) public {
        require(
            AlgocracyCollectionNFT.ownerOf(id) == msg.sender,
            "AlgocracyCollection::setCollectionState() - msg.sender is not ownerOf id"
        );

        Mint memory oldState = AlgocracyCollectionRegistry.getCollectionState(id);
        Mint memory newState = Mint(isActive, oldState.isRandom, oldState.isAllowListed, maxQuantity, price);
        AlgocracyCollectionRegistry.CollectionRegistry[id].State = newState;
    }

    function setCollectionStateAllowListInternal(
        uint256 id, bool isAllowListed
    ) public {
        require(
            AlgocracyCollectionRegistry.getCollectionContract(id).Prime == msg.sender,
            "AlgocracyCollection::setCollectionStateInternal() - msg.sender is not Prime of id"
        );

        Mint memory oldState = AlgocracyCollectionRegistry.getCollectionState(id);
        Mint memory newState = Mint(oldState.isActive, oldState.isRandom, isAllowListed, oldState.maxQuantity, oldState.price);
        AlgocracyCollectionRegistry.CollectionRegistry[id].State = newState;
    }

    function setCollectionStateRandomInternal(
        uint256 id, bool isRandom
    ) public {
        require(
            AlgocracyCollectionRegistry.getCollectionContract(id).Prime == msg.sender,
            "AlgocracyCollection::setCollectionStateInternal() - msg.sender is not Prime of id"
        );

        Mint memory oldState = AlgocracyCollectionRegistry.getCollectionState(id);
        Mint memory newState = Mint(oldState.isActive, isRandom, oldState.isAllowListed, oldState.maxQuantity, oldState.price);
        AlgocracyCollectionRegistry.CollectionRegistry[id].State = newState;
    }
    
    function tokenURI(uint256 id)
    public view returns (string memory) {
        iAlgocracyCollection CollectionProvider = iAlgocracyCollection(DAO.CollectionProvider());

        require(
            AlgocracyCollectionNFT.exist(id),
            "AlgocracyCollection::tokenUri() - id do not exist"
        );
        
        return CollectionProvider.generateMetadata(id);
    }

    function owner()
    public view returns (address) {
        iAlgocracyCollection PassNFT = iAlgocracyCollection(DAO.PassNFT());

        return PassNFT.ownerOf(1);
    }
}
// File: AlgocracyCollectionNFT.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8;

interface iAlgocracyCollectionNFT {
    function onERC721Received(address operator, address from, uint256 id, bytes calldata data)
    external returns (bytes4);
}

/// @title Algocracy Collection NFT
/// @author jolan.eth

abstract contract AlgocracyCollectionNFT {
    uint256 supply;
    
    mapping (uint256 => address) owners;
    mapping (address => uint256) balances;
    
    mapping (uint256 => address) approvals;
    mapping (address => mapping(address => bool)) operatorApprovals;

    event Transfer(address indexed from, address indexed to, uint256 indexed id);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    event Approval(address indexed owner, address indexed approved, uint256 indexed id);

    function supportsInterface(bytes4 interfaceId)
    public pure returns (bool) {
        return interfaceId == 0x80ac58cd || interfaceId == 0x5b5e139f;
    }

    function exist(uint256 id)
    public view returns (bool) {
        return owners[id] != address(0);
    }

    function totalSupply()
    public view returns (uint256) {
        return supply;
    }

    function balanceOf(address owner)
    public view returns (uint256) {
        require(
            owner != address(0),
            "AlgocracyCollectionNFT::balanceOf() - owner is address(0)"
        );
        return balances[owner];
    }

    function ownerOf(uint256 id)
    public view returns (address) {
        require(
            exist(id),
            "AlgocracyCollectionNFT::ownerOf() - id do not exist"
        );
        return owners[id];
    }

    function isApprovedForAll(address owner, address operator)
    public view returns (bool) {
        return operatorApprovals[owner][operator];
    }

    function getApproved(uint256 id)
    public view returns (address) {
        require(
            exist(id),
            "AlgocracyCollectionNFT::getApproved() - id do not exist"
        );
        return approvals[id];
    }

    function approve(address to, uint256 id)
    public {
        address owner = owners[id];
        require(
            to != owner,
            "AlgocracyCollectionNFT::approve() - to is owner"
        );
        require(
            owner == msg.sender ||
            operatorApprovals[owner][msg.sender],
            "AlgocracyCollectionNFT::approve() - msg.sender is not owner or approved"
        );
        approvals[id] = to;
        emit Approval(owner, to, id);
    }

    function setApprovalForAll(address operator, bool approved)
    public {
        require(
            operator != msg.sender,
            "AlgocracyCollectionNFT::setApprovalForAll() - msg.sender is operator"
        );
        operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function transferFrom(address from, address to, uint256 id)
    public {
        address owner = owners[id];
        require(
            exist(id),
            "AlgocracyCollectionNFT::transferFrom() - id do not exist"
        );
        require(
            msg.sender == owner ||
            msg.sender == approvals[id] ||
            operatorApprovals[owner][msg.sender],
            "AlgocracyCollectionNFT::transferFrom() - msg.sender is not owner or approved"
        );

        _transfer(from, to, id);
    }

    function safeTransferFrom(address from, address to, uint256 id)
    public {
        address owner = owners[id];
        require(
            exist(id),
            "AlgocracyCollectionNFT::safeTransferFrom() - id do not exist"
        );
        require(
            msg.sender == owner ||
            msg.sender == approvals[id] ||
            operatorApprovals[owner][msg.sender],
            "AlgocracyCollectionNFT::safeTransferFrom() - msg.sender is not owner or approved"
        );

        _safeTransfer(from, to, id, '');
    }

    function safeTransferFrom(address from, address to, uint256 id, bytes memory data)
    public {
        address owner = owners[id];
        require(
            exist(id),
            "AlgocracyCollectionNFT::safeTransferFrom() - id do not exist"
        );
        require(
            msg.sender == owner ||
            msg.sender == approvals[id] ||
            operatorApprovals[owner][msg.sender],
            "AlgocracyCollectionNFT::safeTransferFrom() - msg.sender is not owner or approved"
        );

        _safeTransfer(from, to, id, data);
    }

    function _mint(address to)
    internal {
        balances[to]++;
        owners[supply] = to;
        emit Transfer(address(this), to, supply++);
    }

    function _transfer(address from, address to, uint256 id)
    private {
        require(
            address(0) != to,
            "AlgocracyCollectionNFT::_transferFrom() - to is address(0)"
        );

        approve(address(0), id);
        balances[from]--;
        balances[to]++;
        owners[id] = to;
        
        emit Transfer(from, to, id);
    }

    function _safeTransfer(address from, address to, uint256 id, bytes memory data)
    private {
        _transfer(from, to, id);
        require(
            _checkOnERC721Received(from, to, id, data),
            "AlgocracyCollectionNFT::_safeTransfer() - to is not ERC721 receiver"
        );
    }

    function _checkOnERC721Received(address from, address to, uint256 id, bytes memory _data)
    internal returns (bool) {
        uint256 size;

        assembly {
            size := extcodesize(to)
        }

        if (size > 0)
            try iAlgocracyCollectionNFT(to).onERC721Received(msg.sender, from, id, _data) returns (bytes4 retval) {
                return retval == iAlgocracyCollectionNFT(to).onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) revert("AlgocracyCollectionNFT::_checkOnERC721Received() - to is not receiver");
                else assembly {
                        revert(add(32, reason), mload(reason))
                    }
            }
        else return true;
    }
}
// File: AlgocracyCollectionRegistry.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8;

/// @title Algocracy Collection Registry
/// @author jolan.eth

abstract contract AlgocracyCollectionRegistry {
    struct Module {
        address NFT;
        address Prime;
        address Provider;
    }

    struct Meta {
        string name;
        string cover;
        string description;
        uint256 maxSupply;
        uint256 blockNumber;
    }

    struct Mint {
        bool isActive;
        bool isRandom;
        bool isAllowListed;
        uint256 maxQuantity;
        uint256 price;
    }

    struct Collection {
        Meta Data;
        Mint State;
        Module Contract;
    }

    uint256[2] public FIXED_NAME = [1,15];
    uint256[2] public FIXED_DESCRIPTION = [1,140];
    uint256[2] public FIXED_MAX_SUPPLY = [1,1000];

    uint256 collectionIndex;
    mapping(uint256 => Collection) CollectionRegistry;

    function getCollectionRegistryLength()
    public view returns (uint256) {
        return collectionIndex;
    }

    function getCollectionData(uint256 id)
    public view returns (Meta memory) {
        return CollectionRegistry[id].Data;
    }

    function getCollectionContract(uint256 id)
    public view returns (Module memory) {
        return CollectionRegistry[id].Contract;
    }

    function getCollectionState(uint256 id)
    public view returns (Mint memory) {
        return CollectionRegistry[id].State;
    }

    function getCollectionRegistry(uint256 id)
    public view returns (Collection memory) {
        return CollectionRegistry[id];
    }

    function setCollectionRegistration(
        address _NFT, address _Prime, address _Provider,
        string memory _name, string memory _cover, string memory _description,
        uint256 _maxSupply, uint256 _REGISTRY_IDENTIFIER
    ) internal {
        require(
            _maxSupply >= FIXED_MAX_SUPPLY[0] && _maxSupply <= FIXED_MAX_SUPPLY[1],
            "AlgocracyCollectionRegistry::setCollectionRegistration() - _max supply is out of bound"
        );

        require(
            bytes(_name).length >= FIXED_NAME[0] && bytes(_name).length <= FIXED_NAME[1],
            "AlgocracyCollectionRegistry::setCollectionRegistration() - _name is out of bound"
        );

        require(
            bytes(_description).length >= FIXED_DESCRIPTION[0] && bytes(_description).length <= FIXED_DESCRIPTION[1],
            "AlgocracyCollectionRegistry::setCollectionRegistration() - _description is out of bound"
        );

        Module memory Contract = Module(
            _NFT, _Prime, _Provider
        );

        Meta memory Data = Meta(
            _name, _cover, _description, 
            _maxSupply, block.number
        );

        Mint memory State = Mint(
            false, false, false, 0, 0
        );

        Collection memory newCollection = Collection(
            Data, State, Contract
        );

        CollectionRegistry[_REGISTRY_IDENTIFIER] = newCollection;
    }
}
// File: iAlgocracyCollection.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8;

interface iAlgocracyCollection {
    /// AlgocracyDAO::
    function VRFConsumer() external view returns (address);
    
    function Deployer() external view returns (address);
    function NFTFactory() external view returns (address);
    function PrimeFactory() external view returns (address);

    function PassNFT() external view returns (address);
    function CollectionNFT() external view returns (address);
    function RandomNFT() external view returns (address);

    function PassProvider() external view returns (address);
    function CollectionProvider() external view returns (address);

    function chain() external view returns (uint256);

    /// AlgocracyPass::
    function LOCK() external view returns (bool);
    function ACCESS_LEVEL_CORE() external view returns (uint256);
    function ACCESS_LEVEL_VETOER() external view returns (uint256);
    function ACCESS_LEVEL_OPERATOR() external view returns (uint256);
    function ACCESS_LEVEL_BASE() external view returns (uint256);
    function getAccessLevel(uint256) external view returns (uint256);
    function mintPassFromCollection(address, uint256) external;
    function mintPassFromEvent(address) external;
    function mintPassFromCore(address) external;
    
    /// AlgocracyCollection::
    struct Meta{string name; string cover; string description; uint256 maxSupply; uint256 blockNumber;}
    struct Mint{bool isActive;bool isRandom;bool isAllowListed;uint256 maxQuantity;uint256 price;}
    struct Module{address NFT; address Prime; address Provider;}
    function getCollectionRegistryLength() external view returns (uint256);
    function getCollectionData(uint256) external view returns (Meta memory);
    function getCollectionState(uint256) external view returns (Mint memory);
    function getCollectionContract(uint256) external view returns (Module memory);
    function setCollectionStateAllowListInternal(uint256, bool) external;
    function setCollectionStateRandomInternal(uint256, bool) external;
    function mintCollectionNFT(
        address, address, address, address,
        string memory, string memory, string memory,
        uint256
    ) external;

    /// AlgocracyRandom
    struct Random {uint256 id;uint256 provableRandom;}
    function FIXED_TIC() external view returns (uint16);
    function FIXED_GAS() external view returns (uint32);
    function FIXED_QTY() external view returns (uint32);
    function getRandomRegistry(uint256) external view returns (Random memory);
    function mintRandom(uint256, uint256) external;

    /// AlgocracyFactory::
    function deployAlgocracyNFT(address, uint256) external returns (address);
    function deployAlgocracyPrime(uint256) external returns (address);

    /// AlgocracyERC721::
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function maxSupply() external view returns (uint256);
    function totalSupply() external view returns (uint256);
    function exist(uint256) external view returns (bool);
    function balanceOf(address) external view returns (uint256);
    function ownerOf(uint256) external view returns (address);
    function ownedBy(address) external view returns (uint256);
    function mintSequentialNFT(address, uint256) external;
    function mintRandomNFT(address, uint256[] memory) external;

    /// AlgocracyProvider::
    function generateMetadata(uint256 id) external view returns (string memory);

    /// AlgocracyUtils::Regex
    function matches(string memory) external pure returns (bool);

    /// AlgocracyVRF::
    function getRequestConfig() external view returns (uint16, uint32, bytes32[] memory);
    function requestRandomWords(bytes32, uint64, uint16, uint32, uint32 ) external returns (uint256);
    function createSubscription() external returns (uint64);
    function getSubscription(uint64) external view returns (uint96, uint64, address, address[] memory);
    function requestSubscriptionOwnerTransfer(uint64, address) external;
    function acceptSubscriptionOwnerTransfer(uint64) external;
    function addConsumer(uint64, address) external;
    function removeConsumer(uint64, address) external;
    function cancelSubscription(uint64, address) external;
    function pendingRequestExists(uint64) external view returns (bool);
}