contract StreamingFeeModule is ModuleBase, ReentrancyGuard {
  using SafeMath for uint256;
  using PreciseUnitMath for uint256;
  using SafeCast for uint256;

  using SignedSafeMath for int256;
  using PreciseUnitMath for int256;
  using SafeCast for int256;

  /* ============ Structs ============ */

  struct FeeState {
    address feeRecipient; // Address to accrue fees to
    uint256 maxStreamingFeePercentage; // Max streaming fee maanager commits to using (1% = 1e16, 100% = 1e18)
    uint256 streamingFeePercentage; // Percent of Set accruing to manager annually (1% = 1e16, 100% = 1e18)
    uint256 lastStreamingFeeTimestamp; // Timestamp last streaming fee was accrued
  }

  /* ============ Events ============ */

  event FeeActualized(address indexed _setToken, uint256 _managerFee, uint256 _protocolFee);
  event StreamingFeeUpdated(address indexed _setToken, uint256 _newStreamingFee);
  event FeeRecipientUpdated(address indexed _setToken, address _newFeeRecipient);

  /* ============ Constants ============ */

  uint256 private constant ONE_YEAR_IN_SECONDS = 365.25 days;
  uint256 private constant PROTOCOL_STREAMING_FEE_INDEX = 0;

  /* ============ State Variables ============ */

  mapping(ISetToken => FeeState) public feeStates;

  /* ============ Constructor ============ */

  // solhint-disable-next-line no-empty-blocks
  constructor(IController _controller) ModuleBase(_controller) {}

  /* ============ External Functions ============ */

  /*
   * Calculates total inflation percentage then mints new Sets to the fee recipient. Position units are
   * then adjusted down (in magnitude) in order to ensure full collateralization. Callable by anyone.
   *
   * @param _setToken       Address of SetToken
   */
  function accrueFee(ISetToken _setToken)
    public
    nonReentrant
    onlyValidAndInitializedSet(_setToken)
  {
    uint256 managerFee;
    uint256 protocolFee;

    if (_streamingFeePercentage(_setToken) > 0) {
      uint256 inflationFeePercentage = _calculateStreamingFee(_setToken);

      // Calculate incentiveFee inflation
      uint256 feeQuantity = _calculateStreamingFeeInflation(_setToken, inflationFeePercentage);

      // Mint new Sets to manager and protocol
      (managerFee, protocolFee) = _mintManagerAndProtocolFee(_setToken, feeQuantity);

      _editPositionMultiplier(_setToken, inflationFeePercentage);
    }

    // solhint-disable-next-line not-rely-on-time
    feeStates[_setToken].lastStreamingFeeTimestamp = block.timestamp;

    emit FeeActualized(address(_setToken), managerFee, protocolFee);
  }

  /**
   * SET MANAGER ONLY. Initialize module with SetToken and set the fee state for the SetToken. Passed
   * _settings will have lastStreamingFeeTimestamp over-written.
   *
   * @param _setToken                 Address of SetToken
   * @param _settings                 FeeState struct defining fee parameters
   */
  function initialize(ISetToken _setToken, FeeState memory _settings)
    external
    onlySetManager(_setToken, msg.sender)
    onlyValidAndPendingSet(_setToken)
  {
    require(_settings.feeRecipient != address(0), "Fee Recipient must be non-zero address.");
    require(
      _settings.maxStreamingFeePercentage < PreciseUnitMath.preciseUnit(),
      "Max fee must be < 100%."
    );
    require(
      _settings.streamingFeePercentage <= _settings.maxStreamingFeePercentage,
      "Fee must be <= max."
    );

    // solhint-disable-next-line not-rely-on-time
    _settings.lastStreamingFeeTimestamp = block.timestamp;

    feeStates[_setToken] = _settings;
    _setToken.initializeModule();
  }

  /**
   * Removes this module from the SetToken, via call by the SetToken. Manager's feeState is deleted. Fees
   * are not accrued in case reason for removing module is related to fee accrual.
   */
  function removeModule() external override {
    delete feeStates[ISetToken(msg.sender)];
  }

  /*
   * Set new streaming fee. Fees accrue at current rate then new rate is set.
   * Fees are accrued to prevent the manager from unfairly accruing a larger percentage.
   *
   * @param _setToken       Address of SetToken
   * @param _newFee         New streaming fee 18 decimal precision
   */
  function updateStreamingFee(ISetToken _setToken, uint256 _newFee)
    external
    onlySetManager(_setToken, msg.sender)
    onlyValidAndInitializedSet(_setToken)
  {
    require(_newFee < _maxStreamingFeePercentage(_setToken), "Fee must be less than max");
    accrueFee(_setToken);

    feeStates[_setToken].streamingFeePercentage = _newFee;

    emit StreamingFeeUpdated(address(_setToken), _newFee);
  }

  /*
   * Set new fee recipient.
   *
   * @param _setToken             Address of SetToken
   * @param _newFeeRecipient      New fee recipient
   */
  function updateFeeRecipient(ISetToken _setToken, address _newFeeRecipient)
    external
    onlySetManager(_setToken, msg.sender)
    onlyValidAndInitializedSet(_setToken)
  {
    require(_newFeeRecipient != address(0), "Fee Recipient must be non-zero address.");

    feeStates[_setToken].feeRecipient = _newFeeRecipient;

    emit FeeRecipientUpdated(address(_setToken), _newFeeRecipient);
  }

  /*
   * Calculates total inflation percentage in order to accrue fees to manager.
   *
   * @param _setToken       Address of SetToken
   * @return  uint256       Percent inflation of supply
   */
  function getFee(ISetToken _setToken) external view returns (uint256) {
    return _calculateStreamingFee(_setToken);
  }

  /* ============ Internal Functions ============ */

  /**
   * Calculates streaming fee by multiplying streamingFeePercentage by the elapsed amount of time since the last fee
   * was collected divided by one year in seconds, since the fee is a yearly fee.
   *
   * @param  _setToken          Address of Set to have feeState updated
   * @return uint256            Streaming fee denominated in percentage of totalSupply
   */
  function _calculateStreamingFee(ISetToken _setToken) internal view returns (uint256) {
    // solhint-disable-next-line not-rely-on-time
    uint256 timeSinceLastFee = block.timestamp.sub(_lastStreamingFeeTimestamp(_setToken));

    // Streaming fee is streaming fee times years since last fee
    return timeSinceLastFee.mul(_streamingFeePercentage(_setToken)).div(ONE_YEAR_IN_SECONDS);
  }

  /**
   * Returns the new incentive fee denominated in the number of SetTokens to mint. The calculation for the fee involves
   * implying mint quantity so that the feeRecipient owns the fee percentage of the entire supply of the Set.
   *
   * The formula to solve for fee is:
   * (feeQuantity / feeQuantity) + totalSupply = fee / scaleFactor
   *
   * The simplified formula utilized below is:
   * feeQuantity = fee * totalSupply / (scaleFactor - fee)
   *
   * @param   _setToken               SetToken instance
   * @param   _feePercentage          Fee levied to feeRecipient
   * @return  uint256                 New RebalancingSet issue quantity
   */
  function _calculateStreamingFeeInflation(ISetToken _setToken, uint256 _feePercentage)
    internal
    view
    returns (uint256)
  {
    uint256 totalSupply = _setToken.totalSupply();

    // fee * totalSupply
    uint256 a = _feePercentage.mul(totalSupply);

    // ScaleFactor (10e18) - fee
    uint256 b = PreciseUnitMath.preciseUnit().sub(_feePercentage);

    return a.div(b);
  }

  /**
   * Mints sets to both the manager and the protocol. Protocol takes a percentage fee of the total amount of Sets
   * minted to manager.
   *
   * @param   _setToken               SetToken instance
   * @param   _feeQuantity            Amount of Sets to be minted as fees
   * @return  uint256                 Amount of Sets accrued to manager as fee
   * @return  uint256                 Amount of Sets accrued to protocol as fee
   */
  function _mintManagerAndProtocolFee(ISetToken _setToken, uint256 _feeQuantity)
    internal
    returns (uint256, uint256)
  {
    address protocolFeeRecipient = controller.feeRecipient();
    uint256 protocolFee = controller.getModuleFee(address(this), PROTOCOL_STREAMING_FEE_INDEX);

    uint256 protocolFeeAmount = _feeQuantity.preciseMul(protocolFee);
    uint256 managerFeeAmount = _feeQuantity.sub(protocolFeeAmount);

    _setToken.mint(_feeRecipient(_setToken), managerFeeAmount);

    if (protocolFeeAmount > 0) {
      _setToken.mint(protocolFeeRecipient, protocolFeeAmount);
    }

    return (managerFeeAmount, protocolFeeAmount);
  }

  /**
   * Calculates new position multiplier according to following formula:
   *
   * newMultiplier = oldMultiplier * (1-inflationFee)
   *
   * This reduces position sizes to offset increase in supply due to fee collection.
   *
   * @param   _setToken               SetToken instance
   * @param   _inflationFee           Fee inflation rate
   */
  function _editPositionMultiplier(ISetToken _setToken, uint256 _inflationFee) internal {
    int256 currentMultipler = _setToken.positionMultiplier();
    int256 newMultiplier = currentMultipler.preciseMul(
      PreciseUnitMath.preciseUnit().sub(_inflationFee).toInt256()
    );

    _setToken.editPositionMultiplier(newMultiplier);
  }

  function _feeRecipient(ISetToken _set) internal view returns (address) {
    return feeStates[_set].feeRecipient;
  }

  function _lastStreamingFeeTimestamp(ISetToken _set) internal view returns (uint256) {
    return feeStates[_set].lastStreamingFeeTimestamp;
  }

  function _maxStreamingFeePercentage(ISetToken _set) internal view returns (uint256) {
    return feeStates[_set].maxStreamingFeePercentage;
  }

  function _streamingFeePercentage(ISetToken _set) internal view returns (uint256) {
    return feeStates[_set].streamingFeePercentage;
  }
}
/**
 * @title StreamingFeeModule
 * @author Set Protocol
 *
 * Smart contract that accrues streaming fees for Set managers. Streaming fees are denominated as percent
 * per year and realized as Set inflation rewarded to the manager.
 */
function _editPositionMultiplier(ISetToken _setToken, uint256 _inflationFee) internal {
  int256 currentMultipler = _setToken.positionMultiplier();
  int256 newMultiplier = currentMultipler.preciseMul(
    PreciseUnitMath.preciseUnit().sub(_inflationFee).toInt256()
  );

  _setToken.editPositionMultiplier(newMultiplier);
}

/**
 * Calculates new position multiplier according to following formula:
 *
 * newMultiplier = oldMultiplier * (1-inflationFee)
 *
 * This reduces position sizes to offset increase in supply due to fee collection.
 *
 * @param   _setToken               SetToken instance
 * @param   _inflationFee           Fee inflation rate
 */