contract RetroArt is ERC721Token, Ownable, Withdrawable, ERC721WithState {
    
    address public stemTokenContractAddress; 
    uint256 public currentPrice;
    uint256 constant initiailPrice = 0.03 ether;
    //new asset price increase at the rate that determined by the variable below
    //it is caculated from the current price + (current price / ( price rate * totalTokens / slowDownRate ))
    uint public priceRate = 10;
    uint public slowDownRate = 7;
    //Commission will be charged if a profit is made
    //Commission is the pure profit / profit Commission  
    // measured in basis points (1/100 of a percent) 
    // Values 0-10,000 map to 0%-100%
    uint public profitCommission = 500;

    //the referral percentage of the commission of selling of aset
    // measured in basis points (1/100 of a percent) 
    // Values 0-10,000 map to 0%-100%
    uint public referralCommission = 3000;

    //share will be given to all tokens equally if a new asset is acquired. 
    //the amount of total shared value is assetValue/sharePercentage   
    // measured in basis points (1/100 of a percent) 
    // Values 0-10,000 map to 0%-100%
    uint public sharePercentage = 3000;

    //number of shares for acquiring new asset. 
    uint public numberOfShares = 10;

    string public uriPrefix ="";


    // Mapping from owner to list of owned token IDs
    mapping (uint256 => string) internal tokenTitles;
    mapping (uint256 => RecordKeeping.priceRecord) internal initialPriceRecords;
    mapping (uint256 => RecordKeeping.priceRecord) internal lastPriceRecords;
    mapping (uint256 => uint256) internal currentTokenPrices;


    event AssetAcquired(address indexed _owner, uint256 indexed _tokenId, string  _title, uint256 _price);
    event TokenPriceSet(uint256 indexed _tokenId,  uint256 _price);
    event TokenBrought(address indexed _from, address indexed _to, uint256 indexed _tokenId, uint256 _price);
    event PriceRateChanged(uint _priceRate);
    event SlowDownRateChanged(uint _slowDownRate);
    event ProfitCommissionChanged(uint _profitCommission);
    event MintPriceChanged(uint256 _price);
    event SharePercentageChanged(uint _sharePercentage);
    event NumberOfSharesChanged(uint _numberOfShares);
    event ReferralCommissionChanged(uint _referralCommission);
    event Burn(address indexed _owner, uint256 _tokenId);

   

    bytes4 private constant InterfaceId_RetroArt = 0x94fb30be;
    /*
    bytes4(keccak256("buyTokenFrom(address,address,uint256)"))^
    bytes4(keccak256("setTokenPrice(uint256,uint256)"))^
    bytes4(keccak256("setTokenState(uint256,uint8)"))^
    bytes4(keccak256("getTokenState(uint256)"));
    */

    address[] internal auctionContractAddresses;
 
   

    function tokenTitle(uint256 _tokenId) public view returns (string memory) {
        require(exists(_tokenId));
        return tokenTitles[_tokenId];
    }
    function lastPriceOf(uint256 _tokenId) public view returns (uint256) {
        require(exists(_tokenId));
        return  lastPriceRecords[_tokenId].price;
    }   

    function lastTransactionTimeOf(uint256 _tokenId) public view returns (uint256) {
        require(exists(_tokenId));
        return  lastPriceRecords[_tokenId].timestamp;
    }

    function firstPriceOf(uint256 _tokenId) public view returns (uint256) {
        require(exists(_tokenId));
        return  initialPriceRecords[_tokenId].price;
    }   
    function creatorOf(uint256 _tokenId) public view returns (address) {
        require(exists(_tokenId));
        return  initialPriceRecords[_tokenId].owner;
    }
    function firstTransactionTimeOf(uint256 _tokenId) public view returns (uint256) {
        require(exists(_tokenId));
        return  initialPriceRecords[_tokenId].timestamp;
    }
    
  
    //problem with current web3.js that can't return an array of struct
    function lastHistoryOf(uint256 _tokenId) internal view returns (RecordKeeping.priceRecord storage) {
        require(exists(_tokenId));
        return lastPriceRecords[_tokenId];
    }

    function firstHistoryOf(uint256 _tokenId) internal view returns (RecordKeeping.priceRecord storage) {
        require(exists(_tokenId)); 
        return   initialPriceRecords[_tokenId];
    }

    function setPriceRate(uint _priceRate) public onlyOwner {
        priceRate = _priceRate;
        emit PriceRateChanged(priceRate);
    }

    function setSlowDownRate(uint _slowDownRate) public onlyOwner {
        slowDownRate = _slowDownRate;
        emit SlowDownRateChanged(slowDownRate);
    }
 
    function setprofitCommission(uint _profitCommission) public onlyOwner {
        require(_profitCommission <= 10000);
        profitCommission = _profitCommission;
        emit ProfitCommissionChanged(profitCommission);
    }

    function setSharePercentage(uint _sharePercentage) public onlyOwner  {
        require(_sharePercentage <= 10000);
        sharePercentage = _sharePercentage;
        emit SharePercentageChanged(sharePercentage);
    }

    function setNumberOfShares(uint _numberOfShares) public onlyOwner  {
        numberOfShares = _numberOfShares;
        emit NumberOfSharesChanged(numberOfShares);
    }

    function setReferralCommission(uint _referralCommission) public onlyOwner  {
        require(_referralCommission <= 10000);
        referralCommission = _referralCommission;
        emit ReferralCommissionChanged(referralCommission);
    }

    function setUriPrefix(string memory _uri) public onlyOwner  {
       uriPrefix = _uri;
    }
  
    //use the token name, symbol as usual
    //this contract create another ERC20 as stemToken,
    //the constructure takes the stemTokenName and stemTokenSymbol

    constructor(string memory _name, string memory _symbol , address _stemTokenAddress) 
        ERC721Token(_name, _symbol) Ownable() public {
       
        currentPrice = initiailPrice;
        stemTokenContractAddress = _stemTokenAddress;
        _registerInterface(InterfaceId_RetroArt);
    }

    function getAllAssets() public view returns (uint256[] memory){
        return allTokens;
    }

    function getAllAssetsForSale() public view returns  (uint256[] memory){
      
        uint arrayLength = allTokens.length;
        uint forSaleCount = 0;
        for (uint i = 0; i<arrayLength; i++) {
            if (currentTokenPrices[allTokens[i]] > 0) {
                forSaleCount++;              
            }
        }
        
        uint256[] memory tokensForSale = new uint256[](forSaleCount);

        uint j = 0;
        for (uint i = 0; i<arrayLength; i++) {
            if (currentTokenPrices[allTokens[i]] > 0) {                
                tokensForSale[j] = allTokens[i];
                j++;
            }
        }

        return tokensForSale;
    }

    function getAssetsForSale(address _owner) public view returns (uint256[] memory) {
      
        uint arrayLength = allTokens.length;
        uint forSaleCount = 0;
        for (uint i = 0; i<arrayLength; i++) {
            if (currentTokenPrices[allTokens[i]] > 0 && tokenOwner[allTokens[i]] == _owner) {
                forSaleCount++;              
            }
        }
        
        uint256[] memory tokensForSale = new uint256[](forSaleCount);

        uint j = 0;
        for (uint i = 0; i<arrayLength; i++) {
            if (currentTokenPrices[allTokens[i]] > 0 && tokenOwner[allTokens[i]] == _owner) {                
                tokensForSale[j] = allTokens[i];
                j++;
            }
        }

        return tokensForSale;
    }

    function getAssetsByState(uint8 _state) public view returns (uint256[] memory){
        
        uint arrayLength = allTokens.length;
        uint matchCount = 0;
        for (uint i = 0; i<arrayLength; i++) {
            if (tokenState[allTokens[i]] == _state) {
                matchCount++;              
            }
        }
        
        uint256[] memory matchedTokens = new uint256[](matchCount);

        uint j = 0;
        for (uint i = 0; i<arrayLength; i++) {
            if (tokenState[allTokens[i]] == _state) {                
                matchedTokens[j] = allTokens[i];
                j++;
            }
        }

        return matchedTokens;
    }
      

    function acquireAsset(uint256 _tokenId, string memory _title) public payable{
        acquireAssetWithReferral(_tokenId, _title, address(0));
    }

    function acquireAssetFromStemToken(address _tokenOwner, uint256 _tokenId, string calldata _title) external {     
         require(msg.sender == stemTokenContractAddress);
        _acquireAsset(_tokenId, _title, _tokenOwner, 0);
    }

    function acquireAssetWithReferral(uint256 _tokenId, string memory _title, address referralAddress) public payable{
        require(msg.value >= currentPrice);
        
        uint totalShares = numberOfShares;
        if (referralAddress != address(0)) totalShares++;

        uint numberOfTokens = allTokens.length;
     
        if (numberOfTokens > 0 && sharePercentage > 0) {

            uint256 perShareValue = 0;
            uint256 totalShareValue = msg.value * sharePercentage / 10000 ;

            if (totalShares > numberOfTokens) {
                               
                if (referralAddress != address(0)) 
                    perShareValue = totalShareValue / (numberOfTokens + 1);
                else
                    perShareValue = totalShareValue / numberOfTokens;
            
                for (uint i = 0; i < numberOfTokens; i++) {
                    //turn off events if there are too many tokens in the loop
                    if (numberOfTokens > 100) {
                        _depositWithoutEvent(tokenOwner[allTokens[i]], perShareValue);
                    }else{
                        _deposit(tokenOwner[allTokens[i]], perShareValue, 2);
                    }
                }
                
            }else{
               
                if (referralAddress != address(0)) 
                    perShareValue = totalShareValue / (totalShares + 1);
                else
                    perShareValue = totalShareValue / totalShares;
              
                uint[] memory randomArray = random(numberOfShares);

                for (uint i = 0; i < numberOfShares; i++) {
                    uint index = randomArray[i] % numberOfTokens;

                    if (numberOfShares > 100) {
                        _depositWithoutEvent(tokenOwner[allTokens[index]], perShareValue);
                    }else{
                        _deposit(tokenOwner[allTokens[index]], perShareValue, 2);
                    }
                }
            }
                    
            if (referralAddress != address(0) && perShareValue > 0) _deposit(referralAddress, perShareValue, 5);

        }

        _acquireAsset(_tokenId, _title, msg.sender, msg.value);
     
    }

    function _acquireAsset(uint256 _tokenId, string memory _title, address _purchaser, uint256 _value) internal {
        
        currentPrice = CalculateNextPrice();
        _mint(_purchaser, _tokenId);        
      
        tokenTitles[_tokenId] = _title;
       
        RecordKeeping.priceRecord memory pr = RecordKeeping.priceRecord(_value, _purchaser, block.timestamp);
        initialPriceRecords[_tokenId] = pr;
        lastPriceRecords[_tokenId] = pr;     

        emit AssetAcquired(_purchaser,_tokenId, _title, _value);
        emit TokenBrought(address(0), _purchaser, _tokenId, _value);
        emit MintPriceChanged(currentPrice);
    }

    function CalculateNextPrice() public view returns (uint256){      
        return currentPrice + currentPrice * slowDownRate / ( priceRate * (allTokens.length + 2));
    }

    function tokensOf(address _owner) public view returns (uint256[] memory){
        return ownedTokens[_owner];
    }

    function _buyTokenFromWithReferral(address _from, address _to, uint256 _tokenId, address referralAddress, address _depositTo) internal {
        require(currentTokenPrices[_tokenId] != 0);
        require(msg.value >= currentTokenPrices[_tokenId]);
        
        tokenApprovals[_tokenId] = _to;
        safeTransferFrom(_from,_to,_tokenId);

        uint256 valueTransferToOwner = msg.value;
        uint256 lastRecordPrice = lastPriceRecords[_tokenId].price;
        if (msg.value >  lastRecordPrice){
            uint256 profit = msg.value - lastRecordPrice;           
            uint256 commission = profit * profitCommission / 10000;
            valueTransferToOwner = msg.value - commission;
            if (referralAddress != address(0)){
                _deposit(referralAddress, commission * referralCommission / 10000, 5);
            }           
        }
        
        if (valueTransferToOwner > 0) _deposit(_depositTo, valueTransferToOwner, 1);
        writePriceRecordForAssetSold(_depositTo, msg.sender, _tokenId, msg.value);
        
    }

    function buyTokenFromWithReferral(address _from, address _to, uint256 _tokenId, address referralAddress) public payable {
        _buyTokenFromWithReferral(_from, _to, _tokenId, referralAddress, _from);        
    }

    function buyTokenFrom(address _from, address _to, uint256 _tokenId) public payable {
        buyTokenFromWithReferral(_from, _to, _tokenId, address(0));        
    }   

    function writePriceRecordForAssetSold(address _from, address _to, uint256 _tokenId, uint256 _value) internal {
       RecordKeeping.priceRecord memory pr = RecordKeeping.priceRecord(_value, _to, block.timestamp);
       lastPriceRecords[_tokenId] = pr;
       
       tokenApprovals[_tokenId] = address(0);
       currentTokenPrices[_tokenId] = 0;
       emit TokenBrought(_from, _to, _tokenId, _value);       
    }

    function recordAuctionPriceRecord(address _from, address _to, uint256 _tokenId, uint256 _value)
       external {

       require(findAuctionContractIndex(msg.sender) >= 0); //make sure the sender is from one of the auction addresses
       writePriceRecordForAssetSold(_from, _to, _tokenId, _value);

    }

    function setTokenPrice(uint256 _tokenId, uint256 _newPrice) public  {
        require(isApprovedOrOwner(msg.sender, _tokenId));
        currentTokenPrices[_tokenId] = _newPrice;
        emit TokenPriceSet(_tokenId, _newPrice);
    }

    function getTokenPrice(uint256 _tokenId)  public view returns(uint256) {
        return currentTokenPrices[_tokenId];
    }

    function random(uint num) private view returns (uint[] memory) {
        
        uint base = uint(keccak256(abi.encodePacked(block.difficulty, now, tokenOwner[allTokens[allTokens.length-1]])));
        uint[] memory randomNumbers = new uint[](num);
        
        for (uint i = 0; i<num; i++) {
            randomNumbers[i] = base;
            base = base * 2 ** 3;
        }
        return  randomNumbers;
        
    }


    function getAsset(uint256 _tokenId)  external
        view
        returns
    (
        string memory title,            
        address owner,     
        address creator,      
        uint256 currentTokenPrice,
        uint256 lastPrice,
        uint256 initialPrice,
        uint256 lastDate,
        uint256 createdDate
    ) {
        require(exists(_tokenId));
        RecordKeeping.priceRecord memory lastPriceRecord = lastPriceRecords[_tokenId];
        RecordKeeping.priceRecord memory initialPriceRecord = initialPriceRecords[_tokenId];

        return (
             
            tokenTitles[_tokenId],        
            tokenOwner[_tokenId],   
            initialPriceRecord.owner,           
            currentTokenPrices[_tokenId],      
            lastPriceRecord.price,           
            initialPriceRecord.price,
            lastPriceRecord.timestamp,
            initialPriceRecord.timestamp
        );
    }

    function getAssetUpdatedInfo(uint256 _tokenId) external
        view
        returns
    (         
        address owner, 
        address approvedAddress,
        uint256 currentTokenPrice,
        uint256 lastPrice,      
        uint256 lastDate
      
    ) {
        require(exists(_tokenId));
        RecordKeeping.priceRecord memory lastPriceRecord = lastPriceRecords[_tokenId];
     
        return (
            tokenOwner[_tokenId],   
            tokenApprovals[_tokenId],  
            currentTokenPrices[_tokenId],      
            lastPriceRecord.price,   
            lastPriceRecord.timestamp           
        );
    }

    function getAssetStaticInfo(uint256 _tokenId)  external
        view
        returns
    (
        string memory title,            
        string memory tokenURI,    
        address creator,            
        uint256 initialPrice,       
        uint256 createdDate
    ) {
        require(exists(_tokenId));      
        RecordKeeping.priceRecord memory initialPriceRecord = initialPriceRecords[_tokenId];

        return (
             
            tokenTitles[_tokenId],        
            tokenURIs[_tokenId],
            initialPriceRecord.owner,
            initialPriceRecord.price,         
            initialPriceRecord.timestamp
        );
         
    }

    function burnExchangeToken(address _tokenOwner, uint256 _tokenId) external  {
        require(msg.sender == stemTokenContractAddress);       
        _burn(_tokenOwner, _tokenId);       
        emit Burn(_tokenOwner, _tokenId);
    }

    function findAuctionContractIndex(address _addressToFind) public view returns (int)  {
        
        for (int i = 0; i < int(auctionContractAddresses.length); i++){
            if (auctionContractAddresses[uint256(i)] == _addressToFind){
                return i;
            }
        }
        return -1;
    }

    function addAuctionContractAddress(address _auctionContractAddress) public onlyOwner {
        require(findAuctionContractIndex(_auctionContractAddress) == -1);
        auctionContractAddresses.push(_auctionContractAddress);
    }

    function removeAuctionContractAddress(address _auctionContractAddress) public onlyOwner {
        int index = findAuctionContractIndex(_auctionContractAddress);
        require(index >= 0);        

        for (uint i = uint(index); i < auctionContractAddresses.length-1; i++){
            auctionContractAddresses[i] = auctionContractAddresses[i+1];         
        }
        auctionContractAddresses.length--;
    }

    function setStemTokenContractAddress(address _stemTokenContractAddress) public onlyOwner {        
        stemTokenContractAddress = _stemTokenContractAddress;
    }          
   

    function tokenURI(uint256 _tokenId) public view returns (string memory) {
        require(exists(_tokenId));   
        return string(abi.encodePacked(uriPrefix, uint256ToString(_tokenId)));

    }
    // Functions used for generating the URI
    function amountOfZeros(uint256 num, uint256 base) public pure returns(uint256){
        uint256 result = 0;
        num /= base;
        while (num > 0){
            num /= base;
            result += 1;
        }
        return result;
    }

      function uint256ToString(uint256 num) public pure returns(string memory){
        if (num == 0){
            return "0";
        }
        uint256 numLen = amountOfZeros(num, 10) + 1;
        bytes memory result = new bytes(numLen);
        while(num != 0){
            numLen -= 1;
            result[numLen] = byte(uint8((num - (num / 10 * 10)) + 48));
            num /= 10;
        }
        return string(result);
    }

}
function amountOfZeros(uint256 num, uint256 base) public pure returns(uint256){
    uint256 result = 0;
    num /= base;
    while (num > 0){
        num /= base;
        result += 1;
    }
    return result;
}

// Functions used for generating the URI