// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

// Interfaces
interface IERC721 {
  function mint(
    address _to,
    bytes12 _traitCode,
    string memory _metadataHash,
    string memory _contentHash) external returns (bool);
}

// Abstract Contracts
abstract contract Ownable {
  address public owner;

  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  constructor() {
    owner = msg.sender;
  }

  modifier onlyOwner() {
    require(owner == msg.sender, "Ownable: caller is not the owner");
    _;
  }

  function transferOwnership(address _newOwner) public virtual onlyOwner {
    require(_newOwner != address(0), "Ownable: new owner is the zero address");
    owner = _newOwner;
  }
}

// Contract
contract ShizukSales is Ownable {
  IERC721 public nft;
  address payable public treasury;
  uint256 public price = 0;
  uint256 public constant LIMIT = 3;
  mapping (address => bool) public signers;
  mapping (address => uint256) private numPurchased;

  modifier underLimit() {
    require(numPurchased[msg.sender] < LIMIT, "ShizukSales: purchase limit exceeded");
    _;
  }

  constructor() {
    treasury = payable(msg.sender);
  }

  function setNft(address _nftAddress) external onlyOwner returns (bool) {
    require(address(nft) == address(0), 'ShizukSales: NFT address is already set');
    nft = IERC721(_nftAddress);
    return true;
  }

  function setTreasury(address payable _treasury) external onlyOwner returns (bool) {
    treasury = _treasury;
    return true;
  }

  function setSigner(address _signer) external onlyOwner returns (bool) {
    require(!signers[_signer], 'ShizukSales: _signer is already a signer');
    signers[_signer] = true;
    return true;
  }

  function removeSigner(address _signer) external onlyOwner returns (bool) {
    require(signers[_signer], 'ShizukSales: _signer is not a signer');
    signers[_signer] = false;
    return true;
  }

  function setPrice(uint256 _price) external onlyOwner returns (bool) {
    price = _price;
    return true;
  }

  function purchase(
    address _to,
    bytes12 _traitCode,
    string memory _metadataHash,
    string memory _contentHash,
    bytes memory _sig) external payable underLimit() returns (bool) {
    require(msg.value >= price, 'ShizukSales: insufficient msg.value');
    require(signers[getSigner(_traitCode, _metadataHash, _contentHash, _sig)], 'SizukSales: invalid signature');
    if(msg.value > 0) treasury.transfer(msg.value);
    nft.mint(_to, _traitCode, _metadataHash, _contentHash);
    numPurchased[msg.sender]++;
    return true;
  }

  function getSigner(
    bytes12 _traitCode,
    string memory _metadataHash,
    string memory _contentHash,
    bytes memory _sig) private pure returns (address) {
    bytes32 hash = keccak256(abi.encodePacked(_traitCode, _metadataHash, _contentHash));
    return recover(hash, _sig);
  }

  function getLimit(address _addr) external view returns (uint256 purchased, uint256 limit) {
    purchased = numPurchased[_addr];
    limit = LIMIT;
  }

  function recover(bytes32 _hash, bytes memory _sig) public pure returns (address) {
    bytes32 r;
    bytes32 s;
    uint8 v;

    //Check the signature length
    if (_sig.length != 65) {
      return (address(0));
    }

    // Divide the signature in r, s and v variables
    assembly {
      r := mload(add(_sig, 32))
      s := mload(add(_sig, 64))
      v := byte(0, mload(add(_sig, 96)))
    }

    // Version of signature should be 27 or 28, but 0 and 1 are also possible versions
    if (v < 27) {
      v += 27;
    }

    // If the version is correct return the signer address
    if (v != 27 && v != 28) {
      return (address(0));
    } else {
      return ecrecover(_hash, v, r, s);
    }
  }
}