// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

import {IPool} from '../../../interfaces/IPool.sol';
import {IInitializableAToken} from '../../../interfaces/IInitializableAToken.sol';
import {IInitializableDebtToken} from '../../../interfaces/IInitializableDebtToken.sol';
import {InitializableImmutableAdminUpgradeabilityProxy} from '../../../misc/aave-upgradeability/InitializableImmutableAdminUpgradeabilityProxy.sol';
import {IReserveInterestRateStrategy} from '../../../interfaces/IReserveInterestRateStrategy.sol';
import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol';
import {DataTypes} from '../types/DataTypes.sol';
import {Errors} from '../helpers/Errors.sol';
import {ConfiguratorInputTypes} from '../types/ConfiguratorInputTypes.sol';
import {IERC20Detailed} from '../../../dependencies/openzeppelin/contracts/IERC20Detailed.sol';

/**
 * @title ConfiguratorLogic library
 * @author Aave
 * @notice Implements the functions to initialize reserves and update aTokens and debtTokens
 */
library ConfiguratorLogic {
  using ReserveConfiguration for DataTypes.ReserveConfigurationMap;

  // See `IPoolConfigurator` for descriptions
  event ReserveInitialized(
    address indexed asset,
    address indexed aToken,
    address stableDebtToken,
    address variableDebtToken,
    address interestRateStrategyAddress
  );
  event ATokenUpgraded(
    address indexed asset,
    address indexed proxy,
    address indexed implementation
  );
  event VariableDebtTokenUpgraded(
    address indexed asset,
    address indexed proxy,
    address indexed implementation
  );

  /**
   * @notice Initialize a reserve by creating and initializing aToken and variable debt token
   * @dev Emits the `ReserveInitialized` event
   * @param pool The Pool in which the reserve will be initialized
   * @param input The needed parameters for the initialization
   */
  function executeInitReserve(
    IPool pool,
    ConfiguratorInputTypes.InitReserveInput calldata input
  ) external {
    // It is an assumption that the asset listed is non-malicious, and the external call doesn't create re-entrancies
    uint8 underlyingAssetDecimals = IERC20Detailed(input.underlyingAsset).decimals();
    require(underlyingAssetDecimals > 5, Errors.INVALID_DECIMALS);

    address aTokenProxyAddress = _initTokenWithProxy(
      input.aTokenImpl,
      abi.encodeWithSelector(
        IInitializableAToken.initialize.selector,
        pool,
        input.treasury,
        input.underlyingAsset,
        input.incentivesController,
        underlyingAssetDecimals,
        input.aTokenName,
        input.aTokenSymbol,
        input.params
      )
    );

    address variableDebtTokenProxyAddress = _initTokenWithProxy(
      input.variableDebtTokenImpl,
      abi.encodeWithSelector(
        IInitializableDebtToken.initialize.selector,
        pool,
        input.underlyingAsset,
        input.incentivesController,
        underlyingAssetDecimals,
        input.variableDebtTokenName,
        input.variableDebtTokenSymbol,
        input.params
      )
    );

    pool.initReserve(
      input.underlyingAsset,
      aTokenProxyAddress,
      variableDebtTokenProxyAddress,
      input.interestRateStrategyAddress
    );

    DataTypes.ReserveConfigurationMap memory currentConfig = DataTypes.ReserveConfigurationMap(0);

    currentConfig.setDecimals(underlyingAssetDecimals);

    currentConfig.setActive(true);
    currentConfig.setPaused(false);
    currentConfig.setFrozen(false);
    currentConfig.setVirtualAccActive(input.useVirtualBalance);

    pool.setConfiguration(input.underlyingAsset, currentConfig);

    IReserveInterestRateStrategy(input.interestRateStrategyAddress).setInterestRateParams(
      input.underlyingAsset,
      input.interestRateData
    );

    emit ReserveInitialized(
      input.underlyingAsset,
      aTokenProxyAddress,
      address(0),
      variableDebtTokenProxyAddress,
      input.interestRateStrategyAddress
    );
  }

  /**
   * @notice Updates the aToken implementation and initializes it
   * @dev Emits the `ATokenUpgraded` event
   * @param cachedPool The Pool containing the reserve with the aToken
   * @param input The parameters needed for the initialize call
   */
  function executeUpdateAToken(
    IPool cachedPool,
    ConfiguratorInputTypes.UpdateATokenInput calldata input
  ) external {
    DataTypes.ReserveDataLegacy memory reserveData = cachedPool.getReserveData(input.asset);

    (, , , uint256 decimals, ) = cachedPool.getConfiguration(input.asset).getParams();

    bytes memory encodedCall = abi.encodeWithSelector(
      IInitializableAToken.initialize.selector,
      cachedPool,
      input.treasury,
      input.asset,
      input.incentivesController,
      decimals,
      input.name,
      input.symbol,
      input.params
    );

    _upgradeTokenImplementation(reserveData.aTokenAddress, input.implementation, encodedCall);

    emit ATokenUpgraded(input.asset, reserveData.aTokenAddress, input.implementation);
  }

  /**
   * @notice Updates the variable debt token implementation and initializes it
   * @dev Emits the `VariableDebtTokenUpgraded` event
   * @param cachedPool The Pool containing the reserve with the variable debt token
   * @param input The parameters needed for the initialize call
   */
  function executeUpdateVariableDebtToken(
    IPool cachedPool,
    ConfiguratorInputTypes.UpdateDebtTokenInput calldata input
  ) external {
    DataTypes.ReserveDataLegacy memory reserveData = cachedPool.getReserveData(input.asset);

    (, , , uint256 decimals, ) = cachedPool.getConfiguration(input.asset).getParams();

    bytes memory encodedCall = abi.encodeWithSelector(
      IInitializableDebtToken.initialize.selector,
      cachedPool,
      input.asset,
      input.incentivesController,
      decimals,
      input.name,
      input.symbol,
      input.params
    );

    _upgradeTokenImplementation(
      reserveData.variableDebtTokenAddress,
      input.implementation,
      encodedCall
    );

    emit VariableDebtTokenUpgraded(
      input.asset,
      reserveData.variableDebtTokenAddress,
      input.implementation
    );
  }

  /**
   * @notice Creates a new proxy and initializes the implementation
   * @param implementation The address of the implementation
   * @param initParams The parameters that is passed to the implementation to initialize
   * @return The address of initialized proxy
   */
  function _initTokenWithProxy(
    address implementation,
    bytes memory initParams
  ) internal returns (address) {
    InitializableImmutableAdminUpgradeabilityProxy proxy = new InitializableImmutableAdminUpgradeabilityProxy(
        address(this)
      );

    proxy.initialize(implementation, initParams);

    return address(proxy);
  }

  /**
   * @notice Upgrades the implementation and makes call to the proxy
   * @dev The call is used to initialize the new implementation.
   * @param proxyAddress The address of the proxy
   * @param implementation The address of the new implementation
   * @param  initParams The parameters to the call after the upgrade
   */
  function _upgradeTokenImplementation(
    address proxyAddress,
    address implementation,
    bytes memory initParams
  ) internal {
    InitializableImmutableAdminUpgradeabilityProxy proxy = InitializableImmutableAdminUpgradeabilityProxy(
        payable(proxyAddress)
      );

    proxy.upgradeToAndCall(implementation, initParams);
  }
}
