contract LookRevLeafNFT is
    ERC721,
    ILookRevLeafNFT
    {
    mapping (address => uint256[]) private ownerNFTTokens;
    mapping (uint256 => uint256) public tokenCredits;

    uint256 public nextTokenId = 1;

    /// inital conversion rate is 1 ETH = 200,000 LooksCoin
    uint256 public COIN_RATE = 200000;

    /// LooksCoin credit for new LeafNFT token
    uint256 constant public INIT_CREDIT = 10 ** 15;

    /// Cost of leafNFT changes with the growth of the tree
    /// Initial cost of leafNFT is 0.05 ETH
    uint256 public averageCost = 5 * 10 ** 16;
    /// Maximum amount of LeafNFT allowed to mint at a time
    uint256 public maxBatchCount = 10;
    
    address private admin;
    address private ceo;
    address private cfo;
    address private treeStorage;

    /// LooksCoin contract
    IERC20 public _looksCoin;
    /// LookRevTree contract
    ILookRevTree public _lrTree;
    address[] private treeVersions;
    address[] private tokenVersions;
    
    event CreditRedeemed(
        string indexed str,
        address indexed recipient,
        uint256 indexed amount
    );
    
    event RoleUpdated(
        string indexed str,
        address indexed newaddress
    );

    /// constructor
    constructor() ERC721("LookRev LeafNFT", "LOOKNFT") {
        admin = msg.sender;
    }

    /// @dev Mint yourself a leafNFT
    /// @dev Check averageCost to get cost of leaf on the branch.
    /// This function is going to mint a NFTToken on the open branch of the specified genesis branch.
    /// If the amount sent is more than the cost of the leaf, the remaining amount will be minted into
    /// LooksCoin ERC20 tokens, which will be deposited at the recipient's address.
    /// @param genesisBranch is the id of the genesis branch, new leaf is added to its open branch
    /// @param recipient is the recipient wallet address for receiving ERC721 leafNFT token and
    /// LooksCoin ERC20 tokens.
    function mintLeafNFT(uint256 genesisBranch, address recipient) external payable {
        require(recipient != address(0), "LookRevLeafNFT: recipient needs valid wallet address");
        require(msg.value > averageCost, "LookRevLeafNFT: Amount sent is too small");
        
        _safeMint(recipient, nextTokenId);
        ownerNFTTokens[recipient].push(nextTokenId);
        // Add LooksCoin credits for recipient. Use the redeemCredits method to redeem
        uint256 _creditamt = SafeMath.sub(msg.value, averageCost) + INIT_CREDIT;
        tokenCredits[nextTokenId] = SafeMath.add(tokenCredits[nextTokenId], _creditamt);
        nextTokenId += 1;

        averageCost = _lrTree.addLeafToOpenBranch(genesisBranch, nextTokenId - 1);
    }

    /// @dev Mint leaves on open branches, starting from genesis branches
    /// Can mint up to 10 leafNFT at a time.
    /// @dev Use averageCost to estimate cost of leaf
    /// This function is going to add new leaves on the open branches on the tree
    /// @dev If the amount sent is more than the cost of leaves, the recipient will receive credits
    /// stored in NFTTokens
    /// @param recipient is the recipient wallet address for receiving ERC721 leafNFT token and
    ///  ERC20 LooksCoin tokens.
    function mintLeafNFTTokens(address recipient) external payable {
        require(recipient != address(0), "LookRevLeafNFT: recipient needs valid wallet address");
        require(msg.value > averageCost, "LookRevLeafNFT: Amount sent is too small");
        
        uint256 _creditamt = 0;
        uint256 _count = 0;
        if (msg.value > averageCost * maxBatchCount) {
            _count = maxBatchCount;
            _creditamt = SafeMath.sub(msg.value, averageCost * maxBatchCount);
        } else {
            _count = SafeMath.div(msg.value, averageCost);
            _creditamt = SafeMath.sub(msg.value, _count * averageCost);
        }

        uint256[] memory _tokens = new uint256[](_count);
        for (uint256 i = 0; i < _count; i++) {
            ownerNFTTokens[recipient].push(nextTokenId);
            tokenCredits[nextTokenId] = SafeMath.add(tokenCredits[nextTokenId], INIT_CREDIT);
            _tokens[i] = nextTokenId;
            _safeMint(recipient, nextTokenId);
            nextTokenId += 1;
        }
        
        if (_creditamt > 0) {
            tokenCredits[nextTokenId - 1] = SafeMath.add(tokenCredits[nextTokenId - 1], _creditamt);
        }
        
        /// After successfully minted token for user, add them on the tree
        averageCost = _lrTree.addLeavesToOpenBranches(_tokens);
    }

    /// Let token owner to redeem coin credits from credits stored in LeafNFT tokens
    /// @dev Requires refilling the tree escrow as needed
    /// @param recipient is the LeafNFT token owner wallet address
    /// @return uint256 amount of LooksCoin ERC20 token redeemed for token owner
    function redeemCredits(address recipient) external override returns (uint256) {
        require(msg.sender == recipient || msg.sender == admin, "LookRevLeafNFT: Not authorized");
        require(recipient != address(0), "LookRevLeafNFT: recipient needs valid wallet address");
        require(Address.isContract(recipient) == false, "LookRevLeafNFT: recipient can not be a contract");
        uint256[] memory _tokenIDs = ownerNFTTokens[recipient];
        if (_tokenIDs.length < 1) {
            return 0;
        }
        uint256 _amt = 0;
        // prevent reentry
        for (uint256 i = 0; i < _tokenIDs.length; i++) {
            uint256 _c = tokenCredits[_tokenIDs[i]];
            if (_c > 0) {
                tokenCredits[_tokenIDs[i]] = 0;
                _amt = SafeMath.add(_amt, _c);
            }
        }
        if (_amt <= 0) {
            return 0;
        }
        uint256 _coinamt = SafeMath.mul(_amt, COIN_RATE);
        if (_looksCoin.balanceOf(address(this)) >= _coinamt) {
            _looksCoin.transfer(recipient, _coinamt);
            emit CreditRedeemed("Credit Redeemed", recipient, _coinamt);
        } else {
            emit CreditRedeemed("Error redeem credit. Need to refill escrow", msg.sender, _coinamt);
            revert("LookRevLeafNFT: Need to refill tree escrow");
        }
        return _coinamt;
    }
    
    function getNFTTokenRecipient(uint256 tokenID) external override view returns (address) {
        require(tokenID > 0 && tokenID < nextTokenId, "LookRevLeafNFT: tokenID out of range");
        return ERC721.ownerOf(tokenID);
    }
    
    ///===============================
    /// Maintenance methods for admin
    ///===============================
    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override {
        ERC721._beforeTokenTransfer(from, to, tokenId);
        if (tokenId < 1 || tokenId >= nextTokenId) {
            return;
        }
        if (from != address(0)) {
            for (uint256 i = 0; i < ownerNFTTokens[from].length; i++) {
                if (ownerNFTTokens[from][i] == tokenId) {
                    delete ownerNFTTokens[from][i];
                }
            }
        }
        if (to != address(0)) {
            ownerNFTTokens[to].push(tokenId);
        }
    }
            
    // Reward contribution for team on off-chain project development
    function rewardTokenID(uint256[] calldata tokenIDs, uint256[] calldata amount) external override {
        require(msg.sender == admin, "LookRevLeafNFT: Not authorized");
        for (uint256 i = 0; i < tokenIDs.length; i++) {
            uint256 _c = tokenIDs[i];
            if(_c > 0 && _c < nextTokenId) {
                tokenCredits[_c] = SafeMath.add(tokenCredits[_c], amount[i]);
            }
        }
    }
        
    /// @dev Set the custom token uri
    function setTokenURI(uint256 tokenId, string memory tokenURI) public {
        require(msg.sender == admin, "LookRevLeafNFT: Not authorized");
        _setTokenURI(tokenId, tokenURI);
    }
    
    /// @dev Withdraw from this contract to fund projects on tree branches
    function withdraw() public {
        require(msg.sender == admin|| msg.sender == ceo || msg.sender == cfo, "LookRevLeafNFT: Not authorized");
        require(treeStorage != address(0), "LookRevLeafNFT: Set storage first");
        payable(treeStorage).transfer(address(this).balance);
    }

    function updateRole(uint256 _role, address _new) public {
        require(msg.sender == admin || msg.sender == ceo || msg.sender == cfo, "LookRevLeafNFT: Not authorized");
        if (_role == 1) {
            admin = _new;
            emit RoleUpdated("admin", _new);
        } else if (_role == 2) {
            ceo = _new;
            emit RoleUpdated("ceo", _new);
        } else if (_role == 3) {
            cfo = _new;
            emit RoleUpdated("cfo", _new);
        } else if (_role == 4) {
            treeStorage = _new;
            emit RoleUpdated("storage", _new);
        } else if (_role == 5) {
            _lrTree = ILookRevTree(_new);
            treeVersions.push(_new);
            emit RoleUpdated("tree", _new);
        }
    }

    function getRoles() public view returns (address[] memory) {
        require(msg.sender == admin || msg.sender == ceo || msg.sender == cfo, "LookRevLeafNFT: Not authorized");
        address[] memory _roles = new address[](5);
        _roles[0] = admin;
        _roles[1] = ceo;
        _roles[2] = cfo;
        _roles[3] = treeStorage;
        _roles[4] = address(_lrTree);
        return _roles;
    }
        
    function updateCoinVersion(address _token, uint256 rate) public {
        require(msg.sender == admin, "LookRevLeafNFT: Not authorized");
        _looksCoin = IERC20(_token);
        tokenVersions.push(_token);
        COIN_RATE = rate;
        emit RoleUpdated("Coin Version Updated", _token);
    }

    function getTreeVersions() public view returns (address[] memory) {
        require(msg.sender == admin, "LookRevLeafNFT: Not authorized");
        return treeVersions;
    }
        
    function getTokenVersions() public view returns (address[] memory) {
        require(msg.sender == admin, "LookRevLeafNFT: Not authorized");
        return tokenVersions;
    }
}
/// LookRevLeafNFT is ERC721
function mintLeafNFTTokens(address recipient) external payable {
    require(recipient != address(0), "LookRevLeafNFT: recipient needs valid wallet address");
    require(msg.value > averageCost, "LookRevLeafNFT: Amount sent is too small");
        
    uint256 _creditamt = 0;
    uint256 _count = 0;
    if (msg.value > averageCost * maxBatchCount) {
        _count = maxBatchCount;
        _creditamt = SafeMath.sub(msg.value, averageCost * maxBatchCount);
    } else {
        _count = SafeMath.div(msg.value, averageCost);
        _creditamt = SafeMath.sub(msg.value, _count * averageCost);
    }

    uint256[] memory _tokens = new uint256[](_count);
    for (uint256 i = 0; i < _count; i++) {
        ownerNFTTokens[recipient].push(nextTokenId);
        tokenCredits[nextTokenId] = SafeMath.add(tokenCredits[nextTokenId], INIT_CREDIT);
        _tokens[i] = nextTokenId;
        _safeMint(recipient, nextTokenId);
        nextTokenId += 1;
    }
        
    if (_creditamt > 0) {
        tokenCredits[nextTokenId - 1] = SafeMath.add(tokenCredits[nextTokenId - 1], _creditamt);
    }
        
    /// After successfully minted token for user, add them on the tree
    averageCost = _lrTree.addLeavesToOpenBranches(_tokens);
}

/// @dev Mint leaves on open branches, starting from genesis branches
/// Can mint up to 10 leafNFT at a time.
/// @dev Use averageCost to estimate cost of leaf
/// This function is going to add new leaves on the open branches on the tree
/// @dev If the amount sent is more than the cost of leaves, the recipient will receive credits
/// stored in NFTTokens
/// @param recipient is the recipient wallet address for receiving ERC721 leafNFT token and
///  ERC20 LooksCoin tokens.