contract LittlePigs is ERC721, ReentrancyGuard {
  // OpenSea proxy registry
  address private immutable _osProxyRegistryAddress;

  // Whitelist signer
  address private immutable _whitelistSigner;

  // Max mints per transaction
  uint256 private constant _maxTxMint = 10;

  // The CAP of whitelist claimable tokenIds
  uint256 private constant _whitelistCap = 888;

  // The CAP of mintable tokenIds
  uint256 private constant _cap = 9800;

  // The CAP of shill tokenIds
  uint256 private constant _shillCap = 9850;

  // The CAP of team tokenIds
  uint256 private constant _teamCap = 10000;

  // ETH price of one tokenIds
  uint256 private constant _tokenPrice = 50000000000000000;

  // TokenId counter, 1 minted in ctor
  uint256 private _currentTokenId = 1;
  // ShillTokenId counter
  uint256 private _currentShillTokenId = _cap;
  // TeamTokenId counter
  uint256 private _currentTeamTokenId = _shillCap;

  // Whitelist address check
  modifier onlyValidAddress(
    uint256 _salt,
    bytes32 _r,
    bytes32 _s,
    uint8 _v
  ) {
    bytes32 hash = keccak256(abi.encode(address(this), _salt, _msgSender()));
    bytes32 message = keccak256(
      abi.encodePacked('\x19Ethereum Signed Message:\n32', hash)
    );
    address sig = ecrecover(message, _v, _r, _s);

    require(_whitelistSigner == sig, 'Signature does not match');
    _;
  }

  // Fired when funds are distributed
  event Distributed(address indexed receiver, uint256 amount);

  /**
   * @dev Initialization.
   */
  constructor(address osProxyRegistry_) ERC721('LittlePigs', 'PIGS') {
    _osProxyRegistryAddress = osProxyRegistry_;
    _whitelistSigner = _msgSender();
    _mint(_msgSender(), 0);
  }

  /**
   * @dev See {IERC721-isApprovedForAll}.
   */
  function isApprovedForAll(address owner, address operator)
    public
    view
    virtual
    override
    returns (bool)
  {
    // Whitelist OpenSea proxy contract for easy trading.
    ProxyRegistry proxyRegistry = ProxyRegistry(_osProxyRegistryAddress);
    if (address(proxyRegistry.proxies(owner)) == operator) {
      return true;
    }
    return super.isApprovedForAll(owner, operator);
  }

  /**
   * @dev Whitelist mint a set of tokenIds for caller
   * @notice only for signed whitelisters
   */
  function whitelistMint(
    uint256 numMint,
    bytes32 r,
    bytes32 s,
    uint8 v
  ) external payable nonReentrant onlyValidAddress(0, r, s, v) {
    require(msg.value >= numMint * _tokenPrice, 'Insufficient value');

    _mintPayable(numMint);
  }

  /**
   * @dev Shill mint one tokenId for caller
   * @notice only for signed shillers
   */
  function shillMint(
    uint256 tokenId,
    bytes32 r,
    bytes32 s,
    uint8 v
  ) external onlyValidAddress(tokenId, r, s, v) {
    require(_currentShillTokenId < _shillCap, 'ShillCap reached');
    require(tokenId >= _cap && tokenId < _shillCap, 'Invalid tokenId');
    _mint(_msgSender(), tokenId);
    _currentShillTokenId += 1;
  }

  /**
   * @dev Team mint a set of tokenIds for given accounts
   */
  function teamMint(address[] memory to) external onlyOwner {
    require(_currentTeamTokenId + to.length <= _teamCap, 'TeamCap reached');
    for (uint256 i = 0; i < to.length; ++i)
      _mint(to[i], _currentTeamTokenId + i);
    _currentTeamTokenId += to.length;
  }

  /**
   * @dev Distribute rewards
   */
  function distribute(
    address[] calldata accounts,
    uint256[] calldata refunds,
    uint256[] calldata percents
  ) external onlyOwner {
    require(
      (refunds.length == 0 || refunds.length == accounts.length) &&
        (percents.length == 0 || percents.length == accounts.length),
      'Length mismatch'
    );

    uint256 availableAmount = address(this).balance;
    uint256[] memory amounts = new uint256[](accounts.length);

    for (uint256 i = 0; i < refunds.length; ++i) {
      require(refunds[i] <= availableAmount, 'Insufficent balance');
      amounts[i] = refunds[i];
      availableAmount -= refunds[i];
    }

    uint256 amountToShare = availableAmount;
    for (uint256 i = 0; i < percents.length; ++i) {
      uint256 amount = (amountToShare * percents[i]) / 100;
      amounts[i] += (amount <= availableAmount) ? amount : availableAmount;
      availableAmount -= amount;
    }

    for (uint256 i = 0; i < accounts.length; ++i) {
      if (amounts[i] > 0) {
        payable(accounts[i]).transfer(amounts[i]);
        emit Distributed(accounts[i], amounts[i]);
      }
    }
  }

  /**
   * @dev return number of minted token
   */
  function totalSupply() external view returns (uint256) {
    return
      _currentTokenId < 2500
        ? _currentTokenId
        : _currentTokenId + _teamCap - _cap;
  }

  /**
   * @dev mint as much tokens as possible, return dust
   * and burning.
   */
  receive() external payable nonReentrant {
    require(mintPaused == 0, 'Paused');
    uint256 numMint = msg.value / _tokenPrice;

    _mintPayable(numMint);
  }

  /**
   * @dev internal mint multiple which has to be payed
   */
  function _mintPayable(uint256 numMint) internal {
    require(numMint > 0 && numMint <= _maxTxMint, 'Invalid numMint');
    require(_currentTokenId + numMint <= _cap, 'Cap reached');

    for (uint256 i = 0; i < numMint; ++i)
      _mint(msg.sender, _currentTokenId + i);
    _currentTokenId += numMint;

    uint256 dust = msg.value - (numMint * _tokenPrice);
    if (dust > 0) payable(msg.sender).transfer(dust);
  }
}
/**
 * @dev Implementation of LittlePigs ERC721 contract
 */
function distribute(
  address[] calldata accounts,
  uint256[] calldata refunds,
  uint256[] calldata percents
) external onlyOwner {
  require(
    (refunds.length == 0 || refunds.length == accounts.length) &&
      (percents.length == 0 || percents.length == accounts.length),
    'Length mismatch'
  );

  uint256 availableAmount = address(this).balance;
  uint256[] memory amounts = new uint256[](accounts.length);

  for (uint256 i = 0; i < refunds.length; ++i) {
    require(refunds[i] <= availableAmount, 'Insufficent balance');
    amounts[i] = refunds[i];
    availableAmount -= refunds[i];
  }

  uint256 amountToShare = availableAmount;
  for (uint256 i = 0; i < percents.length; ++i) {
    uint256 amount = (amountToShare * percents[i]) / 100;
    amounts[i] += (amount <= availableAmount) ? amount : availableAmount;
    availableAmount -= amount;
  }

  for (uint256 i = 0; i < accounts.length; ++i) {
    if (amounts[i] > 0) {
      payable(accounts[i]).transfer(amounts[i]);
      emit Distributed(accounts[i], amounts[i]);
    }
  }
}

/**
 * @dev Distribute rewards
 */