pragma solidity ^0.8.10;

// SPDX-License-Identifier: MIT


// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)


// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}



interface IWhitelist {
  function useDiscount(address account) external;

  function getDiscount(address account) external view returns (uint256);
  function deadline() external view returns (uint256);
  function isExpired() external view returns (bool);
  function exists(address account) external view returns (bool);
}

contract Whitelist is Ownable, IWhitelist {

  struct Coupon {
    address account;
    uint256[] items;
    uint256 count;
  }

  uint256 private constant MAX_COUNT = 172;

  uint256 private _deadline;
  address private _consumer;
  uint256 private _count;

  mapping(address => Coupon) _coupons;
  mapping(address => bool) _existed;

  event UseDiscount(address account, uint256 discount);

  constructor(uint256 deadline_) {
    _deadline = deadline_;
  }

  function add(address[] calldata accounts, uint256[] calldata discounts) external onlyOwner {
    require(!_isExpired(), "Whitelist: expired");
    require(accounts.length == discounts.length, "Whitelist: length mismatch");
    require(_count + accounts.length <= MAX_COUNT, "Whitelist: cap exceeded");

    uint256 length = accounts.length;
    address account;
    for(uint256 i = 0; i < length; i++) {
      account = accounts[i];
      if(_exists(account)) {
        _coupons[account].items.push(discounts[i]);
      } else {
        Coupon storage coupon = _coupons[account];
        coupon.account = account;
        coupon.items.push(discounts[i]);

        _existed[account] = true;
      }
    }

    _count += length;
  }

  function setConsumer(address consumer_) external onlyOwner {
    require(!_isExpired(), "Whitelist: expired");
    require(_consumer == address(0), "Whitelist: consumer already set");

    _consumer = consumer_;
  }

  function useDiscount(address account) external override {
    require(_consumer == _msgSender(), "Whitelist: not the consumer");

    require(!_isExpired(), "Whitelist: expired");
    require(_exists(account), "Whitelist: not on the whitelist");
    require(_hasDiscount(account), "Whitelist: discount used");

    uint256 discount = _getDiscount(account);
    _coupons[account].count += 1;

    emit UseDiscount(account, discount);
  }

  function getDiscount(address account) external view override returns (uint256) {
    return !_isExpired() && _exists(account) && _hasDiscount(account) ? _getDiscount(account) : 0;
  }

  function deadline() external view override returns (uint256) {
    return _deadline;
  }

  function consumer() external view returns (address) {
    return _consumer;
  }

  function count() external view returns (uint256) {
    return _count;
  }

  function isExpired() external view override returns (bool) {
    return _isExpired();
  }

  function exists(address account) external view override returns (bool) {
    return _exists(account);
  }

  function _isExpired() private view returns (bool) {
    return block.timestamp > _deadline;
  }

  function _exists(address account) private view returns (bool) {
    return _existed[account];
  }

  function _hasDiscount(address account) private view returns (bool) {
    return _coupons[account].count < _coupons[account].items.length;
  }

  function _getDiscount(address account) private view returns (uint256) {
    return _coupons[account].items[_coupons[account].count];
  }
}