contract HodlFactory is HodlSpawner, Ownable {
  /// @dev mapping from parameters hash to its deployed address
  mapping(bytes32 => address) private _idToAddress;

  /// @dev if the address is a valid hToken deployed by this factory
  mapping(address => bool) private _isValidHToken;

  /// @dev mapping of whitelisted asset to prevent people from creating hodling competition with malicious ERC20.
  mapping(address => bool) public isWhitelistAsset;

  address implementation;

  constructor(address _implementation) {
    require(_implementation != address(0), "INVALID_IMPL");
    implementation = _implementation;
  }

  event AssetWhitelisted(address asset, bool whitelisted);

  /// @notice emitted when the factory creates a new hToken "barrel"
  event HodlCreated(
    address contractAddress,
    address indexed token,
    uint256 indexed penalty,
    uint256 lockWindow,
    uint256 indexed expiry,
    uint256 feePortion,
    uint256 n,
    address feeRecipient,
    address creator,
    address bonusToken
  );

  /**
   * @dev owner only function to whitelist or un-whitelist an asset.
   * the owner should be a governance module
   */
  function whitelistAsset(address _asset, bool _isWhitelist) external onlyOwner {
    isWhitelistAsset[_asset] = _isWhitelist;
    emit AssetWhitelisted(_asset, _isWhitelist);
  }

  /**
   * @notice create new HodlERC20 proxy
   * @dev deploy an eip-1167 minimal proxy with CREATE2 and register it to the whitelist module
   * @param _token token to hold
   * @param _penalty penalty 1 out of 1000
   * @param _lockWindow duration locked before expiry
   * @param _expiry expiry timestamp
   * @param _fee fee out of every 1000 penalty
   * @param _feeRecipient address that collect fees
   * @param _bonusToken extra reward token (optional, set to zero address if using just the base token)
   * @return newHodl newly deployed contract address
   */
  function createHodlERC20(
    address _token,
    uint256 _penalty,
    uint256 _lockWindow,
    uint256 _expiry,
    uint256 _fee,
    uint256 _n,
    address _feeRecipient,
    address _bonusToken
  ) external returns (address newHodl) {
    bytes32 id = _getHodlId(_token, _penalty, _lockWindow, _expiry, _fee, _n, _feeRecipient, _bonusToken);
    require(_idToAddress[id] == address(0), "CREATED");
    require(isWhitelistAsset[_token], "NOT_WHITELISTED");
    require(_bonusToken == address(0) || isWhitelistAsset[_bonusToken], "NOT_WHITELISTED");
    string memory name;
    string memory symbol;

    {
      // create another scope to avoid stack-too-deep error
      IERC20WithDetail token = IERC20WithDetail(_token);
      string memory tokenName = token.name();
      name = _concat("Hodl", tokenName);

      string memory tokenSymbol = token.symbol();
      symbol = _concat("h", tokenSymbol);
    }

    bytes memory initializationCalldata = abi.encodeWithSelector(
      IHodlERC20(implementation).init.selector,
      _token,
      _penalty,
      _lockWindow,
      _expiry,
      _fee,
      _n,
      _feeRecipient,
      name,
      symbol,
      _bonusToken
    );

    newHodl = _spawn(implementation, initializationCalldata);

    _isValidHToken[newHodl] = true;
    _idToAddress[id] = newHodl;

    emit HodlCreated(newHodl, _token, _penalty, _lockWindow, _expiry, _fee, _n, _feeRecipient, msg.sender, _bonusToken);

    return newHodl;
  }

  /**
   * @notice if no hToken has been created with these parameters, it will return address(0)
   * @param _token token to hold
   * @param _penalty penalty 1 out of 1000
   * @param _lockWindow duration locked before expiry
   * @param _expiry expiry timestamp
   * @param _fee fee out of every 1000 penalty
   * @param _feeRecipient address that collects fees
   * @param _bonusToken extra token for donations
   * @return
   */
  function getCreatedHToken(
    address _token,
    uint256 _penalty,
    uint256 _lockWindow,
    uint256 _expiry,
    uint256 _fee,
    uint256 _n,
    address _feeRecipient,
    address _bonusToken
  ) external view returns (address) {
    bytes32 id = _getHodlId(_token, _penalty, _lockWindow, _expiry, _fee, _n, _feeRecipient, _bonusToken);
    return _idToAddress[id];
  }

  /**
   * @notice get the address at which a new hToken with these parameters would be deployed
   * @dev return the exact address that will be deployed at with _computeAddress
   * @param _token token to hold
   * @param _penalty penalty 1 out of 1000
   * @param _lockWindow duration locked before expiry
   * @param _expiry expiry timestamp
   * @param _fee fee out of every 1000 penalty
   * @param _feeRecipient address that collect fees
   * @param _bonusToken extra token for donations
   * @return
   */
  function getTargetHTokenAddress(
    address _token,
    uint256 _penalty,
    uint256 _lockWindow,
    uint256 _expiry,
    uint256 _fee,
    uint256 _n,
    address _feeRecipient,
    address _bonusToken
  ) external view returns (address) {
    address _implementation = implementation;
    string memory name;
    string memory symbol;

    // create another scope to avoid stack-too-deep error
    {
      IERC20WithDetail token = IERC20WithDetail(_token);
      string memory tokenName = token.name();
      name = _concat("Hodl", tokenName);

      string memory tokenSymbol = token.symbol();
      symbol = _concat("h", tokenSymbol);
    }

    bytes memory initializationCalldata = abi.encodeWithSelector(
      IHodlERC20(_implementation).init.selector,
      _token,
      _penalty,
      _lockWindow,
      _expiry,
      _fee,
      _n,
      _feeRecipient,
      name,
      symbol,
      _bonusToken
    );
    return _computeAddress(_implementation, initializationCalldata);
  }

  /**
   * @dev hash parameters and get a unique hToken id
   * @param _token token to hold
   * @param _penalty penalty 1 out of 1000
   * @param _lockWindow duration locked before expiry
   * @param _expiry expiry timestamp
   * @param _fee fee out of every 1000 penalty
   * @param _feeRecipient address that collect fees
   * @param _bonusToken extra token for donations
   * @return id the unique id of an hToken
   */
  function _getHodlId(
    address _token,
    uint256 _penalty,
    uint256 _lockWindow,
    uint256 _expiry,
    uint256 _fee,
    uint256 _n,
    address _feeRecipient,
    address _bonusToken
  ) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked(_token, _penalty, _lockWindow, _expiry, _fee, _n, _feeRecipient, _bonusToken));
  }

  function _concat(string memory a, string memory b) internal pure returns (string memory) {
    return string(abi.encodePacked(a, b));
  }
}
/**
 * @title A factory to create hToken
 * @notice Create new hToken and keep track of all hToken addresses
 * @dev Calculate contract address before each creation with CREATE2
 * and deploy eip-1167 minimal proxies for the logic contract
 */
function _getHodlId(
  address _token,
  uint256 _penalty,
  uint256 _lockWindow,
  uint256 _expiry,
  uint256 _fee,
  uint256 _n,
  address _feeRecipient,
  address _bonusToken
) internal pure returns (bytes32) {
  return keccak256(abi.encodePacked(_token, _penalty, _lockWindow, _expiry, _fee, _n, _feeRecipient, _bonusToken));
}

/**
 * @dev hash parameters and get a unique hToken id
 * @param _token token to hold
 * @param _penalty penalty 1 out of 1000
 * @param _lockWindow duration locked before expiry
 * @param _expiry expiry timestamp
 * @param _fee fee out of every 1000 penalty
 * @param _feeRecipient address that collect fees
 * @param _bonusToken extra token for donations
 * @return id the unique id of an hToken
 */