contract LootCreativeDomain is ReentrancyGuard {
    using SafeMath for uint256;

    uint256 private registryPrice = 50000000000000000; // initiated at 0.05 ETH
    uint256 private customPropPrice = 10000000000000000; // initiated at 0.01 ETH

    uint256 private constant NUM_LOOT = 8000;
    uint256 public protocolClaimableFees;

    address public lootAddress = 0xFF9C1b15B16263C61d017ee9F65C50e4AE0113D7;
    LootInterface lootContract = LootInterface(lootAddress);

    address public gov;
    address public protocol;
    IERC20 public lcdToken;

    mapping(address => Project) projectRegistry;

    mapping(address => mapping(uint256 => string)) projectToTokenIdToLambdaProp;
    mapping(address => mapping(uint256 => string)) projectToTokenIdToOmegaProp;
    mapping(address => mapping(uint256 => string)) projectToTokenIdToCustomURI;

    mapping(address => uint256) affiliateOrCuratorFeesClaimable;
    mapping(address => mapping(uint256 => bool)) tokenRegistry;

    event ProjectRegister(
        address indexed _project,
        uint256 _lootPrice,
        uint256 _nonLootPrice,
        uint256 _nonLootMintCap,
        uint256 _curatorMintCap,
        uint256 _timestamp,
        bool _isCustomProject
    );

    event Endorse(
        address indexed _project,
        uint256 _amountPerMint,
        uint256 _timestamp
    );

    event RevokeEndorse(
        address indexed _project,
        uint256 _timestamp
    );

    event LCDMint(
        address indexed _project,
        address indexed _minter,
        uint256 _tokenId,
        bool _mintWithLoot
    );

    event FeeClaim(
        address indexed _claimer,
        uint256 _amount,
        uint256 _timestamp
    );

    event ProtocolClaim(
        uint256 _amount,
        uint256 _timestamp
    );

    event LambdaPropSet(
        address indexed _project,
        uint256 indexed _tokenId,
        string _lambdaProp,
        address indexed _affiliate
    );

    event OmegaPropSet(
        address indexed _project,
        uint256 indexed _tokenId,
        string _omegaProp,
        address indexed _affiliate
    );

    event CustomURISet(
        address indexed _project,
        uint256 indexed _tokenId,
        string _customURI,
        address indexed _affiliate
    );

    struct Project {
        address curator;
        uint256 lootPrice;
        uint256 nonLootPrice;
        uint256 nonLootMintCap;
        uint256 nonLootMints;
        uint256 curatorMintCap;
        uint256 curatorMints;
        uint256 endorsementPerMint;
        bool isCustomURIEnabled;
        bool isCustomProject;
    }

    constructor(IERC20 _lcdToken) public {
        gov = msg.sender;
        protocol = msg.sender;
        lcdToken = _lcdToken;
    }

    modifier onlyGov {
        require(msg.sender == gov, "Not gov");
        _;
    }

    modifier onlyProtocol {
        require(msg.sender == protocol, "Not protocol");
        _;
    }

    /*
    *
    * Mint
    *
    */

    /*
    * Mint on standard projects as Loot holder
    */
    function mintWithLoot(address _project, uint256 _lootId) public payable nonReentrant {
        Project memory project = projectRegistry[_project];
        require(lootContract.ownerOf(_lootId) == msg.sender, "Not owner");
        require(msg.value == project.lootPrice, "Incorrect value");
        require(!project.isCustomProject, "Custom project");

        LCDProject(_project).mintWithLoot(msg.sender, _lootId);

        _registerId(_project, _lootId);
        _registerFeesFromMint(_project, project.lootPrice);
        _distributeEndorsement(msg.sender, project.endorsementPerMint);

        emit LCDMint(_project, msg.sender, _lootId, true);
    }

    /*
    * Mint on standard projects as non-Loot holder
    * Note that the tokenId is not accepted as a param; it's generated linearly
    */
    function mintWithoutLoot(address _project) public payable nonReentrant {
        Project memory project = projectRegistry[_project];
        require(msg.value == project.nonLootPrice, "Incorrect value");
        require(project.nonLootMints < project.nonLootMintCap, "Capped");
        require(!project.isCustomProject, "Custom project");

        project.nonLootMints++;
        uint256 tokenId = NUM_LOOT.add(project.nonLootMints);

        LCDProject(_project).mintWithoutLoot(msg.sender, tokenId);

        _registerId(_project, tokenId);
        _registerFeesFromMint(_project, project.nonLootPrice);
        _distributeEndorsement(msg.sender, project.endorsementPerMint);

        emit LCDMint(_project, msg.sender, tokenId, false);
    }

    /*
    * Mint on custom projects as anyone
    */
    function mintCustom(address _project, uint256 _tokenId) public payable {
        Project memory project = projectRegistry[_project];
        require(project.isCustomProject, "Not custom project");
        require(msg.value == project.nonLootPrice, "Incorrect value");
        require(_tokenId > 0 && _tokenId <= project.nonLootMintCap, "Invalid id");

        LCDCustomProject(_project).mintCustom(msg.sender, _tokenId);
        project.nonLootMints++;

        _registerId(_project, _tokenId);
        _registerFeesFromMint(_project, project.nonLootPrice);
        _distributeEndorsement(msg.sender, project.endorsementPerMint);

        emit LCDMint(_project, msg.sender, _tokenId, false);
    }

    /*
    * Mint on standard projects as curator
    */
    function mintAsCurator(address _project, uint256 _tokenId) public {
        Project memory project = projectRegistry[_project];
        require(msg.sender == project.curator, "Not curator");
        require(!project.isCustomProject, "Custom project");
        require(project.curatorMints < project.curatorMintCap, "No more mints");
        require(
            _tokenId > NUM_LOOT.add(project.nonLootMintCap) &&
            _tokenId <= NUM_LOOT.add(project.nonLootMintCap).add(project.curatorMintCap),
            "Invalid id"
        );

        LCDProject(_project).mintAsCurator(msg.sender, _tokenId);
        _registerId(_project, _tokenId);

        emit LCDMint(_project, msg.sender, _tokenId, false);
    }

    /*
    * Mint on custom projects as curator
    */
    function mintCustomAsCurator(address _project, uint256 _tokenId) public {
        Project memory project = projectRegistry[_project];
        require(msg.sender == project.curator, "Not curator");
        require(project.isCustomProject, "Not custom project");
        require(
            _tokenId > project.nonLootMintCap &&
            _tokenId <= project.nonLootMintCap.add(project.curatorMintCap), "Invalid id"
        );

        LCDCustomProject(_project).mintCustomAsCurator(msg.sender, _tokenId);
        _registerId(_project, _tokenId);

        emit LCDMint(_project, msg.sender, _tokenId, false);
    }

    /*
    *
    * Gov
    *
    */

    /*
    * Called to incentivize minters with LCD tokens on certain projects
    */
    function endorse(address _project, uint256 _endorsementPerMint) public onlyGov {
        require(_endorsementPerMint <= 5e16, "Too high");
        projectRegistry[_project].endorsementPerMint = _endorsementPerMint;

        emit Endorse(_project, _endorsementPerMint, block.timestamp);
    }

    /*
    * Called to no longer incentivize a project
    */
    function revokeEndorsement(address _project) public onlyGov {
        projectRegistry[_project].endorsementPerMint = 0;

        emit RevokeEndorse(_project, block.timestamp);
    }

    /*
    * Change ETH amount required to register custom prop on LCD (wei notation)
    */
    function changeCustomPropPrice(uint256 _newPrice) public onlyGov {
        customPropPrice = _newPrice;
    }

    /*
    * Change ETH amount required to register project on LCD (wei notation)
    */
    function changeRegistryPrice(uint256 _newPrice) public onlyGov {
        registryPrice = _newPrice;
    }

    /*
    * Withdraw ERC20s from contract
    */
    function withdrawTokens(address token) public onlyGov {
        IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
    }

    /*
    * Set new gov address
    */
    function setGov(address _gov) public onlyGov {
        require(_gov != address(0));
        gov = _gov;
    }

    /*
    * Set new protocol address
    */
    function setProtocol(address _protocol) public onlyProtocol {
        require(_protocol != address(0));
        protocol = _protocol;
    }

    /*
    *
    * Claim Fees
    *
    */

    /*
    * Affiliate or curator claim of ETH fees
    */
    function claim(address _claimer) public nonReentrant {
        require(msg.sender == _claimer, "Not affiliate/curator");
        uint256 claimable = affiliateOrCuratorFeesClaimable[_claimer];
        require(claimable > 0, "Nothing to claim");
        affiliateOrCuratorFeesClaimable[_claimer] = 0;

        (bool sent, ) = _claimer.call{value: claimable}("");
        require(sent, "Failed");

        emit FeeClaim(_claimer, claimable, block.timestamp);
    }

    /*
    * Protocol claim of ETH fees
    */
    function protocolClaim() public onlyProtocol {
        uint256 claimable = protocolClaimableFees;
        protocolClaimableFees = 0;
        (bool sent, ) = protocol.call{value: claimable}("");
        require(sent, "Failed");

        emit ProtocolClaim(claimable, block.timestamp);
    }

    /*
    *
    * Curator
    *
    */

    /*
    * Registers NFT project where Loot owners are entitled to mint with respective lootId
    * _project: NFT address
    * _lootPrice: ETH payable per Loot mint (can be 0) - wei notation
    * _nonLootPrice: ETH payable per non-Loot mint (can be 0) - wei notation
    * _nonLootMintCap: Tokens mintable to non-Loot owners
    * _curatorMintCap: Tokens mintable by curator
    * _isCustomURIEnabled: bool for whether token holders can set Custom URI for token on LCD contract
    */
    function registerProject(
        address _project,
        uint256 _lootPrice,
        uint256 _nonLootPrice,
        uint256 _nonLootMintCap,
        uint256 _curatorMintCap,
        bool _isCustomURIEnabled
    ) public payable {
        require(msg.value == registryPrice, "Incorrect value");
        Project storage project = projectRegistry[_project];
        require(project.curator == address(0), "Project exists");

        project.curator = msg.sender;
        project.lootPrice = _lootPrice;
        project.nonLootPrice = _nonLootPrice;
        project.nonLootMintCap = _nonLootMintCap;
        project.curatorMintCap = _curatorMintCap;
        project.isCustomURIEnabled = _isCustomURIEnabled;

        _registerFeesFromProp(address(0), registryPrice);

        emit ProjectRegister(_project, _lootPrice, _nonLootPrice, _nonLootMintCap, _curatorMintCap, block.timestamp, false);
    }

    /*
    * Registers NFT project where minting is not linked to Loot ownership
    * _project: NFT address
    * _price: ETH payable per mint (can be 0) - wei notation
    * _mintCap: total Rokens mintable by public
    * _curatorMintCap: Tokens mintable by curator
    * _isCustomURIEnabled: bool for whether token holders can set Custom URI for token on LCD contract
    */
    function registerCustomProject(
        address _project,
        uint256 _price,
        uint256 _mintCap,
        uint256 _curatorMintCap,
        bool _isCustomURIEnabled
    ) public payable {
        require(msg.value == registryPrice, "Incorrect value");
        Project storage project = projectRegistry[_project];
        require(project.curator == address(0), "Project exists");

        project.curator = msg.sender;
        project.nonLootPrice = _price;
        project.nonLootMintCap = _mintCap;
        project.curatorMintCap = _curatorMintCap;
        project.isCustomProject = true;
        project.isCustomURIEnabled = _isCustomURIEnabled;

        _registerFeesFromProp(address(0), registryPrice);

        emit ProjectRegister(_project, 0, _price, _mintCap, _curatorMintCap, block.timestamp, true);
    }

    /*
    * Changes curator of project and recipient of future mint fees
    */
    function changeCurator(address _project, address _newCurator) public {
        require(msg.sender == projectRegistry[_project].curator, "Not curator");
        require(_newCurator != address(0));
        projectRegistry[_project].curator = _newCurator;
    }

    /*
    *
    * Props
    *
    */

    /*
    * Set a custom string prop
    * Lambda prop has no specific intended use case. Developers can use this
    * prop to unlock whichever features or experiences they want to incorporate
    * into their creation
    * _affiliate is (for example) the developer of gaming or visual experience
    * that integrates the NFT
    * Affiliate earns 80% of ETH fee
    */
    function setLambdaProp(
        address _project,
        uint256 _tokenId,
        string memory _lambdaProp,
        address _affiliate
    ) public payable nonReentrant {
        require(msg.sender == LCDProject(_project).ownerOf(_tokenId), "Not owner");
        require(msg.value == customPropPrice, "Incorrect value");

        projectToTokenIdToLambdaProp[_project][_tokenId] = _lambdaProp;
        _registerFeesFromProp(_affiliate, customPropPrice);

        emit LambdaPropSet(_project, _tokenId, _lambdaProp, _affiliate);
    }

    /*
    * Set a custom string prop
    * Omega prop has no specific intended use case. Developers can use this
    * prop to unlock whichever features or experiences they want to incorporate
    * into their creation
    * _affiliate is (for example) the developer of gaming or visual experience
    * that integrates the NFT
    * Omega prop price == 2x Lambda prop price
    * Affiliate earns 80% of ETH fee
    */
    function setOmegaProp(
        address _project,
        uint256 _tokenId,
        string memory _omegaProp,
        address _affiliate
    ) public payable nonReentrant {
        require(msg.sender == LCDProject(_project).ownerOf(_tokenId), "Not owner");
        require(msg.value == customPropPrice * 2, "Incorrect value");

        projectToTokenIdToOmegaProp[_project][_tokenId] = _omegaProp;
        _registerFeesFromProp(_affiliate, customPropPrice * 2);

        emit OmegaPropSet(_project, _tokenId, _omegaProp, _affiliate);
    }

    /*
    * LCD allows token holders to set a custom URI of their choosing if curator has enabled feature
    * See LootVanGogh project for example use case, where rarity/properties are returned statically
    * via getProps but user can modify custom URI interpretation of those props
    * Example of _customURI prop would be an IPFS url
    * _affiliate is (for example) the developer of gaming or visual experience
    * that integrates the NFT
    * Affiliate earns 80% of ETH fee
    */
    function setCustomURI(
        address _project,
         uint256 _tokenId,
         string memory _customURI,
         address _affiliate
    ) public payable nonReentrant {
        require(projectRegistry[_project].isCustomURIEnabled, "Disabled");
        require(msg.sender == LCDProject(_project).ownerOf(_tokenId), "Not owner");
        require(msg.value == customPropPrice, "Incorrect value");

        projectToTokenIdToCustomURI[_project][_tokenId] = _customURI;
        _registerFeesFromProp(_affiliate, customPropPrice);

        emit CustomURISet(_project, _tokenId, _customURI, _affiliate);
    }

    /*
    *
    * Reads
    *
    */

    /*
    * Returns whether token is included in LCD canonical registry
    */
    function isTokenRegistered(address _project, uint256 _tokenId) public view returns(bool){
        return tokenRegistry[_project][_tokenId];
    }

    /*
    * Returns a custom string set on LCD contract via setLambdaProp
    * Lambda prop has no specific intended use case. Developers can use this
    * prop to unlock whichever features or experiences they want to incorporate
    * into their creation
    */
    function getLambdaProp(address _project, uint256 _tokenId) public view returns(string memory){
        return projectToTokenIdToLambdaProp[_project][_tokenId];
    }

    /*
    * Returns a custom string set on LCD contract via setOmegaProp
    * Omega prop has no specific intended use case. Developers can use this
    * prop to unlock whichever features or experiences they want to incorporate
    * into their creation
    */
    function getOmegaProp(address _project, uint256 _tokenId) public view returns(string memory){
        return projectToTokenIdToOmegaProp[_project][_tokenId];
    }

    /*
    * Returns either a custom URI set on LCD contract or tokenURI from respective project contract
    */
    function tokenURI(address _project, uint256 _tokenId) public view returns(string memory){
        if(bytes(projectToTokenIdToCustomURI[_project][_tokenId]).length > 0){
            return projectToTokenIdToCustomURI[_project][_tokenId];
        }
        return LCDProject(_project).tokenURI(_tokenId);
    }

    /*
    * Randomly-generated, constantly changing number for a given token to be used interpretatively
    * (as creator sees fit) on contracts, frontends, game experiences, etc.
    */
    function getRandomProp(address _project, uint256 _tokenId) public view returns(uint256){
        return uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp, _project, _tokenId))).div(1e18);
    }

    function getCustomTokenURI(address _project, uint256 _tokenId) public view returns(string memory){
        return projectToTokenIdToCustomURI[_project][_tokenId];
    }

    /*
    * Returns concatenated prop string for tokenId with global LCD properties
    * and project-specific properties
    */
    function getProps(address _project, uint256 _tokenId) public view returns(string memory){
        require(isTokenRegistered(_project, _tokenId), "Unregistered");
        return string(
            abi.encodePacked(
                LCDProject(_project).getProps(_tokenId),
                " + lambda:",
                getLambdaProp(_project, _tokenId),
                " + omega:",
                getOmegaProp(_project, _tokenId),
                " + URI:",
                tokenURI(_project, _tokenId)
            )
        );
    }

    /*
    * Returns registry price, custom URI/lambdaProp price, omegaPropPrice
    */
    function getPropPrices() public view returns(uint256, uint256, uint256){
        return (registryPrice, customPropPrice, customPropPrice * 2);
    }

    /*
    * Returns claimable ETH amount for given affiliate or curator
    */
    function getAffiliateOrCuratorClaimable(address _claimer) public view returns(uint256){
        return affiliateOrCuratorFeesClaimable[_claimer];
    }

    /*
    * Returns basic project info
    */
    function getBasicProjectInfo(address _project) public view returns(
        address,
        uint256,
        uint256,
        uint256,
        bool
    ){
        return (
            projectRegistry[_project].curator,
            projectRegistry[_project].lootPrice,
            projectRegistry[_project].nonLootPrice,
            projectRegistry[_project].endorsementPerMint,
            projectRegistry[_project].isCustomProject
        );
    }

    /*
    * Returns advanced project info
    */
    function getAdvancedProjectInfo(address _project) public view returns(
        uint256,
        uint256,
        uint256,
        bool
    ) {
        return (
            projectRegistry[_project].nonLootMints,
            projectRegistry[_project].curatorMintCap,
            projectRegistry[_project].curatorMints,
            projectRegistry[_project].isCustomURIEnabled
        );
    }

    function isProjectEndorsed(address _project) public view returns(bool){
        return projectRegistry[_project].endorsementPerMint > 0;
    }

    function getOwnerOf(address _project, uint256 _tokenId) public view returns(address){
        if(tokenRegistry[_project][_tokenId]){
            return LCDProject(_project).ownerOf(_tokenId);
        }
        return address(0);
    }

    /*
    *
    * Private
    *
    */

    /*
    * Distributes LCD tokens to minters of endorsed projects
    */
    function _distributeEndorsement(address _minter, uint256 _amount) private {
        if(_amount > 0 && lcdToken.balanceOf(address(this)) >= _amount){
            lcdToken.transfer(_minter, _amount);
        }
    }

    /*
    * Registers tokenIds to global registry
    */
    function _registerId(address _project, uint256 _tokenId) private {
        require(!tokenRegistry[_project][_tokenId], "Already registered");
        tokenRegistry[_project][_tokenId] = true;
    }

    /*
    * Registers respective shares of mint fee to curator and protocol
    */
    function _registerFeesFromMint(address _project, uint256 _amount) private {
        if(_amount > 0){
            uint256 protocolShare = _amount.div(5);
            affiliateOrCuratorFeesClaimable[projectRegistry[_project].curator] =
                affiliateOrCuratorFeesClaimable[projectRegistry[_project].curator].add(_amount.sub(protocolShare));
            protocolClaimableFees = protocolClaimableFees.add(protocolShare);
        }
    }

    /*
    * Registers respective shares of prop or registry fee to affiliate and protocol
    */
    function _registerFeesFromProp(address _affiliate, uint256 _amount) private {
        if(_affiliate == address(0)){
            protocolClaimableFees = protocolClaimableFees.add(_amount);
        } else {
            uint256 protocolShare = _amount.div(5);
            affiliateOrCuratorFeesClaimable[_affiliate] = affiliateOrCuratorFeesClaimable[_affiliate].add(_amount.sub(protocolShare));
            protocolClaimableFees = protocolClaimableFees.add(protocolShare);
        }
    }
}
function getLambdaProp(address _project, uint256 _tokenId) public view returns(string memory){
    return projectToTokenIdToLambdaProp[_project][_tokenId];
}

/*
 * Returns a custom string set on LCD contract via setLambdaProp
 * Lambda prop has no specific intended use case. Developers can use this
 * prop to unlock whichever features or experiences they want to incorporate
 * into their creation
 */