contract DERPNATION is ERC721, IERC721Receiver, Ownable, ReentrancyGuard {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    event SoldToVault(uint256 indexed txId, address indexed user, uint256 tokenId, uint256 price);
    event BoughtFromVault(uint256 indexed txId, address indexed user, uint256 tokenId, uint256 price);
    event IssueLoan(uint256 indexed loanId, address indexed debtor, uint256 value, uint256 tokenId);
    event LoanPaidInFull(uint256 indexed loanId, address indexed debtor, uint256 value, uint256 tokenId);

    struct Loan {
      uint256 loanId;
      address debtor;
      uint256 total;
      uint256 startBlock;
      uint256 interest;
    }

    struct VaultStats {
      uint256 loanId;
      uint256 txId;
      uint256 totalLoanPrinciple;
      uint256 totalLoanInterest;
      uint256 paidLoanPrinciple;
      uint256 paidLoanInterest;
      uint256 totalVaultSellVolume;
      uint256 totalVaultBuyVolume;
    }

    struct TokenData{
        bytes32 dataTxHash;
        uint8[4] traits;
    }

    mapping(uint256 => Loan) public _currentLoans;
    string _currentBaseURI = "https://www.derpnation.xyz/api/";
    bytes32 _dataHash = 0;
    uint256 _dataLocked = 0;
    uint256 _maxSupply = 20000;
    uint256 public _lastEndowment = 0;
    uint256 public _loanTerm = 24; // all loans are 24 weeks
    uint256 public _mintPrice = (25* (10 ** 15)); //0.025 eth per
    uint256 public _mintRaise;
    uint256 _vaultOpen = 0;
    uint256 public _loanTermBlocks = 44500; // 1week ~= 44500 blocks *********
    uint16[15] public _traitNumbers;
    address _a1 = 0xA7058206694Ad85467aF11B8A7ADe4a1a8ebe62D;
    address _a2 = 0x81a5F4c1718BC4C177E9F4b362Ddf2CEa53D7C63;
    address _ar = 0xA23270E0fb611896e26617bdFb0cA5D52a00556c;
    address _pixelPawnAddress = 0xe17EBBD2DBF998CA4D24242F09b20db24A2fb586; // mainnet pixelpawn address

    VaultStats public vStats;

    constructor() payable ERC721("DerpNation.xyz", "DERPv3") {
      _mintRaise = _maxSupply*_mintPrice;
      vStats.loanId = 1;
      vStats.txId = 1;
      _tokenIds.increment();
      super._mint(address(this), _tokenIds.current());
      _tokenIds.increment();
      super._mint(_pixelPawnAddress, _tokenIds.current());

      uint i = 0;
      for(i=0; i< 10; i++){
        _tokenIds.increment();
        super._mint(msg.sender, _tokenIds.current());
        _tokenIds.increment();
        super._mint(_a1, _tokenIds.current());
        _tokenIds.increment();
        super._mint(_a2, _tokenIds.current());
        _tokenIds.increment();
        super._mint(_ar, _tokenIds.current());
      }
    }

    receive() external payable {}
    fallback() external payable {}

    function deposit() public payable {

    }

    function openVault(uint8 open) public onlyOwner{
      require(_vaultOpen == 0);
      if(open > 0)
        _vaultOpen = 1;
    }

    function endowment(address destination) public nonReentrant onlyOwner {
      require((address(this).balance/333) > (1* (10 ** 18)), "No"); // ensure endowment cannot rug the safety net
      require(_lastEndowment + _loanTermBlocks*8 < block.number); // ensure endowment of 1% can be pulled one time every 8 weeks
      _lastEndowment = block.number;
      (bool sent, bytes memory data) = payable(destination).call{value: (address(this).balance/100)}("");
    }

    function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) public override returns (bytes4) {
      require(msg.sender == address(this)); // ensure we can only receive our own erc721's
      return this.onERC721Received.selector;
    }


    function _baseURI() internal view virtual override returns(string memory) {
        return _currentBaseURI;
    }

    function updateDataHash(bytes32 dHash) public onlyOwner{
        require(_dataLocked == 0);
        _dataHash = dHash;
    }

    // Allows for the traits to be locked after they are all properly setup
    function lockdata(uint8 lock) public onlyOwner {
        if(lock == 1)
            _dataLocked = 1;
    }

    function getDataHash() public view returns (bytes32){
        return _dataHash;
    }

    function updateBaseURI(string memory newBaseURI) public onlyOwner{
      _currentBaseURI = newBaseURI;
    }

    function getUserSellPrice() public view returns(uint256){
      if(address(this).balance > _mintRaise/2)
        return(_mintPrice/2 + (address(this).balance - _mintRaise/2)/350);
      else
        return (_mintPrice/2);
    }

    function getUserBuyPrice() public view returns(uint256){
      if(address(this).balance > _mintRaise/2)
        return(_mintPrice + (address(this).balance - _mintRaise/2)/333);
      else
        return (_mintPrice);
    }

    function takeLoan(uint256 tokenId) public nonReentrant payable {
      if(_vaultOpen == 0)
        require(totalSupply() >= _maxSupply, "Closed");
      uint256 loanValue = getUserSellPrice()/4; // loans are 25% of token value
      uint256 interest = loanValue*3*_loanTerm/1000; //loans are 0.3% per week ~7.2% for 24 week loan
      // take the collateral
      safeTransferFrom(msg.sender ,address(this) ,tokenId);
      // create the loan
      Loan memory currentLoan;
      currentLoan.debtor = msg.sender;
      currentLoan.total = loanValue + interest;
      currentLoan.interest = interest;
      currentLoan.startBlock = block.number;
      currentLoan.loanId = vStats.loanId;
      vStats.totalLoanPrinciple = vStats.totalLoanPrinciple + loanValue;
      vStats.totalLoanInterest = vStats.totalLoanInterest + interest;
      // issue the loan
      (bool sent, bytes memory data) = payable(msg.sender).call{value: loanValue}("");
      _currentLoans[tokenId] = currentLoan;
      emit IssueLoan(vStats.loanId, msg.sender, currentLoan.total, tokenId);
      vStats.loanId = vStats.loanId + 1;
    }

    function payLoan(uint256 tokenId) public nonReentrant payable {
      // If the loan hasnt expired, ensure the debtor is trying to pay, otherwise let anyone with the cash claim it
      if(block.number < (_loanTerm*_loanTermBlocks +  _currentLoans[tokenId].startBlock))
        require(_currentLoans[tokenId].debtor == msg.sender, "Nacho loan");
      // ensure the value is greater than the loan total
      require(msg.value >= _currentLoans[tokenId].total, "Pay in full");
      // send their collateral back to them
      IERC721 derpContract = IERC721(address(this));
      derpContract.safeTransferFrom(address(this) ,msg.sender ,tokenId);
      vStats.paidLoanPrinciple = vStats.paidLoanPrinciple + _currentLoans[tokenId].total - _currentLoans[tokenId].interest;
      vStats.paidLoanInterest = vStats.paidLoanInterest + _currentLoans[tokenId].interest;
      _currentLoans[tokenId].total = 0;
      _currentLoans[tokenId].debtor = address(0);
      _currentLoans[tokenId].startBlock = 0;
      _currentLoans[tokenId].interest = 0;
      emit LoanPaidInFull(_currentLoans[tokenId].loanId, msg.sender, msg.value, tokenId);
      _currentLoans[tokenId].loanId = 0;
    }

    function sellToken(uint256 tokenId, uint256 minPrice) public nonReentrant payable {
      // The user is selling their NFT, and this vault is buying it
      if(_vaultOpen == 0)
        require(totalSupply() >= _maxSupply, "Closed");
      require(msg.sender == tx.origin, "No contract"); // ensure contracts cant cant execute multi hop mevbot magic
      uint256 price = getUserSellPrice();
      require(getUserSellPrice() >= minPrice, "Too low");
      safeTransferFrom(msg.sender ,address(this) ,tokenId);
      (bool sent, bytes memory data) = payable(msg.sender).call{value: price}("");
      vStats.totalVaultBuyVolume = vStats.totalVaultBuyVolume + price; // user selling, vault buying
      emit SoldToVault(vStats.txId, msg.sender, tokenId, price);
      vStats.txId = vStats.txId + 1;
    }

    function buyToken(uint256 tokenId) public nonReentrant payable {
      // The user is buying our NFT and this vault is selling it
      if(_vaultOpen == 0)
        require(totalSupply() >= _maxSupply, "Closed");
      require(msg.sender == tx.origin, "No contract"); // ensure contracts cant cant execute multi hop mevbot magic
      require(tokenId <= _maxSupply, "Id Err");
      require(msg.value >= getUserBuyPrice(), "Too low");
      require(_currentLoans[tokenId].loanId == 0, "Loan collateral"); // require there is no loan against the token this user is trying to buy
      vStats.totalVaultSellVolume = vStats.totalVaultSellVolume + msg.value; // user buying, vault selling
      IERC721 derpContract = IERC721(address(this));
      derpContract.safeTransferFrom(address(this) ,msg.sender ,tokenId);
      emit BoughtFromVault(vStats.txId, msg.sender, tokenId, msg.value);
      vStats.txId = vStats.txId + 1;
    }


   /**
   * @dev Mints a new NFT.
   */
    function mint(uint256 mints) public nonReentrant payable {
        require(msg.value == _mintPrice*mints, "0.025 per"); // 0.025 eth
        require(msg.sender == tx.origin, "No contracts"); // ensure no contract mints
        require(mints <= 5, "Max 5"); // max of 5 mints per transaction
        uint8 i = 0;
        for(i=0; i< mints; i++){
          _tokenIds.increment();
          require(_tokenIds.current() <= _maxSupply, "Mint Over");
          super._safeMint(msg.sender, _tokenIds.current());
          // Mint every 10th token to this contract for price discovery
          if(_tokenIds.current()%10 == 7){
            _tokenIds.increment();
            super._mint(address(this), _tokenIds.current());
          }
          // mint every 50th token to pixel pawn
          if(_tokenIds.current()%50 == 42){
            _tokenIds.increment();
            super._mint(_pixelPawnAddress, _tokenIds.current());
          }
        }

        if(_tokenIds.current() == _maxSupply){ // If the project sells out, 80% stays for automatic market maker
          (bool sent, bytes memory data) = payable(_a1).call{value: (address(this).balance/5)}("");
        }
    }


    function totalSupply() public view returns (uint256){
      return _tokenIds.current();
    }

    function uploadTraitNumbers (uint16 [15] memory newTraitNumbers) public onlyOwner{
      require(_dataLocked == 0);
      uint8 i = 0;
      for(i=0; i < 15; i++){
        _traitNumbers[i] = newTraitNumbers[i];
      }
    }

    function getNftTraits(uint256 reqTokenId) public view returns(uint8[4] memory){
      require(ownerOf(reqTokenId) != address(0));
      uint16 [4] memory tmp;
      tmp[0] = uint16(uint256(keccak256(abi.encodePacked(reqTokenId*177601))) % 32768);
      tmp[1] = uint16(uint256(keccak256(abi.encodePacked(tmp[0])))%32768);
      tmp[2] = uint16(uint256(keccak256(abi.encodePacked(tmp[1])))%32768);
      tmp[3] = uint16(uint256(keccak256(abi.encodePacked(tmp[2])))%32768);

      uint8 i = 0;
      uint8 j = 0;

      uint8 [4] memory traits = [0,0,0,0];

      for(j=0; j < 4; j++)
          for(i=0; i < 15; i++){
              if(tmp[j] < _traitNumbers[i]){
                traits[j] = i;
                break;
              }
          }
      return traits;
    }

    function tokenData(uint256 reqTokenId) public view returns (TokenData memory){
        TokenData memory currentTokenData;
        currentTokenData.traits = getNftTraits(reqTokenId);
        currentTokenData.dataTxHash = _dataHash;
        return currentTokenData;
    }

    function uploadData(string[15] calldata textColor, string[15] calldata backgroundColor, string[15] calldata font, string[15] calldata data) external {}

}
function lockdata(uint8 lock) public onlyOwner {
    if(lock == 1)
        _dataLocked = 1;
}

// Allows for the traits to be locked after they are all properly setup