contract ChainbreakersItemsERC721 is ERC721Token("Chainbreakers Items", "CBI"), BasicAccessControl, randomRange {

    address proxyRegistryAddress;

    using SafeMath for uint256;
    using strings for *;

    uint256 public totalItems;
    uint256 public totalItemClass;
    uint256 public totalTokens;
    uint8 public currentGen;

    string _baseURI = "http://api.chainbreakers.io/api/v1/items/metadata?tokenId=";


    uint public presaleStart = 1541073600;

    // use as seed for random
    address private lastMinter;

    ItemClass[] private globalClasses;

    mapping(uint256 => ItemData) public tokenToData;
    mapping(uint256 => ItemClass) public classIdToClass;

    struct ItemClass {
        uint256 classId;
        string name;
        uint16 amount;
        string hostfile;
        uint16 minLevel;
        uint16 rarity;
        uint16 weapon;
        uint[] category;
        uint[] statsMin;
        uint[] statsMax;
        string desc;
        uint256 total;
        uint price;
        bool active;
    }

    struct ItemData {
        uint256 tokenId;
        uint256 classId;
        uint[] stats;
        uint8 gen;
    }

    event ItemMinted(uint classId, uint price, uint256 total, uint tokenId);
    event GenerationIncreased(uint8 currentGen);
    event OwnerPayed(uint amount);
    event OwnerPayedETH(uint amount);

    // declare interface for communication between smart contracts
    MANAInterface MANAContract;

    /* HELPER FUNCTIONS - START */
    /** @dev Concatenate two strings
      * @param _a The first string
      * @param _b The second string
      */
    function addToString(string _a, string _b) internal pure returns(string) {
        return _a.toSlice().concat(_b.toSlice());
    }

    /** @dev Converts an uint to a string
      * @notice used with addToString() to generate the tokenURI
      * @param i The uint you want to convert into a string
      */
    function uint2str(uint i) internal pure returns(string) {
        if (i == 0) return "0";
        uint j = i;
        uint length;
        while (j != 0){
            length++;
            j /= 10;
        }
        bytes memory bstr = new bytes(length);
        uint k = length - 1;
        while (i != 0){
            bstr[k--] = byte(48 + i % 10);
            i /= 10;
        }
        return string(bstr);
    }
    /* HELPER FUNCTIONS - END */

    constructor(address _proxyRegistryAddress) public {
        proxyRegistryAddress = _proxyRegistryAddress;
    }

    /** @dev changes the date of the start of the presale
      * @param _start Timestamp the presale starts
      */
    function changePresaleData(uint _start) public onlyModerators {
        presaleStart = _start;
    }

    /** @dev Used to init the communication between our contracts
      * @param _manaContractAddress The contract address for the currency you want to accept e.g. MANA
      */
    function setDatabase(address _manaContractAddress) public onlyModerators {
        MANAContract = MANAInterface(_manaContractAddress); // change to official MANA contract address alter (0x0f5d2fb29fb7d3cfee444a200298f468908cc942)
    }

    /** @dev changes the tokenURI of all minted items + the _baseURI value
      * @param _newBaseURI base url to the api which reads the meta data from the contract e.g. "http://api.chainbreakers.io/api/v1/items/metadata?tokenId="
      */
    function changeBaseURIAll(string _newBaseURI) public onlyModerators {
        _baseURI = _newBaseURI;

        for(uint a = 0; a < totalTokens; a++) {
            uint tokenId = tokenByIndex(a);
            _setTokenURI(tokenId, addToString(_newBaseURI, uint2str(tokenId)));
        }
    }

    /** @dev changes the _baseURI value
      * @param _newBaseURI base url to the api which reads the meta data from the contract e.g. "http://api.chainbreakers.io/api/v1/items/metadata?tokenId="
      */
    function changeBaseURI(string _newBaseURI) public onlyModerators {
        _baseURI = _newBaseURI;
    }

    /** @dev changes the active state of an item class by its class id
      * @param _classId calss id of the item class
      * @param _active active state of the item class
      */
    function editActiveFromClassId(uint256 _classId, bool _active) public onlyModerators {
        ItemClass storage _itemClass = classIdToClass[_classId];
        _itemClass.active = _active;
    }

    /** @dev Adds an item to the contract which can be minted by the user paying the selected currency (MANA)
      * @notice You will find a list of the meanings of the individual indexes on top of the document
      * @param _name The name of the item
      * @param _rarity Defines the rarity on an item
      * @param _weapon Defines which weapon this item is
      * @param _statsMin An array of integers of the lowest stats an item can have
      * @param _statsMax An array of integers of the highest stats an item can have
      * @param _amount Defines how many items can be minted in general
      * @param _hostfile A string contains links to the 3D object, the icon and the thumbnail
      * @notice All links inside the _hostfile string has to be seperated by commas. Use `.split(",")` to get an array in frontend
      * @param _minLevel The lowest level a unit has to be to equip this item
      * @param _desc An optional item description used for legendary items mostly
      * @param _price The price of the item
      */
    function addItemWithClassAndData(string _name, uint16 _rarity, uint16 _weapon, uint[] _statsMin, uint[] _statsMax, uint16 _amount, string _hostfile, uint16 _minLevel, string _desc, uint _price) public onlyModerators {
        ItemClass storage _itemClass = classIdToClass[totalItemClass];
        _itemClass.classId = totalItemClass;
        _itemClass.name = _name;
        _itemClass.amount = _amount;
        _itemClass.rarity = _rarity;
        _itemClass.weapon = _weapon;
        _itemClass.statsMin = _statsMin;
        _itemClass.statsMax = _statsMax;
        _itemClass.hostfile = _hostfile;
        _itemClass.minLevel = _minLevel;
        _itemClass.desc = _desc;
        _itemClass.total = 0;
        _itemClass.price = _price;
        _itemClass.active = true;

        totalItemClass = globalClasses.push(_itemClass);

        totalItems++;
    }

    /** @dev The function the user calls to buy the selected item for a given price
      * @notice The price of the items increases after each bought item by a given amount
      * @param _classId The class id of the item which the user wants to buy
      */
    function buyItem(uint256 _classId) public {
        require(now > presaleStart, "The presale is not started yet");

        ItemClass storage class = classIdToClass[_classId];
        require(class.active == true, "This item is not for sale");
        require(class.amount > 0);

        require(class.total < class.amount, "Sold out");
        require(class.statsMin.length == class.statsMax.length);

        if (class.price > 0) {
            require(MANAContract != address(0), "Invalid contract address for MANA. Please use the setDatabase() function first.");
            require(MANAContract.transferFrom(msg.sender, address(this), class.price) == true, "Failed transfering MANA");
        }

        _mintItem(_classId, msg.sender);
    }

    /** @dev This function mints the item on the blockchain and generates an ERC721 token
      * @notice All stats of the item are randomly generated by using the getRandom() function using min and max values
      * @param _classId The class id of the item which one will be minted
      * @param _address The address of the owner of the new item
      */
    function _mintItem(uint256 _classId, address _address) internal {
        ItemClass storage class = classIdToClass[_classId];
        uint[] memory stats = new uint[](6);
        for(uint j = 0; j < class.statsMin.length; j++) {
            if (class.statsMax[j] > 0) {
                if (stats.length == class.statsMin.length) {
                    stats[j] = getRandom(class.statsMin[j], class.statsMax[j], uint8(j + _classId + class.total), lastMinter);
                }
            } else {
                if (stats.length == class.statsMin.length) {
                    stats[j] = 0;
                }
            }
        }

        ItemData storage _itemData = tokenToData[totalTokens + 1];
        _itemData.tokenId = totalTokens + 1;
        _itemData.classId = _classId;
        _itemData.stats = stats;
        _itemData.gen = currentGen;

        class.total += 1;
        totalTokens += 1;
        _mint(_address, totalTokens);
        _setTokenURI(totalTokens, addToString(_baseURI, uint2str(totalTokens)));

        lastMinter = _address;

        emit ItemMinted(class.classId, class.price, class.total, totalTokens);
    }

    /** @dev Gets the min and the max range of stats a given class id can have
      * @param _classId The class id of the item you want to return the stats of
      * @return statsMin An array of the lowest stats the given item can have
      * @return statsMax An array of the highest stats the given item can have
      */
    function getStatsRange(uint256 _classId) public view returns(uint[] statsMin, uint[] statsMax) {
        return (classIdToClass[_classId].statsMin, classIdToClass[_classId].statsMax);
    }

    /** @dev Gets information about the item stands behind the given token
      * @param _tokenId The id of the token you want to get the item data from
      * @return tokenId The id of the token
      * @return classId The class id of the item behind the token
      * @return stats The randomly generated stats of the item behind the token
      * @return gen The generation of the item
      */
    function getItemDataByToken(uint256 _tokenId) public view returns(uint256 tokenId, uint256 classId, uint[] stats, uint8 gen) {
        return (tokenToData[_tokenId].tokenId, tokenToData[_tokenId].classId, tokenToData[_tokenId].stats, tokenToData[_tokenId].gen);
    }

    /** @dev Returns information about the item category of the given class id
      * @param _classId The class id of the item you want to return the stats of
      * @return classId The class id of the item
      * @return category An array contains information about the category of the item
      */
    function getItemCategory(uint256 _classId) public view returns(uint256 classId, uint[] category) {
        return (classIdToClass[_classId].classId, classIdToClass[_classId].category);
    }

    /** @dev Edits the item class
      * @param _classId The class id of the item you want to edit
      * @param _name The name of the item
      * @param _rarity Defines the rarity on an item
      * @param _weapon Defines which weapon this item is
      * @param _statsMin An array of integers of the lowest stats an item can have
      * @param _statsMax An array of integers of the highest stats an item can have
      * @param _amount Defines how many items can be minted in general
      * @param _hostfile A string contains links to the 3D object, the icon and the thumbnail
      * @notice All links inside the _hostfile string has to be seperated by commas. Use `.split(",")` to get an array in frontend
      * @param _minLevel The lowest level a unit has to be to equip this item
      * @param _desc An optional item description used for legendary items mostly
      * @param _price The price of the item
      */
    function editClass(uint256 _classId, string _name, uint16 _rarity, uint16 _weapon, uint[] _statsMin, uint[] _statsMax, uint16 _amount, string _hostfile, uint16 _minLevel, string _desc, uint _price) public onlyModerators {
        ItemClass storage _itemClass = classIdToClass[_classId];
        _itemClass.name = _name;
        _itemClass.rarity = _rarity;
        _itemClass.weapon = _weapon;
        _itemClass.statsMin = _statsMin;
        _itemClass.statsMax = _statsMax;
        _itemClass.amount = _amount;
        _itemClass.hostfile = _hostfile;
        _itemClass.minLevel = _minLevel;
        _itemClass.desc = _desc;
        _itemClass.price = _price;
    }

    /** @dev Returns a count of created item classes
      * @return totalClasses Integer of how many items are able to be minted
      */
    function countItemsByClass() public view returns(uint totalClasses) {
        return (globalClasses.length);
    }

    /** @dev This function mints an item as a quest reward. The quest contract needs to be added as a moderator
      * @param _classId The id of the item should be minted
      * @param _address The address of the future owner of the minted item
      */
    function mintItemFromQuest(uint256 _classId, address _address) public onlyModerators {
        _mintItem(_classId, _address);
    }

    /** @dev Changes the tokenURI from a minted item by its tokenId
      * @param _tokenId The id of the token
      * @param _uri The new URI of the token for metadata e.g. http://api.chainbreakers.io/api/v1/items/metadata?tokenId=TOKEN_ID
      */
    function changeURIFromTokenByTokenId(uint256 _tokenId, string _uri) public onlyModerators {
        _setTokenURI(_tokenId, _uri);
    }

    function increaseGen() public onlyModerators {
        currentGen += 1;

        emit GenerationIncreased(currentGen);
    }

    /** @dev Function to get a given amount of MANA from this contract.
      * @param _amount The amount of coins you want to get from this contract.
      */
    function payOwner(uint _amount) public onlyOwner {
        MANAContract.transfer(msg.sender, _amount);
        emit OwnerPayed(_amount);
    }

    /** @dev Returns all MANA from this contract to the owner of the contract. */
    function payOwnerAll() public onlyOwner {
        uint tokens = MANAContract.balanceOf(address(this));
        MANAContract.transfer(msg.sender, tokens);
        emit OwnerPayed(tokens);
    }

    /** @dev Function to get a given amount of ETH from this contract.
      * @param _amount The amount of coins you want to get from this contract.
      */
    function payOwnerETH(uint _amount) public onlyOwner {
        msg.sender.transfer(_amount);
        emit OwnerPayedETH(_amount);
    }

    /** @dev Returns all ETH from this contract to the owner of the contract. */
    function payOwnerAllETH() public onlyOwner {
        uint balance = address(this).balance;
        msg.sender.transfer(balance);
        emit OwnerPayedETH(balance);
    }

    function isApprovedForAll(address owner, address operator) public view returns (bool) {
        // Whitelist OpenSea proxy contract for easy trading.
        ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
        if (proxyRegistry.proxies(owner) == operator) {
            return true;
        }

        return super.isApprovedForAll(owner, operator);
    }
}
function getItemDataByToken(uint256 _tokenId) public view returns(uint256 tokenId, uint256 classId, uint[] stats, uint8 gen) {
    return (tokenToData[_tokenId].tokenId, tokenToData[_tokenId].classId, tokenToData[_tokenId].stats, tokenToData[_tokenId].gen);
}

/** @dev Gets information about the item stands behind the given token
 * @param _tokenId The id of the token you want to get the item data from
 * @return tokenId The id of the token
 * @return classId The class id of the item behind the token
 * @return stats The randomly generated stats of the item behind the token
 * @return gen The generation of the item
 */