library Position {
  using SafeCast for uint256;
  using SafeMath for uint256;
  using SafeCast for int256;
  using SignedSafeMath for int256;
  using PreciseUnitMath for uint256;

  /* ============ Helper ============ */

  /**
   * Returns whether the SetToken has a default position for a given component (if the real unit is > 0)
   */
  function hasDefaultPosition(ISetToken _setToken, address _component)
    internal
    view
    returns (bool)
  {
    return _setToken.getDefaultPositionRealUnit(_component) > 0;
  }

  /**
   * Returns whether the SetToken has an external position for a given component (if # of position modules is > 0)
   */
  function hasExternalPosition(ISetToken _setToken, address _component)
    internal
    view
    returns (bool)
  {
    return _setToken.getExternalPositionModules(_component).length > 0;
  }

  /**
   * Returns whether the SetToken component default position real unit is greater than or equal to units passed in.
   */
  function hasSufficientDefaultUnits(
    ISetToken _setToken,
    address _component,
    uint256 _unit
  ) internal view returns (bool) {
    return _setToken.getDefaultPositionRealUnit(_component) >= _unit.toInt256();
  }

  /**
   * Returns whether the SetToken component external position is greater than or equal to the real units passed in.
   */
  function hasSufficientExternalUnits(
    ISetToken _setToken,
    address _component,
    address _positionModule,
    uint256 _unit
  ) internal view returns (bool) {
    return _setToken.getExternalPositionRealUnit(_component, _positionModule) >= _unit.toInt256();
  }

  /**
   * If the position does not exist, create a new Position and add to the SetToken. If it already exists,
   * then set the position units. If the new units is 0, remove the position. Handles adding/removing of
   * components where needed (in light of potential external positions).
   *
   * @param _setToken           Address of SetToken being modified
   * @param _component          Address of the component
   * @param _newUnit            Quantity of Position units - must be >= 0
   */
  function editDefaultPosition(
    ISetToken _setToken,
    address _component,
    uint256 _newUnit
  ) internal {
    bool isPositionFound = hasDefaultPosition(_setToken, _component);
    if (!isPositionFound && _newUnit > 0) {
      // If there is no Default Position and no External Modules, then component does not exist
      if (!hasExternalPosition(_setToken, _component)) {
        _setToken.addComponent(_component);
      }
    } else if (isPositionFound && _newUnit == 0) {
      // If there is a Default Position and no external positions, remove the component
      if (!hasExternalPosition(_setToken, _component)) {
        _setToken.removeComponent(_component);
      }
    }

    _setToken.editDefaultPositionUnit(_component, _newUnit.toInt256());
  }

  /**
   * Update an external position and remove and external positions or components if necessary. The logic flows as follows:
   * 1) If component is not already added then add component and external position.
   * 2) If component is added but no existing external position using the passed module exists then add the external position.
   * 3) If the existing position is being added to then just update the unit and data
   * 4) If the position is being closed and no other external positions or default positions are associated with the component
   *    then untrack the component and remove external position.
   * 5) If the position is being closed and other existing positions still exist for the component then just remove the
   *    external position.
   *
   * @param _setToken         SetToken being updated
   * @param _component        Component position being updated
   * @param _module           Module external position is associated with
   * @param _newUnit          Position units of new external position
   * @param _data             Arbitrary data associated with the position
   */
  function editExternalPosition(
    ISetToken _setToken,
    address _component,
    address _module,
    int256 _newUnit,
    bytes memory _data
  ) internal {
    if (_newUnit != 0) {
      if (!_setToken.isComponent(_component)) {
        _setToken.addComponent(_component);
        _setToken.addExternalPositionModule(_component, _module);
      } else if (!_setToken.isExternalPositionModule(_component, _module)) {
        _setToken.addExternalPositionModule(_component, _module);
      }
      _setToken.editExternalPositionUnit(_component, _module, _newUnit);
      _setToken.editExternalPositionData(_component, _module, _data);
    } else {
      require(_data.length == 0, "Passed data must be null");
      // If no default or external position remaining then remove component from components array
      if (_setToken.getExternalPositionRealUnit(_component, _module) != 0) {
        address[] memory positionModules = _setToken.getExternalPositionModules(_component);
        if (_setToken.getDefaultPositionRealUnit(_component) == 0 && positionModules.length == 1) {
          require(
            positionModules[0] == _module,
            "External positions must be 0 to remove component"
          );
          _setToken.removeComponent(_component);
        }
        _setToken.removeExternalPositionModule(_component, _module);
      }
    }
  }

  /**
   * Get total notional amount of Default position
   *
   * @param _setTokenSupply     Supply of SetToken in precise units (10^18)
   * @param _positionUnit       Quantity of Position units
   *
   * @return                    Total notional amount of units
   */
  function getDefaultTotalNotional(uint256 _setTokenSupply, uint256 _positionUnit)
    internal
    pure
    returns (uint256)
  {
    return _setTokenSupply.preciseMul(_positionUnit);
  }

  /**
   * Get position unit from total notional amount
   *
   * @param _setTokenSupply     Supply of SetToken in precise units (10^18)
   * @param _totalNotional      Total notional amount of component prior to
   * @return                    Default position unit
   */
  function getDefaultPositionUnit(uint256 _setTokenSupply, uint256 _totalNotional)
    internal
    pure
    returns (uint256)
  {
    return _totalNotional.preciseDiv(_setTokenSupply);
  }

  /**
   * Get the total tracked balance - total supply * position unit
   *
   * @param _setToken           Address of the SetToken
   * @param _component          Address of the component
   * @return                    Notional tracked balance
   */
  function getDefaultTrackedBalance(ISetToken _setToken, address _component)
    internal
    view
    returns (uint256)
  {
    int256 positionUnit = _setToken.getDefaultPositionRealUnit(_component);
    return _setToken.totalSupply().preciseMul(positionUnit.toUint256());
  }

  /**
   * Calculates the new default position unit and performs the edit with the new unit
   *
   * @param _setToken                 Address of the SetToken
   * @param _component                Address of the component
   * @param _setTotalSupply           Current SetToken supply
   * @param _componentPreviousBalance Pre-action component balance
   * @return                          Current component balance
   * @return                          Previous position unit
   * @return                          New position unit
   */
  function calculateAndEditDefaultPosition(
    ISetToken _setToken,
    address _component,
    uint256 _setTotalSupply,
    uint256 _componentPreviousBalance
  )
    internal
    returns (
      uint256,
      uint256,
      uint256
    )
  {
    uint256 currentBalance = IERC20(_component).balanceOf(address(_setToken));
    uint256 positionUnit = _setToken.getDefaultPositionRealUnit(_component).toUint256();

    uint256 newTokenUnit;
    if (currentBalance > 0) {
      newTokenUnit = calculateDefaultEditPositionUnit(
        _setTotalSupply,
        _componentPreviousBalance,
        currentBalance,
        positionUnit
      );
    } else {
      newTokenUnit = 0;
    }

    editDefaultPosition(_setToken, _component, newTokenUnit);

    return (currentBalance, positionUnit, newTokenUnit);
  }

  /**
   * Calculate the new position unit given total notional values pre and post executing an action that changes SetToken state
   * The intention is to make updates to the units without accidentally picking up airdropped assets as well.
   *
   * @param _setTokenSupply     Supply of SetToken in precise units (10^18)
   * @param _preTotalNotional   Total notional amount of component prior to executing action
   * @param _postTotalNotional  Total notional amount of component after the executing action
   * @param _prePositionUnit    Position unit of SetToken prior to executing action
   * @return                    New position unit
   */
  function calculateDefaultEditPositionUnit(
    uint256 _setTokenSupply,
    uint256 _preTotalNotional,
    uint256 _postTotalNotional,
    uint256 _prePositionUnit
  ) internal pure returns (uint256) {
    // If pre action total notional amount is greater then subtract post action total notional and calculate new position units
    uint256 airdroppedAmount = _preTotalNotional.sub(_prePositionUnit.preciseMul(_setTokenSupply));
    return _postTotalNotional.sub(airdroppedAmount).preciseDiv(_setTokenSupply);
  }
}
/**
 * @title Position
 * @author Set Protocol
 *
 * Collection of helper functions for handling and updating SetToken Positions
 *
 * CHANGELOG:
 *  - Updated editExternalPosition to work when no external position is associated with module
 */
function hasExternalPosition(ISetToken _setToken, address _component)
  internal
  view
  returns (bool)
{
  return _setToken.getExternalPositionModules(_component).length > 0;
}

/**
 * Returns whether the SetToken has an external position for a given component (if # of position modules is > 0)
 */