contract Molecules is ERC721, Ownable {
    using SafeMath for uint256;
    using IterableMapping for IterableMapping.Map;
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIds;

    address private _owner;
    address private _royaltiesAddr; // royality receiver
    uint256 public royaltyPercentage; // royalty based on sales price
    mapping(address => bool) public excludedList; // list of people who dont have to pay fee

    // cost to mint
    uint256 public mintFeeAmount;

    // // NFT Meta data
    string public baseURL;

    // UnbondingTime
    uint256 public unbondingTime = 604800;

    uint256 public constant maxSupply = 1000;

    // enable flag for public
    bool public openForPublic;

    // define Molecule struct
    struct Molecule {
        uint256 tokenId;
        // string tokenURI;
        address mintedBy;
        address currentOwner;
        uint256 previousPrice;
        uint256 price;
        uint256 numberOfTransfers;
        bool forSale;
        bool bonded;
        uint256 kind;
        uint256 level;
        uint256 lastUpgradeTime;
        uint256 bondedTime;
    }

    

    // map id to Molecules obj
    mapping(uint256 => Molecule) public allMolecules;

    // Mapping from owner to list of owned token IDs
    mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

    // Mapping from token ID to index of the owner tokens list
    mapping(uint256 => uint256) private _ownedTokensIndex;

    // Array with all token ids, used for enumeration
    uint256[] private _allTokens;

    // Mapping from token id to position in the allTokens array
    mapping(uint256 => uint256) private _allTokensIndex;

    //================================= Events ================================= //

    event SaleToggle(uint256 moleculeNumber, bool isForSale, uint256 price);
    event PurchaseEvent(uint256 moleculeNumber, address from, address to, uint256 price);
    event moleculeBonded(uint256 moleculeNumber, address owner, uint256 NodeCreationTime);
    event moleculeUnbonded(uint256 moleculeNumber, address owner, uint256 NodeCreationTime);   
    event moleculeGrown(uint256 moleculeNumber, uint256 newLevel); 

    constructor(
        address _contractOwner,
        address _royaltyReceiver,
        uint256 _royaltyPercentage,
        uint256 _mintFeeAmount,
        string memory _baseURL,
        bool _openForPublic
    ) ERC721("Molecules","M") Ownable() {
        royaltyPercentage = _royaltyPercentage;
        _owner = _contractOwner;
        _royaltiesAddr = _royaltyReceiver;
        mintFeeAmount = _mintFeeAmount.mul(1e18);
        excludedList[_contractOwner] = true; // add owner to exclude list
        excludedList[_royaltyReceiver] = true; // add artist to exclude list
        baseURL = _baseURL;
        openForPublic = _openForPublic;
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

    function mint(uint256 numberOfToken) public payable {
        // check if this function caller is not an zero address account
        require(openForPublic == true, "not open");
        require(msg.sender != address(0));
        require(
            _allTokens.length + numberOfToken <= maxSupply,
            "max supply"
        );
        require(numberOfToken > 0, "Min 1");
        require(numberOfToken <= 3, "Max 3");
        uint256 price = 0;
        // pay for minting cost
        if (excludedList[msg.sender] == false) {
            // send token's worth of ethers to the owner
            price = mintFeeAmount * numberOfToken;
            require(msg.value >= price, "Not enough fee");
            payable(_royaltiesAddr).transfer(msg.value);
        } else {
            // return money to sender // since its free
            payable(msg.sender).transfer(msg.value);
        }
        uint256 newPrice = mintFeeAmount;

        for (uint256 i = 1; i <= numberOfToken; i++) {
            _tokenIds.increment();
            uint256 newItemId = _tokenIds.current();
            _safeMint(msg.sender, newItemId);
            Molecule memory newMolecule = Molecule(
                newItemId,
                msg.sender,
                msg.sender,
                mintFeeAmount,
                0,
                0,
                false,
                false,
                0,
                1,
                0,
                0
            );
            // add the token id to the allMolecules
            allMolecules[newItemId] = newMolecule;
            // increase mint price if 200 (or multiple thereof) has been minted for the next person minting
            // e.g. on avax mint price goes up by 0.5 avax every 200 NFTs
            if (newItemId%200 == 0){
                uint256 addPrice = 5;
                newPrice += addPrice.mul(1e17);
            }
        }
        mintFeeAmount = newPrice;
    }

    function changeUrl(string memory url) external onlyOwner {
        baseURL = url;
    }

    function setMoleculeKind(uint256[] memory _tokens, uint256[] memory _kinds) external onlyOwner{
        require(_tokens.length > 0, "lists can't be empty");
        require(_tokens.length == _kinds.length, "both lists should have same length");
        for (uint256 i = 0; i < _tokens.length; i++) {
            require(_exists(_tokens[i]), "token not found");
            Molecule memory mol = allMolecules[_tokens[i]];
            mol.kind = _kinds[i];
            allMolecules[_tokens[i]] = mol;
        }
    }

    function totalSupply() public view returns (uint256) {
        return _allTokens.length;
    }


    function setPriceForSale(
        uint256 _tokenId,
        uint256 _newPrice,
        bool isForSale
    ) external {
        require(_exists(_tokenId), "token not found");
        address tokenOwner = ownerOf(_tokenId);
        require(tokenOwner == msg.sender, "not owner");
        Molecule memory mol = allMolecules[_tokenId];
        require(mol.bonded == false);
        mol.price = _newPrice;
        mol.forSale = isForSale;
        allMolecules[_tokenId] = mol;
        emit SaleToggle(_tokenId, isForSale, _newPrice);
    }

    function getAllSaleTokens() public view returns (uint256[] memory) {
        uint256 _totalSupply = totalSupply();
        uint256[] memory _tokenForSales = new uint256[](_totalSupply);
        uint256 counter = 0;
        for (uint256 i = 1; i <= _totalSupply; i++) {
            if (allMolecules[i].forSale == true) {
                _tokenForSales[counter] = allMolecules[i].tokenId;
                counter++;
            }
        }
        return _tokenForSales;
    }

    // by a token by passing in the token's id
    function buyToken(uint256 _tokenId) public payable {
        // check if the token id of the token being bought exists or not
        require(_exists(_tokenId));
        // get the token's owner
        address tokenOwner = ownerOf(_tokenId);
        // token's owner should not be an zero address account
        require(tokenOwner != address(0));
        // the one who wants to buy the token should not be the token's owner
        require(tokenOwner != msg.sender);
        // get that token from all Molecules mapping and create a memory of it defined as (struct => Molecules)
        Molecule memory mol = allMolecules[_tokenId];
        // price sent in to buy should be equal to or more than the token's price
        require(msg.value >= mol.price);
        // token should be for sale
        require(mol.forSale);
        uint256 amount = msg.value;
        uint256 _royaltiesAmount = amount.mul(royaltyPercentage).div(100);
        uint256 payOwnerAmount = amount.sub(_royaltiesAmount);
        payable(_royaltiesAddr).transfer(_royaltiesAmount);
        payable(mol.currentOwner).transfer(payOwnerAmount);
        require(mol.bonded == false, "Molecule is Bonded");
        mol.previousPrice = mol.price;
        mol.bonded = false;
        mol.numberOfTransfers += 1;
        mol.price = 0;
        mol.forSale = false;
        allMolecules[_tokenId] = mol;
        _transfer(tokenOwner, msg.sender, _tokenId);
        emit PurchaseEvent(_tokenId, mol.currentOwner, msg.sender, mol.price);
    }

    function tokenOfOwnerByIndex(address owner, uint256 index)
        public
        view
        returns (uint256)
    {
        require(index < balanceOf(owner), "out of bounds");
        return _ownedTokens[owner][index];
    }

    //  URI Storage override functions
    /** Overrides ERC-721's _baseURI function */
    function _baseURI()
        internal
        view
        virtual
        override(ERC721)
        returns (string memory)
    {
        return baseURL;
    }

    function _burn(uint256 tokenId) internal override(ERC721) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning.
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721) {
        super._beforeTokenTransfer(from, to, tokenId);
        Molecule memory mol = allMolecules[tokenId];
        require(mol.bonded == false,"Molecule is bonded!");
        mol.currentOwner = to;
        mol.numberOfTransfers += 1;
        mol.forSale = false;
        allMolecules[tokenId] = mol;
        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId)
        private
    {
        uint256 lastTokenIndex = balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }

    // upgrade contract to support authorized

    mapping(address => bool) public authorized;

    modifier onlyAuthorized() {
        require(authorized[msg.sender] ||  msg.sender == _owner , "Not authorized");
        _;
    }

    function addAuthorized(address _toAdd) public {
        require(msg.sender == _owner, 'Not owner');
        require(_toAdd != address(0));
        authorized[_toAdd] = true;
    }

    function removeAuthorized(address _toRemove) public {
        require(msg.sender == _owner, 'Not owner');
        require(_toRemove != address(0));
        require(_toRemove != msg.sender);
        authorized[_toRemove] = false;
    }

    // upgrade contract to allow OXG Nodes to 

    function bondMolecule(address account,uint256 _tokenId, uint256 nodeCreationTime) external onlyAuthorized {
        require(_exists(_tokenId), "token not found");
        address tokenOwner = ownerOf(_tokenId);
        require(tokenOwner == account, "not owner");
        Molecule memory mol = allMolecules[_tokenId];
        require(mol.bonded == false, "Molecule already bonded");
        mol.bonded = true;
        allMolecules[_tokenId] = mol;
        emit moleculeBonded(_tokenId, account, nodeCreationTime);
    }

    function unbondMolecule(address account,uint256 _tokenId, uint256 nodeCreationTime) external onlyAuthorized {
        require(_exists(_tokenId), "token not found");
        address tokenOwner = ownerOf(_tokenId);
        require(tokenOwner == account, "not owner");
        Molecule memory mol = allMolecules[_tokenId];
        require(mol.bonded == true, "Molecule not bonded");
        require(mol.bondedTime + unbondingTime > block.timestamp, "You have to wait 7 days from bonding to unbond");
        mol.bonded = false;
        allMolecules[_tokenId] = mol;
        emit moleculeUnbonded(_tokenId, account, nodeCreationTime);
    }

    function growMolecule(uint256 _tokenId) external onlyAuthorized {
        require(_exists(_tokenId), "token not found");
        Molecule memory mol = allMolecules[_tokenId];
        mol.level += 1;
        allMolecules[_tokenId] = mol;
        emit moleculeGrown(_tokenId, mol.level);
    }

    function getMoleculeLevel(uint256 _tokenId) public view returns(uint256){
        Molecule memory mol = allMolecules[_tokenId];
        return mol.level;
    }

    function getMoleculeKind(uint256 _tokenId) public view returns(uint256) {
        Molecule memory mol = allMolecules[_tokenId];
        return mol.kind;
    }

    //function to return all the structure data.




}
function _baseURI()
        internal
        view
        virtual
        override(ERC721)
        returns (string memory)
    {
        return baseURL;
    }
/** Overrides ERC-721's _baseURI function */