contract Cipher is ERC721, AccessControl, ReentrancyGuard {
    using SafeMath for uint256;
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

    address payable reserveAccount = 0xf29F7f151aB26c3F3C28cC400CFA6378A07b50da;

    struct Beat {
        uint256 priceFinney;
        uint256 numEditionsAllowed;
        uint256 numEditionsInWild;
        uint256 madeFromId;// source of this beat if remixed
        bool remixable;//can it be remixed?
    }

    Beat[] public beats;
    uint8 public secondaryEditionFee = 25;//
    uint8 public oneOfOneFee = 15;
    uint8 public primaryEditionFee = 10;

    bool public isMintable = true;

    event TokenMinted(uint256 tokenId);

    event TokenCloned(uint256 cloningCost, uint256 _numClonesRequested, uint256 _tokenId, address _to);
    event TokenBurned(uint256 _tokenId);

    modifier mintable {
        require(
            isMintable == true,
            "New beats are no longer mintable on this contract"
        );
        _;
    }

    constructor () ERC721("Cipher", "RAP") {
        // If the array is new, skip over the first index.
        if(beats.length == 0) {
            Beat memory _dummyBeat = Beat({
            priceFinney: 0,
            numEditionsAllowed: 0,
            numEditionsInWild: 0,
            madeFromId: 0,
            remixable: true
            });
            beats.push(_dummyBeat);
        }
    }

    function addAdmin(address _admin) public onlyAdmin {
        grantRole(ADMIN_ROLE, _admin);
    }
    
    function removeAdmin(address _admin) public onlyAdmin {
        require(!hasRole(DEFAULT_ADMIN_ROLE, msg.sender), 'You cannot remove owner as an admin! :D');
        revokeRole(ADMIN_ROLE, _admin);
    }

    function isAdmin() public view returns (bool){
        return hasRole(ADMIN_ROLE, msg.sender);
    }

    modifier onlyAdmin () {
        require(hasRole(ADMIN_ROLE, msg.sender) || hasRole(DEFAULT_ADMIN_ROLE, msg.sender), 'Not Admin nor Owner :P');
        _;
    }

    /// @dev mint(): Mint a new Gen0 Beats.  These are the tokens that other Beats will be "cloned from".
    /// @param _to Address to mint to.
    /// @param _priceFinney Price of the Beats in Finney.
    /// @param _numEditionsAllowed Maximum number of times this Beats is allowed to be cloned.
    /// @param _tokenURI A URL to the JSON file containing the metadata for the Beats.  See metadata.json for an example.
    /// @return tokenId the tokenId of the Beats that has been minted.  Note that in a transaction only the tx_hash is returned.
    function mint(address _to, uint256 _priceFinney, uint256 _numEditionsAllowed, string memory _tokenURI, bool _remixable) public mintable returns (uint256 tokenId) {

        Beat memory _beat = Beat({
        priceFinney: _priceFinney,
        numEditionsAllowed: _numEditionsAllowed,
        numEditionsInWild: 0,
        madeFromId: 0,
        remixable: _remixable
        });
        // The new beat is pushed onto the array and minted
        // Note that Solidity uses 0 as a default value when an item is not found in a mapping.
        tokenId = beats.length;
        beats.push(_beat);
        beats[tokenId].madeFromId = tokenId;

        _mint(_to, tokenId);
        _setTokenURI(tokenId, _tokenURI);
        emit TokenMinted(tokenId);
    }

    function mintNclone(address _to, uint256 _priceFinney, uint256 _numEditionsAllowed, string memory _tokenURI, bool _remixable) public payable mintable nonReentrant {
        uint256 tokenId = 0;
        bool oneOfone = _numEditionsAllowed ==  1 ? true : false;

        Beat memory _beat = Beat({
        priceFinney: _priceFinney,
        numEditionsAllowed: _numEditionsAllowed,
        numEditionsInWild: oneOfone ? 1 : 0,//only allow one edition for oneOfOne
        madeFromId: 0,
        remixable: _remixable
        });
        // The new beat is pushed onto the array and minted
        // Note that Solidity uses 0 as a default value when an item is not found in a mapping.
        tokenId = beats.length;
        beats.push(_beat);
        beats[tokenId].madeFromId = tokenId;

        uint256 cloningCost  = _beat.priceFinney.mul( 10**15 ).mul(1);
        uint256 reserveCut = (cloningCost.mul(10)).div(100);
        uint256 ownerCut;

        //if only one edition available we mint it to the caller wallet

        if(oneOfone == false)
        {
            _mint(_to, tokenId);
            _setTokenURI(tokenId, _tokenURI);
            clone(msg.sender, tokenId, 1);
        }
        else
        {
            require(
                msg.value >= cloningCost,
                "Not enough Wei to pay for the Beats clones.");
            _mint(msg.sender, tokenId);
            _setTokenURI(tokenId, _tokenURI);
            //we need to take the cuts to cipher and the artist

            address payable owner =  payable(_to);

            reserveCut = (cloningCost.mul(oneOfOneFee)).div(100);
            ownerCut = (cloningCost.mul(100-oneOfOneFee)).div(100);

            (bool reserveTransferSuccess, ) = reserveAccount.call{value: reserveCut}("");
            require(reserveTransferSuccess, "Reserve transfer failed.");
            (bool ownerTransferSuccess, ) = owner.call{value: ownerCut}("");
            require(ownerTransferSuccess, "Owner transfer failed.");
        }


        emit TokenMinted(tokenId);

        //return the last item id or a comfirmation message?
    }

    /// @dev clone(): Clone a new Beats from a Gen0 Beats.
    /// @param _to The address to clone to.
    /// @param _tokenId The token id of the Beats to clone and transfer.
    /// @param _numClonesRequested Number of clones to generate.
    function clone(address _to, uint256  _tokenId, uint256 _numClonesRequested) public payable mintable nonReentrant {

        // https://medium.com/swlh/understanding-ownership-and-role-based-access-with-solidity-and-open-zeppelin-dbd096e4bd99
        // Grab existing Beat blueprint
        Beat memory _beat = beats[_tokenId];
        uint256 cloningCost  = _beat.priceFinney.mul( 10**15 ).mul(_numClonesRequested);
        bool firstEdition = false;
        address payable owner =  payable(_to);

        require(
            _beat.numEditionsInWild + _numClonesRequested <= _beat.numEditionsAllowed,
            "The number of Beats clones requested exceeds the number of clones allowed.");
        require(
            msg.value >= cloningCost,
            "Not enough Wei to pay for the Beats clones.");

        uint256 reserveCut = (cloningCost.mul(10)).div(100);
        uint256 cutAmount = (cloningCost.mul(1000-secondaryEditionFee)).div(1000);


        firstEdition = _beat.numEditionsInWild == 0;
        reserveCut = (cloningCost.mul(secondaryEditionFee)).div(1000);

        // Pay cipher the cuts
        if(firstEdition){
            //Primary sales 10% cut
            reserveCut = (cloningCost.mul(primaryEditionFee)).div(100);
            cutAmount = (cloningCost.mul(100-primaryEditionFee)).div(100);//X% of the rest to artist
        }

        // owner.call{value: cutAmount}("");

        // //send the rest to the artist
        // owner.transfer(cutAmount);
        // reserveAccount.transfer(reserveCut);
        (bool ownerTransferSuccess, ) = owner.call{value: cutAmount}("");
        require(ownerTransferSuccess, "Owner transfer failed.");
        (bool reserveTransferSuccess, ) = reserveAccount.call{value: reserveCut}("");
        require(reserveTransferSuccess, "Reserve transfer failed.");


        // Update original beat struct in the array
        _beat.numEditionsInWild += _numClonesRequested;
        beats[_tokenId] = _beat;

        // Create new beat, don't let it be cloned
        for (uint i = 0; i < _numClonesRequested; i++) {
            Beat memory _newBeat;
            _newBeat.priceFinney = _beat.priceFinney;
            _newBeat.remixable = _beat.remixable;
            _newBeat.numEditionsAllowed = 0;
            _newBeat.numEditionsInWild = 0;
            _newBeat.madeFromId = _tokenId;

            // Note that Solidity uses 0 as a default value when an item is not found in a mapping.
            beats.push(_newBeat);
            uint256 newTokenId = beats.length-1;
            _tokenIds.increment();
            // Mint the new beats to the _to account
            _mint(_to, newTokenId);

            // Use the same tokenURI metadata from the Gen0 Beats
            string memory _tokenURI = tokenURI(_tokenId);
            _setTokenURI(newTokenId, _tokenURI);
        }

        // Return the any leftover ETH to the cloningCost
        // msg.sender.transfer( msg.value - cloningCost );

        (bool success, ) = msg.sender.call{value: msg.value - cloningCost}("");
        require(success, "Failed to return leftover ETH.");
        emit TokenCloned(cloningCost, _numClonesRequested, _tokenId, _to);
    }


    /// @dev burn(): Burn Beats token.
    /// @param _tokenId The Beats ID to be burned.
    function burn(uint256 _tokenId) public onlyAdmin {

        Beat memory _beat = beats[_tokenId];
        uint256 gen0Id = _beat.madeFromId;
        if (_tokenId != gen0Id) {
            Beat memory _gen0Beat = beats[gen0Id];
            _gen0Beat.numEditionsInWild -= 1;
            beats[gen0Id] = _gen0Beat;
        }
        delete beats[_tokenId];
        // _burn(_owner, _tokenId);
        _burn(_tokenId);
        emit TokenBurned(_tokenId);
    }

    /// @dev setSecondaryEditionFee(): Update the secondary Beats clone fee percentage.  Upon cloning a new beats,
    ///                               secondaryEditionFee will go to the contract owner, and
    ///                               (100 - secondaryEditionFee) will go to the Gen0 Beats owner.
    /// @param _secondaryEditionFee The percentage fee between 0 and 1000. default is 25 which is 2.5
    function setSecondaryEditionFee(uint8 _secondaryEditionFee) public onlyAdmin {
        require(
            _secondaryEditionFee >= 0 && _secondaryEditionFee <= 1000,
            "Invalid range for secondaryEditionFee.  Must be between 0 and 1000.");
        secondaryEditionFee = _secondaryEditionFee;
    }

    /// @dev setOneOfOneFee(): Update the one of one Beat fee percentage.  Upon miniting a 1 of 1 a new beats,
    ///                               setOneOfOneFee will go to cipher wallet, and
    ///                               (100 - oneOfOneFee) will go to the Gen0 Beats owner.
    /// @param _oneOfOneFee The percentage fee between 0 and 100.
    function setOneOfOneFee(uint8 _oneOfOneFee) public onlyAdmin {
        require(
            _oneOfOneFee >= 0 && _oneOfOneFee <= 100,
            "Invalid range for oneOfOneFee.  Must be between 0 and 100.");
        oneOfOneFee = _oneOfOneFee;
    }

    /// @dev setPrimaryEditionFee(): Update the primary Beat fee percentage.  Upon miniting a new beats,
    ///                               setPrimaryEditionFee will go to cipher wallet, and
    ///                               (100 - primaryEditionFee) will go to the Gen0 Beats owner.
    /// @param _primaryEditionFee The percentage fee between 0 and 100.
    function setPrimaryEditionFee(uint8 _primaryEditionFee) public onlyAdmin {
        require(
            _primaryEditionFee >= 0 && _primaryEditionFee <= 100,
            "Invalid range for primaryEditionFee.  Must be between 0 and 100.");
        primaryEditionFee = _primaryEditionFee;
    }

    /// @dev setMintable(): set the isMintable public variable.  When set to `false`, no new
    ///                     beats are allowed to be minted or cloned.  However, all of already
    ///                     existing beats will remain unchanged.
    /// @param _isMintable flag for the mintable function modifier.
    function setMintable(bool _isMintable) public onlyAdmin {
        isMintable = _isMintable;
    }

    /// @dev setPrice(): Update the Beats listing price.
    /// @param _tokenId The Beats Id.
    /// @param _newPriceFinney The new price of the Beats.
    function setPrice(uint256 _tokenId, uint256 _newPriceFinney) public onlyAdmin {
        Beat memory _beat = beats[_tokenId];

        _beat.priceFinney = _newPriceFinney;
        beats[_tokenId] = _beat;
    }

    /// @dev setTokenURI(): Set an existing token URI.
    /// @param _tokenId The token id.
    /// @param _tokenURI The tokenURI string.  Typically this will be a link to a json file on IPFS.
    function setTokenURI(uint256 _tokenId, string memory _tokenURI) public onlyAdmin {
        _setTokenURI(_tokenId, _tokenURI);
    }

    function getBeatsById(uint256 _tokenId) view public returns (uint256 priceFinney,
        uint256 numEditionsAllowed,
        uint256 numEditionsInWild,
        uint256 madeFromId
    )
    {
        Beat memory _beat = beats[_tokenId];

        priceFinney = _beat.priceFinney;
        numEditionsAllowed = _beat.numEditionsAllowed;
        numEditionsInWild = _beat.numEditionsInWild;
        madeFromId = _beat.madeFromId;
    }

    /// @dev getNumClonesInWild(): Return a Beats struct/array given a Beats Id.
    /// @param _tokenId The Beats Id.
    /// @return numEditionsInWild the number of cloes in the wild
    function getNumClonesInWild(uint256 _tokenId) view public returns (uint256 numEditionsInWild)
    {
        Beat memory _beat = beats[_tokenId];

        numEditionsInWild = _beat.numEditionsInWild;
    }

    /// @dev getLatestId(): Returns the newest X5Badges Id in the beats array.
    /// @return tokenId the latest beats id.
    function getLatestId() view public returns (uint256 tokenId)
    {
        if (beats.length == 0) {
            tokenId = 0;
        } else {
            tokenId = beats.length - 1;
        }
    }
}
/// @title Cipher
/// @author Youssef Khouidi <youssef@cipher.media>
/// @author Garett Laugavitz <garett@cipher.media>
/// auditor Benjamin Beale 
/// @notice Cipher ERC721 interface for minting, cloning, and transferring RAP tokens.
/// based on Kudos contract made by @author Jason Haas <jasonrhaas@gmail.com>
/// https://github.com/gitcoinco/gitcoin-erc721/blob/master/contracts/Kudos.sol
function setPrimaryEditionFee(uint8 _primaryEditionFee) public onlyAdmin {
    require(
        _primaryEditionFee >= 0 && _primaryEditionFee <= 100,
        "Invalid range for primaryEditionFee.  Must be between 0 and 100.");
    primaryEditionFee = _primaryEditionFee;
}

/// @dev setPrimaryEditionFee(): Update the primary Beat fee percentage.  Upon miniting a new beats,
///                               setPrimaryEditionFee will go to cipher wallet, and
///                               (100 - primaryEditionFee) will go to the Gen0 Beats owner.
/// @param _primaryEditionFee The percentage fee between 0 and 100.