contract NFTMarketplace is
  Ownable
{
    using SafeMath for uint256;    
    
     modifier onlyPriceManager() {
      require(
          msg.sender == price_manager,
          "only price manager can call this function"
          );
          _;
    }
    
    modifier onlyOwnerOrPriceManager() {
      require(
          msg.sender == price_manager || msg.sender == owner,
          "only price manager or owner can call this function"
          );
          _;
    }
 
    /**
    * @dev not bullet-proof check, but additional measure, actually we require specific (contract) address,
    * which is key (see onlyBankContract)
    */
    function isContract(address _addr) internal view returns (bool){
      uint32 size;
      assembly {
          size := extcodesize(_addr)
      }
    
      return (size > 0);
    }

    modifier notContract(){
      require(
          (!isContract(msg.sender)),
          "external contracts are not allowed"
          );
          _;
    }

 
    
  //external NFT publishing contract
  CyclopsTokens nftContract;
  ERC20Token token; //CYTR
  
  //hard code address of external contract (NFT), as it can't be redeployed in production
  //what could be redeployed - NFTBank contract -  and we can link new NFT bank 
  //with special method in CyclopsTokens
  address nftContractAddress = 0xd6d778d86Ddf225e3c02C45D6C6e8Eb3497B452A; //NFT contract (Cyclops)
  address paymentTokenAddress = 0xBD05CeE8741100010D8E93048a80Ed77645ac7bf; //payment token (ERC20, CYTR)
  
  address price_manager = 0x0000000000000000000000000000000000000000;
  
  bool internal_prices = true;
  uint256 price_curve = 5; //5%
  
  uint32 constant BAD_NFT_PROFILE_ID = 9999999;
  uint256 constant BAD_PRICE = 0;
  string constant BAD_URL = '';
  uint32 constant UNLIMITED = 9999999;
  
  /**
   * @dev 'database' to store profiles of NFTs
   */
  struct NFTProfile{
      uint32 id;
      uint256 price; //in CYTR, i.e. 1,678 CYTR last 18 digits are decimals
      uint256 sell_price; //in CYTR i.e. 1,678 CYTR last 18 digits are decimals
      string url;
      uint32 limit;
  }
  
  NFTProfile[] public nftProfiles;
  
  uint256 next_token_id = 10;

   /**
   * @dev Events
   */
    //buy from us
    event Minted(uint32 profileID, uint256 tokenId, address wallet, uint256 cytrAmount, uint256 priceAtMoment);
    
    //intermediate event for support of broken buy (CYTR transferred but NFT was not minted) 
    // - for manual resolution from admin panel
    event GotCYTRForNFT(uint32 profileID, address wallet, uint256 cytrAmount, uint256 priceAtMoment);
    
    //intermediate event for support of broken sell (CYTR transferred back NFT was not burned) 
    // - for manual resolution from admin panel
    event SendCYTRForNFT(uint32 profileID, address wallet, uint256 cytrAmount, uint256 buybackPriceAtMoment);
    
    //buy back from user
    event Burned(uint32 profileID, uint256 tokenId, address wallet, uint256 cytrAmount, uint256 buybackPriceAtMoment);
    
    //admin events - CYTR tokens/ether deposit/withdrawal
    event TokensDeposited(uint256 amount, address wallet);
    event FinneyDeposited(uint256 amount, address wallet);
    event Withdrawn(uint256 amount, address wallet);
    event TokensWithdrawn(uint256 amount, address wallet);
    event AdminMinted(uint32 profileID, uint256 tokenId, address wallet, uint256 curPrice); 
    event AdminBurned(uint256 _tokenId,uint32 tokenProfileId, uint256 curSellPrice); 

  /**
   * @dev Contract constructor.
   */
  constructor()
    public
  {
     price_manager = owner;
     nftContract = CyclopsTokens(nftContractAddress);   //NFT minting interface
     token = ERC20Token(paymentTokenAddress);           //CYTR interface
  }
    
    function setPriceManagerRight(address newPriceManager) external onlyOwner{
          price_manager = newPriceManager;
    }
      
    
    function getPriceManager() public view returns(address){
        return price_manager;
    }

    function setInternalPriceCurve() external onlyOwnerOrPriceManager{
          internal_prices = true;
    }
    
    function setExternalPriceCurve() external onlyOwnerOrPriceManager{
          internal_prices = false;
    }
      
    function isPriceCurveInternal() public view returns(bool){
        return internal_prices;
    }
      
    function setPriceCurve(uint256 new_curve) external onlyOwnerOrPriceManager{
          price_curve = new_curve;
    }
      
    
    function getPriceCurve() public view returns(uint256){
        return price_curve;
    }
    
    
    /**
    * @dev setter/getter for ERC20 linked to exchange (current) smartcontract
    */
    function setPaymentToken(address newERC20Contract) external onlyOwner returns(bool){
    
        paymentTokenAddress = newERC20Contract;
        token = ERC20Token(paymentTokenAddress);
    }
    
    
    function getPaymentToken() external view returns(address){
        return paymentTokenAddress;
    }
    
    
    
    /**
    * @dev setter/getter for NFT publisher linked to 'marketplace' smartcontract
    */
    function setNFTContract(address newNFTContract) external onlyOwner returns(bool){
    
        nftContractAddress = newNFTContract;
        nftContract = CyclopsTokens(nftContractAddress);
    }
    
    
    function getNFTContract() external view returns(address){
        return nftContractAddress;
    }



  /**
   * @dev getter for next_token_id
   */
  function getNextTokenId() external  view returns (uint256){
      return next_token_id;
  }
  
   /**
   * @dev setter for next_token_id
   */
  function setNextTokenId(uint32 setId) external onlyOwnerOrPriceManager (){
      next_token_id = setId;
  }
  
  /**
   * @dev adds 'record' to 'database'
   * @param id, unique id of profiles
   * @param price, price of NFT assets which will be generated based on profile
   * @param sell_price, when we will buy out from owner (burn)
   * @param url, url of NFT assets which will be generated based on profile
   */
  function addNFTProfile(uint32 id, uint256 price, uint256 sell_price, string calldata url, uint32 limit) external onlyOwnerOrPriceManager {
      NFTProfile memory temp = NFTProfile(id,price,sell_price,url, limit);
      nftProfiles.push(temp);
  }
  
  
  
  /**
   * @dev removes 'record' to 'database'
   * @param id (profile id)
   *
   */
  function removeNFTProfileAtId(uint32 id) external onlyOwnerOrPriceManager {
     for (uint32 i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              removeNFTProfileAtIndex(i);      
              return;
          }
     }
  }
  
  
  
  /**
   * @dev removes 'record' to 'database'
   * @param index, record number (from 0)
   *
   */
  function removeNFTProfileAtIndex(uint32 index) internal {
     if (index >= nftProfiles.length) return;
     if (index == nftProfiles.length -1){
         nftProfiles.pop();
     } else {
         for (uint i = index; i < nftProfiles.length-1; i++){
             nftProfiles[i] = nftProfiles[i+1];
         }
         nftProfiles.pop();
     }
  }
  
  
  
  /**
   * @dev replaces 'record' in the 'database'
   * @param id, unique id of profile
   * @param price, price of NFT assets which will be generated based on profile
   * @param sell_price, sell price (back to owner) of NFT assets when owner sell to us (and we burn)
   * @param url, url of NFT assets which will be generated based on profile
   */
  function replaceNFTProfileAtId(uint32 id, uint256 price, uint256 sell_price, string calldata url, uint32 limit) external onlyOwnerOrPriceManager {
     for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              nftProfiles[i].price = price;
              nftProfiles[i].sell_price = sell_price;
              nftProfiles[i].url = url;
              nftProfiles[i].limit = limit;
              return;
          }
     }
  }
  
  
  /**
   * @dev replaces 'record' in the 'database'
   * @param atIndex, at which row of array to make replacement
   * @param id, unique id of profiles
   * @param price, price of NFT assets which will be generated based on profile
   * @param sell_price, sell price (back to owner) of NFT assets when owner sell to us (and we burn)
   * @param url, url of NFT assets which will be generated based on profile
   */
  function replaceNFTProfileAtIndex(uint32 atIndex, uint32 id, uint256 price, uint256 sell_price, string calldata url, uint32 limit) external onlyOwnerOrPriceManager  {
     nftProfiles[atIndex].id = id;
     nftProfiles[atIndex].price = price;
     nftProfiles[atIndex].sell_price = sell_price;
     nftProfiles[atIndex].url = url;
     nftProfiles[atIndex].limit = limit;
  }
  
    /**
   * @dev return array of strings is not supported by solidity, we return ids & prices
   */
  function viewNFTProfilesPrices() external view returns( uint32[] memory, uint256[] memory, uint256[] memory){
      uint32[] memory ids = new uint32[](nftProfiles.length);
      uint256[] memory prices = new uint256[](nftProfiles.length);
      uint256[] memory sell_prices = new uint256[](nftProfiles.length);
      for (uint i = 0; i < nftProfiles.length; i++){
          ids[i] = nftProfiles[i].id;
          prices[i] = nftProfiles[i].price;
          sell_prices[i] = nftProfiles[i].sell_price;
      }
      return (ids, prices, sell_prices);
  }
  
  
   /**
   * @dev return price, sell_price & url for profile by id
   */
  function viewNFTProfileDetails(uint32 id) external view returns(uint256, uint256, string memory, uint32){
     for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              return (nftProfiles[i].price, nftProfiles[i].sell_price, nftProfiles[i].url, nftProfiles[i].limit);     
          }
     }
     return (BAD_PRICE, BAD_PRICE, BAD_URL, UNLIMITED);
  }
  
  /**
   * @dev get price by id from 'database'
   * @param id, unique id of profiles
   */
  function getPriceById(uint32 id) public  view returns (uint256){
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              return nftProfiles[i].price;
          }
      }
      return BAD_PRICE;
  }
  
  
 
  
  /**
   * @dev get sell price by id from 'database'
   * @param id, unique id of profiles
   */
  function getSellPriceById(uint32 id) public  view returns (uint256){
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              return nftProfiles[i].sell_price;
          }
      }
      return BAD_PRICE;
  }
  
   /**
   * @dev set new price for asset (profile of NFT), price for which customer can buy
   * @param id, unique id of profiles
   */
  function setPriceById(uint32 id, uint256 new_price) external onlyOwnerOrPriceManager{
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              nftProfiles[i].price = new_price;
              return;
          }
      }
  }
  
   /**
   * @dev set new sell (buy back) price for asset (profile of NFT), 
   * price for which customer can sell to us
   * @param id, unique id of profiles
   */
  function setSellPriceById(uint32 id, uint256 new_price) external onlyOwnerOrPriceManager{
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              nftProfiles[i].sell_price = new_price;
              return;
          }
      }
  }
  
  // for optimization, funciton to update both prices
  function updatePricesById(uint32 id, uint256 new_price, uint256 new_sell_price) external onlyOwnerOrPriceManager{
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              nftProfiles[i].price = new_price;
              nftProfiles[i].sell_price = new_sell_price;
              return;
          }
      }
  }
  
 
  
  /**
   * @dev get url by id from 'database'
   * @param id, unique id of profiles
   */ 
  function  getUrlById(uint32 id) public view returns (string memory){
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
              return nftProfiles[i].url;
          }
      }
      return BAD_URL;
  }
  
  function  getLimitById(uint32 id) public view returns (uint32){
      for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == id){
             return nftProfiles[i].limit;
          }
      }
      return UNLIMITED;
  }
  
   
  /**
   * @dev accepts payment only in CYTR(!) for mint NFT & calls external contract
   * it is public function, i.e called by buyer via dApp
   * buyer selects profile (profileID), provides own wallet address (_to)
   * and dApp provides available _tokenId (for flexibility its calculation is not automatic on 
   * smart contract level, but it is possible to implement) - > nftContract.totalSupply()+1
   * why not recommended: outsite of smart contract with multiple simultaneous customers we can 
   * instanteneusly on level of backend determinte next free id.
   * on CyclopsTokens smartcontract level it can be only calculated correctly after mint transaction is confirmed
   * here utility function is implemented which is used by backend ,getNextTokenId()
   * it is also possible to use setNextTokenId function (by owner) to reset token id if needed
   * normal use is dApp requests next token id (tid = getNextTokenId()) and after that
   * calls publicMint(profile, to, tid)
   * it allows different dApps use different token ids areas
   * like   dapp1: tid = getNextTokenId() + 10000
   *        dapp2: tid = getNextTokenId() + 20000
   */
  function buyNFT(          //'buy' == mint NFT token function, provides NFT token in exchange of CYTR    
    uint32 profileID,       //id of NFT profile
    uint256 cytrAmount,     //amount of CYTR we check it is equal to price, amount in real form i.e. 18 decimals
    address _to,            //where to deliver 
    uint256 _tokenId        //with which id NFT will be generated
  ) 
    external 
    notContract 
    returns (uint256)
  {
    require (getLimitById(profileID) > 0,"limit is over");
    
    uint256 curPrice = getPriceById(profileID);
    require(curPrice != BAD_PRICE, "price for NFT profile not found");
    require(cytrAmount > 0, "You need to provide some CYTR"); //it is already in 'real' form, i.e. with decimals
    
    require(cytrAmount == curPrice); //correct work (i.e. dApp calculated price correctly)
    
    uint256 token_bal = token.balanceOf(msg.sender); //how much CYTR buyer has
    
    require(token_bal >= cytrAmount, "Check the CYTR balance on your wallet"); //is it enough
    
    uint256 allowance = token.allowance(msg.sender, address(this));
    
    require(allowance >= cytrAmount, "Check the CYTR allowance"); //is allowance provided
    
    require(isFreeTokenId(_tokenId), "token id is is occupied"); //adjust on calling party

    //ensure we revert in case of failure
    try token.transferFrom(msg.sender, address(this), cytrAmount) { // get CYTR from buyer
        emit GotCYTRForNFT(profileID, msg.sender, cytrAmount, curPrice);
    } catch {
        require(false,"CYTR transfer failed");
        return 0; 
    }
  
   
    //external contract mint
    try nftContract.mint(_to,_tokenId, getUrlById(profileID)){
        next_token_id++;
        //we should have event pairs GotCYTRForNFT - Minted if all good
        emit Minted(profileID, _tokenId, msg.sender, cytrAmount, curPrice); 
    } catch {
        //return payment by using require..it should revert transaction 
        require(false,"mint failed");
    }
    
    for (uint i = 0; i < nftProfiles.length; i++){
      if (nftProfiles[i].id == profileID){
          if (nftProfiles[i].limit != UNLIMITED) nftProfiles[i].limit--;
      }
    }
    
    if (internal_prices){ //if we manage price curve internally
        for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == profileID){
              uint256 change = nftProfiles[i].price.div(100).mul(price_curve);
              nftProfiles[i].price = nftProfiles[i].price.add(change);
              change = nftProfiles[i].sell_price.div(100).mul(price_curve);
              nftProfiles[i].sell_price = nftProfiles[i].sell_price.add(change);
          }
      }
    }
 
    //return _tokenId; //success, return generated tokenId, works only if called by contract, i.e. not our case
  }

 /**
   * @dev method allows collectible owner to sell it back for sell price
   * collectible is burned, amount of sell price returned to owner of collectible
   * tokenId -> tokenProfileId -> sell price
   */
  function sellNFTBack(uint256 _tokenId) external notContract returns(uint256){ //'sell' == burn, burns and returns CYTR to user
        require(nftContract.ownerOf(_tokenId) == msg.sender, "it is not your NFT");
        uint32 tokenProfileId = getProfileIdByTokenId(_tokenId);
        require(tokenProfileId != BAD_NFT_PROFILE_ID, "NFT profile ID not found");
        uint256 sellPrice = getSellPriceById(tokenProfileId); 
        require(sellPrice != BAD_PRICE, "NFT price not found");
        
        require(token.balanceOf(msg.sender) > sellPrice, "unsufficient CYTR on contract");
        
        try nftContract.burn(_tokenId) {
            emit Burned(tokenProfileId, _tokenId, msg.sender, sellPrice, sellPrice); 
        } catch {
        //ensure error will be send (false, i.e. require is never fulfilled, error send)
            require (false, "NFT burn failed");
        }
      
        //ensure we revert in case of failure
        try token.transfer(msg.sender,  sellPrice) { // send CYTR to seller
            //just continue if all good..
            emit SendCYTRForNFT(tokenProfileId, msg.sender, sellPrice, sellPrice);
        } catch {
            require(false,"CYTR transfer failed");
            return 0; 
        }
        
        for (uint i = 0; i < nftProfiles.length; i++){
          if (nftProfiles[i].id == tokenProfileId){
              if (nftProfiles[i].limit != UNLIMITED) nftProfiles[i].limit++;
          }
        }
       
        if (internal_prices){ //if we manage price curve internally
            for (uint i = 0; i < nftProfiles.length; i++){
              if (nftProfiles[i].id == tokenProfileId){
                  uint256 change = nftProfiles[i].price.div(100).mul(price_curve);
                  nftProfiles[i].price = nftProfiles[i].price.sub(change);
                  change = nftProfiles[i].sell_price.div(100).mul(price_curve);
                  nftProfiles[i].sell_price = nftProfiles[i].sell_price.sub(change);
              }
            }
        }
  }
  
  
  function adminMint(       //mint for free as admin
    uint32 profileID,       //id of NFT profile
    address _to,            //where to deliver 
    uint256 _tokenId        //with which id NFT will be generated
  ) 
    external 
    onlyOwnerOrPriceManager
    returns (uint256)
  {
    uint256 curPrice = getPriceById(profileID);
    require(curPrice != BAD_PRICE, "price for NFT profile not found");
    require(isFreeTokenId(_tokenId), "token id is is occupied");
  

    
    //external contract mint
    try nftContract.mint(_to,_tokenId, getUrlById(profileID)){
        next_token_id++;
        //we should have event pairs GotCYTRForNFT - Minted if all good
        emit AdminMinted(profileID, _tokenId, _to, curPrice); 
    } catch {
        //return payment by using require..it should revert transaction 
        require(false,"mint failed");
    }
    
    return _tokenId; //success, return generated tokenId (works if called by another contract)
  }

  
  
  function adminBurn(uint256 _tokenId) external  onlyOwnerOrPriceManager returns(uint256){  //burn as admin, without CYTR move

        uint32 tokenProfileId = getProfileIdByTokenId(_tokenId);
        //require(tokenProfileId != BAD_NFT_PROFILE_ID, "NFT profile ID not found");
        //in admin mode we do not require it
        uint256 sellPrice = getSellPriceById(tokenProfileId); 
        //require(sellPrice != BAD_PRICE, "NFT price not found");
        //in admin mode we do not require it
        
        try nftContract.burn(_tokenId) {
            emit AdminBurned(_tokenId, tokenProfileId, sellPrice); 
        } catch {
        //ensure error will be send (false, i.e. require is never fulfilled, error send)
            require (false, "NFT burn failed");
        }
      
  }
  
  
  function getProfileIdByTokenId(uint256 tokenId) public view returns(uint32){
      string memory url = BAD_URL;
      try nftContract.tokenURI(tokenId) {
        url = nftContract.tokenURI(tokenId);
        return getProfileIdbyUrl(url);
      } catch {
        return BAD_NFT_PROFILE_ID;
      }
     
  }
  
  function getProfileIdbyUrl(string memory url) public  view returns (uint32){
      for (uint i = 0; i < nftProfiles.length; i++){
          if (keccak256(bytes(nftProfiles[i].url)) == keccak256(bytes(url))){
              return nftProfiles[i].id;
          }
      }
      return BAD_NFT_PROFILE_ID;
  }
  
 
  
  function isFreeTokenId(uint256 tokenId) public view returns (bool){
      try nftContract.tokenURI(tokenId) { 
          //if we can run this successfully it means token id is not free -> false
          return false;
      } catch {
          return true; //if we errored getting url by tokenId, it is free -> true
      }
  }
  
  
  function getTokenPriceByTokenId(uint256 tokenId) public view returns(uint256){
      string memory url = BAD_URL;
      try nftContract.tokenURI(tokenId) {
        url = nftContract.tokenURI(tokenId);
        uint32 profileId = getProfileIdbyUrl(url);
        if (profileId == BAD_NFT_PROFILE_ID){
            return BAD_NFT_PROFILE_ID;
        } else {
            return getSellPriceById(profileId);
        }
      } catch {
        return BAD_NFT_PROFILE_ID;
      }
     
  }
  
  
    /**
    * @dev - six functions below are for owner to check balance and
    * deposit/withdraw eth/tokens to exchange contract
    */
    /**
    * @dev returns contract balance, in wei
    */
    
    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }

    /**
    * @dev returns contract tokens balance
    */
    function getContractTokensBalance() external view returns (uint256) {
        return token.balanceOf(address(this));
    }
    
    
    function withdraw(address payable sendTo, uint256 amount) external onlyOwner {
        require(address(this).balance >= amount, "unsufficient funds");
        bool success = false;
        // ** sendTo.transfer(amount);**
        (success, ) = sendTo.call.value(amount)("");
        require(success, "Transfer failed.");
        // ** end **
        emit Withdrawn(amount, sendTo); //in wei
    }
    
    
    //deposit ether (amount in finney for control is provided as input paramenter)
    function deposit(uint256 amount) payable external onlyOwner { 
        require(amount*(1 finney) == msg.value,"please provide value in finney");
        emit FinneyDeposited(amount, owner); //in finney
    }
    
    
    // tokens with decimals, already converted on frontend
    function depositTokens(uint256 amount) external onlyOwner {
        require(amount > 0, "You need to deposit at least some tokens");
        uint256 allowance = token.allowance(msg.sender, address(this));
        require(allowance >= amount, "Check the token allowance");
        token.transferFrom(msg.sender, address(this), amount);
    
        emit TokensDeposited(amount, owner);
    }
    
    
    // tokens with decimals, already converted on frontend
    function withdrawTokens(address to_wallet, uint256 realAmountTokens) external onlyOwner {
            
        require(realAmountTokens > 0, "You need to withdraw at least some tokens");
      
        uint256 contractTokenBalance = token.balanceOf(address(this));
    
        require(contractTokenBalance > realAmountTokens, "unsufficient funds");
    
         //ensure we revert in case of failure
        try token.transfer(to_wallet, realAmountTokens) {
            emit TokensWithdrawn(realAmountTokens, to_wallet); //in real representation
        } catch {
            require(false,"tokens transfer failed");
    
        }
    
    }
        
    
    
}
function getContractBalance() external view returns (uint256) {
    return address(this).balance;
}

/**
 * @dev returns contract balance, in wei
 */