pragma solidity ^0.8.11;

import "communal/ReentrancyGuard.sol";
import "communal/Owned.sol";
import { SD59x18, sd, fromSD59x18, add, sub, mul, div, sqrt, inv, powu, exp} from "@prb/math/SD59x18.sol";

interface ILSDRegistry {
    function vaultAddress() external view returns (address);
    //ratios should be in the form of [0.25e18, 0.25e18, 0.25e18, 0.25e18] for 4 LSDs
    function targetRatio() external view returns (uint256[] memory);
    function lsdAddresses() external view returns (address[] memory);
    function shanghaiTime() external view returns (uint256);
    function nonce() external view returns (uint256);
}

interface ILSDVault {
    function synchronize() external;
    function isSupported(address lsd) external returns (bool);
    function disableLSD(address lsd) external returns(bool);
}

interface IEuclideanFarm {
    function setRewardRate(address reward_token_address, uint256 new_rate, bool sync_too) external;
    function coordinationMultiplier() external view returns (uint256);
    function getAllRewardRates() external view returns (uint256[] memory);
}

contract LSDRegistry is Owned, ReentrancyGuard, ILSDRegistry {

    /* =========== State ================= */ 
    address public daoAddress;
    address public unshETHaddress;
    address public vaultAddress;
    address public vdMiningAddress;
    address public ushAddress;
    uint256 public shanghaiTime = 1680307199; //timestamp for March 31, 2023
    address[] public lsdAddressesInternal;
    uint256[] private targetRatioInternal;

    uint256 public lastUpdate; //timestamp of last time ratios or supported LSDs was changed
    uint256 public minUpdateCooldown = 21 days; //minimum number of time before ratios/tokens can be changed again
    uint256 public min_update_distance = 0.5e18;


    //used by Vault and Euclidean Farm to make sure they have the latest data
    uint256 public nonce = 0;
    uint256 public max_nonce = 2;

    /* ============ Constructor ============ */
    constructor(
        address _owner,
        address[] memory lsds,
        uint256[] memory _targetRatios
    ) Owned(_owner) {
        require(lsds.length == _targetRatios.length, "Array length mismatch");
        targetRatioInternal = _targetRatios;
        lsdAddressesInternal = lsds;
        lastUpdate = block.timestamp;
    }

    function setVaultAddress(address _vaultAddress) external onlyOwner {
        vaultAddress = _vaultAddress;
    }

    function setVdMiningAddress(address _vdMiningAddress) external onlyOwner {
        vdMiningAddress = _vdMiningAddress;
    }

    function setUSHAddress(address _ushAddress) external onlyOwner {
        ushAddress = _ushAddress;
    }

    //The reason for this fuckery is that the autogenerated getters for arrays are not compatible with the interface
    //so we have to manually create them

    function targetRatio() external view returns (uint256[] memory) {
        return targetRatioInternal;
    }

    function lsdAddresses() external view returns (address[] memory) {
        return lsdAddressesInternal;
    }

    //TODO: delete this for mainnet
    // function setTargetRatio(uint256[] calldata _targetRatios) external onlyOwner {
    //     //TODO: check if the length of an unintialized variable is 0 - probably? 
    //     require(targetRatioInternal.length == 0);
    //     targetRatioInternal = _targetRatios;
    //     nonce++;
    // }

    //TODO: limit these functions to DAO and limit the frequency at which they can be called
    //TODO: also make sure the farm and the vault get the memo
    //best to make them 'phone home' for this info rather than have us send updates twice

    // function updateTargetRatio(uint256[] calldata _targetRatios) external onlyOwner{
    //     //check that minUpdateCooldown has elapsed since lastUpdate
    //     require(((block.timestamp + minUpdateCooldown) > lastUpdate), "Cannot change weights yet!" );
    //     //check that length matches address length
    //     require(_targetRatios.length == lsdAddressesInternal.length, "Array length mismatch!");
    //     targetRatioInternal = _targetRatios;
    //     nonce++;
    //     ILSDVault(vaultAddress).synchronize();
    //     //TODO: update it in Euclidean farm too
    // }
    function euclideanDistance(uint256[] memory a, uint256[] memory b) internal pure returns (uint256) {
        require(a.length == b.length, "Array length mismatch");
        SD59x18 sumSqDiff = sd(0);
        for (uint256 i = 0; i < a.length; i++) {
            SD59x18 target = sd(int256(a[i]));
            SD59x18 current = sd(int256(b[i]));
            SD59x18 squareDiff = powu(sub(target, current), 2);
            sumSqDiff = sumSqDiff.add(squareDiff);
        }
        SD59x18 distance = inv(sqrt(sumSqDiff));
        uint256 dist = uint256(fromSD59x18(distance));
        return dist;
    }

        /* ========== MUTATIVE FUNCTIONS ========== */
    function updateTargetRatio(uint256[] memory proposedWeights) external onlyOwner {
        //check that minUpdateCooldown has elapsed since lastUpdate
        require(((block.timestamp + minUpdateCooldown) > lastUpdate), "Cannot change weights yet!" );
        require(proposedWeights.length == lsdAddressesInternal.length, "Array length mismatch!");
        require(nonce+1 < max_nonce, "Max nonce reached");
        require(block.timestamp > lastUpdate+minUpdateCooldown, "Wait until cooldown");
        //check if the new ratio has enough distance from the old
        uint256[] memory currentRatio = targetRatioInternal;
        uint256 distance = euclideanDistance(currentRatio, proposedWeights);
        require(distance >= min_update_distance, "New ratio too similar");
        targetRatioInternal = proposedWeights;
        nonce++;
        lastUpdate = block.timestamp;
        IEuclideanFarm vdMining = IEuclideanFarm(vdMiningAddress);
        uint256 new_rate = vdMining.coordinationMultiplier()*vdMining.getAllRewardRates()[0]/1e18;
        IEuclideanFarm(vdMiningAddress).setRewardRate(ushAddress, new_rate, true);
    }

    //disables further unshETH minting with a specific LSD
    function disableLSD(address lsd) external onlyOwner {
        require(ILSDVault(vaultAddress).isSupported(lsd), "lsd is not supported");
        //make sure the external call succeeds
        (bool success, ) = vaultAddress.call(abi.encodeWithSignature('disableLSD(address)', lsd));
        require(success);
    }

    //NOTE: this isn't going to change the shanghaiTime for the staked unshETH (nor should it)
    function delayShanghaiTime(uint256 newShanghaiTime) external onlyOwner {
        require(newShanghaiTime > shanghaiTime, "new shanghaiTime must be after current shanghaiTime");
        shanghaiTime = newShanghaiTime;
    }

    function setDAOAddress(address _daoAddress) external onlyOwner {
        //make sure the multisig address is nonzero
        require(_daoAddress != address(0x0));
        daoAddress = _daoAddress;
    } 

    function releaseToDAO() external onlyOwner {
        this.nominateNewOwner(daoAddress);
    }

    //emergency functions
}
[{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address[]","name":"lsds","type":"address[]"},{"internalType":"uint256[]","name":"_targetRatios","type":"uint256[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"PRBMathSD59x18__AbsMinSD59x18","type":"error"},{"inputs":[{"internalType":"SD59x18","name":"x","type":"int256"},{"internalType":"uint256","name":"y","type":"uint256"}],"name":"PRBMathSD59x18__PowuOverflow","type":"error"},{"inputs":[{"internalType":"SD59x18","name":"x","type":"int256"}],"name":"PRBMathSD59x18__SqrtNegativeInput","type":"error"},{"inputs":[{"internalType":"SD59x18","name":"x","type":"int256"}],"name":"PRBMathSD59x18__SqrtOverflow","type":"error"},{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"},{"internalType":"uint256","name":"y","type":"uint256"}],"name":"PRBMath__MulDiv18Overflow","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldOwner","type":"address"},{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerNominated","type":"event"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"daoAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"newShanghaiTime","type":"uint256"}],"name":"delayShanghaiTime","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"lsd","type":"address"}],"name":"disableLSD","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lastUpdate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lsdAddresses","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"lsdAddressesInternal","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"max_nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minUpdateCooldown","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"min_update_distance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"nominateNewOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"nominatedOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"releaseToDAO","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_daoAddress","type":"address"}],"name":"setDAOAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_ushAddress","type":"address"}],"name":"setUSHAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_vaultAddress","type":"address"}],"name":"setVaultAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_vdMiningAddress","type":"address"}],"name":"setVdMiningAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"shanghaiTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"targetRatio","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unshETHaddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"proposedWeights","type":"uint256[]"}],"name":"updateTargetRatio","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"ushAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vaultAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vdMiningAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]