contract MetaJacket is ERC721, Ownable {
  using Strings for uint256;
  using ECDSA for bytes32;

  //NFT params
  string public baseURI;
  string public defaultURI;
  string public mycontractURI;
  bool public finalizeBaseUri = false;

  //sale stages:
  //stage 0: init(no minting)
  //stage 1: pre-sale
  //stage 2: public sale
  uint8 public stage = 0;

  event stageChanged(uint8 stage);

  //pre-sale (stage=1)
  address private _signer;

  uint256 public presalePrice = 0.08 ether;
  uint256 public presaleMintMax = 2;
  uint256 public presaleSupply = 8000;

  mapping(address => uint8) public presaleMintCount;

  //public sale (stage=2)
  uint256 public salePrice = 0.08 ether;
  uint256 public saleMintMax = 2;
  uint256 public totalSaleSupply = 8888;
  mapping(address => uint8) public saleMintCount;

  //others
  bool public paused = false;
  uint256 public currentSupply;

  //sale holders
  address[2] public fundRecipients = [
    0x4f55aE5521c82f09e98657EEB24450a24eA6E513,
    0xBA9A1765a92ea3535DbC66E601c58470205168e7
  ];
  uint256[] public receivePercentagePt = [1000, 9000];   //distribution in basis points

  //royalty
  address public royaltyAddr = 0xBA9A1765a92ea3535DbC66E601c58470205168e7;
  uint256 public royaltyBasis = 500;

  constructor(
    string memory _name,
    string memory _symbol,
    string memory _initBaseURI,
    string memory _defaultURI,
    address _whitelistSigner
  ) ERC721(_name, _symbol) {
    setBaseURI(_initBaseURI);
    defaultURI = _defaultURI;
    _signer = _whitelistSigner;
  }

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

  function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool) {
      return interfaceId == type(IERC721).interfaceId || 
      interfaceId == 0xe8a3d485 /* contractURI() */ ||
      interfaceId == 0x2a55205a /* ERC-2981 royaltyInfo() */ ||
      super.supportsInterface(interfaceId);
  }

  // public
  function mintWhitelist(uint8 _mintAmount, bytes memory signature) public payable {
    uint256 supply = totalSupply();
    require(!paused);
    require(stage > 0, "Sale not started");
    require(isWhitelisted(msg.sender, signature), "Must be whitelisted");
    require(stage == 1, "invalid stage");
    require(_mintAmount > 0, "Must mint at least 1");
    require(supply + _mintAmount <= presaleSupply, "Mint exceed presale supply");
    require(msg.value >= presalePrice * _mintAmount, "Insufficient amount sent");
    require(_mintAmount + presaleMintCount[msg.sender] <= presaleMintMax, "Cannot mint more than presaleMintMax");
    

    presaleMintCount[msg.sender] += _mintAmount;
    currentSupply += _mintAmount;
    
    for (uint256 i = 1; i <= _mintAmount; i++) {
      _safeMint(msg.sender, supply + i);
    }
  }

  function mintPublic(uint8 _mintAmount) public payable {
    uint256 supply = totalSupply();
    require(!paused);
    require(stage == 2, "Not public sale");
    require(_mintAmount > 0, "Must mint at least 1");
    require(supply + _mintAmount <= totalSaleSupply, "Mint exceed total supply");
    require(_mintAmount + saleMintCount[tx.origin] <= saleMintMax, "Cannot mint more than saleMintMax");
    require(msg.value >= salePrice * _mintAmount, "Insufficient amount sent");
    saleMintCount[tx.origin] += _mintAmount;
    currentSupply += _mintAmount;
    for (uint256 i = 1; i <= _mintAmount; i++) {
      _safeMint(msg.sender, supply + i);
    }
  }

  function isWhitelisted(address _addr, bytes memory signature) public view returns(bool){
      return _signer == ECDSA.recover(keccak256(abi.encodePacked(address(this), _addr)), signature);
  }

  function totalSupply() public view returns (uint) {
        return currentSupply;
    }

  function tokensOfOwner(address _owner, uint startId, uint endId) external view returns(uint256[] memory ) {
      uint256 tokenCount = balanceOf(_owner);
      if (tokenCount == 0) {
          return new uint256[](0);
      } else {
          uint256[] memory result = new uint256[](tokenCount);
          uint256 index = 0;

          for (uint256 tokenId = startId; tokenId < endId; tokenId++) {
              if (index == tokenCount) break;

              if (ownerOf(tokenId) == _owner) {
                  result[index] = tokenId;
                  index++;
              }
          }

          return result;
      }
  }

  function tokenURI(uint256 tokenId) public view virtual override returns (string memory)
  {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

    string memory currentBaseURI = _baseURI();
    return bytes(currentBaseURI).length > 0
        ? string(abi.encodePacked(currentBaseURI, tokenId.toString()))
        : defaultURI;
  }

  function contractURI() public view returns (string memory) {
      return string(abi.encodePacked(mycontractURI));
  }

  //ERC-2981
  function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view returns (address receiver, uint256 royaltyAmount){
    return (royaltyAddr, _salePrice * royaltyBasis / 10000);
  }

  //only owner functions ---

  function nextStage() public onlyOwner() {
    require(stage < 2, "Stage cannot be more than 2");
    stage++;
    emit stageChanged(stage);
  }

  function setwhitelistSigner(address _whitelistSigner) public onlyOwner {
    _signer = _whitelistSigner;
  }

  function setSaleMintMax(uint256 _saleMintMax) public onlyOwner() {
    saleMintMax = _saleMintMax;
  }

  function setBaseURI(string memory _newBaseURI) public onlyOwner {
    require(!finalizeBaseUri);
    baseURI = _newBaseURI;
  }

  function finalizeBaseURI() public onlyOwner {
    finalizeBaseUri = true;
  }

  function setContractURI(string memory _contractURI) public onlyOwner {
    mycontractURI = _contractURI;
  }

  function setRoyalty(address _royaltyAddr, uint256 _royaltyBasis) public onlyOwner {
    royaltyAddr = _royaltyAddr;
    royaltyBasis = _royaltyBasis;
  }

  function pause(bool _state) public onlyOwner {
    paused = _state;
  }

  function reserveMint(uint256 _mintAmount, address _to) public onlyOwner {
    uint256 supply = totalSupply();
    require(supply + _mintAmount <= totalSaleSupply, "Mint exceed total supply");

    currentSupply += _mintAmount;
    for (uint256 i = 1; i <= _mintAmount; i++) {
      _safeMint(_to, supply + i);
    }
  }
 
  //fund withdraw functions ---
  function withdrawFund() public onlyOwner {
    uint256 currentBal = address(this).balance;
    require(currentBal > 0);

    for (uint256 i = 0; i < fundRecipients.length-1; i++) {
      _withdraw(fundRecipients[i], currentBal * receivePercentagePt[i] / 10000);
    }
    //final address receives remainder to prevent ether dust
    _withdraw(fundRecipients[fundRecipients.length-1], address(this).balance);
  }

  function _withdraw(address _addr, uint256 _amt) private {
    (bool success,) = _addr.call{value: _amt}("");
    require(success, "Transfer failed");
  }
}
function _baseURI() internal view virtual override returns (string memory) {
  return baseURI;
}

// internal