// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@gnosis.pm/zodiac/contracts/factory/ModuleProxyFactory.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import "./Baal.sol";

contract BaalSummoner is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    // when some of the init addresses are updated
    uint256 public addrsVersion;

    address payable public template; // fixed template for baal using eip-1167 proxy pattern

    // Template contract to use for new Gnosis safe proxies
    address public gnosisSingleton;

    // Library to use for EIP1271 compatability
    address public gnosisFallbackLibrary;

    // Library to use for all safe transaction executions
    address public gnosisMultisendLibrary;

    // template contract to clone for loot ERC20 token
    address public lootSingleton;

    // template contract to clone for shares ERC20 token
    address public sharesSingleton;

    // Proxy summoners
    //
    GnosisSafeProxyFactory gnosisSafeProxyFactory;
    ModuleProxyFactory moduleProxyFactory;

    event SetAddrsVersion(
        uint256 version
    );

    event SummonBaal(
        address indexed baal,
        address indexed loot,
        address indexed shares,
        address safe,
        address forwarder,
        uint256 existingAddrs
    );

    event DaoReferral(
        bytes32 referrer,
        address daoAddress
    );

    event DeployBaalTokens(
        address lootToken, 
        address sharesToken
    );

    event DeployBaalSafe(
        address baalSafe,
        address moduleAddr
    );

    function initialize() initializer public {
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    // must be called after deploy to set libraries
    function setAddrs(
        address payable _template,
        address _gnosisSingleton,
        address _gnosisFallbackLibrary,
        address _gnosisMultisendLibrary,
        address _gnosisSafeProxyFactory,
        address _moduleProxyFactory,
        address _lootSingleton,
        address _sharesSingleton
    ) public onlyOwner {
        require(_lootSingleton != address(0), "!lootSingleton");
        require(_sharesSingleton != address(0), "!sharesSingleton");
        require(_gnosisSingleton != address(0), "!gnosisSingleton");
        require(_gnosisFallbackLibrary != address(0), '!gnosisFallbackLibrary');
        require(_gnosisMultisendLibrary != address(0), '!gnosisMultisendLibrary');
        require(_gnosisSafeProxyFactory != address(0), '!gnosisSafeProxyFactory');
        require(_moduleProxyFactory != address(0), '!moduleProxyFactory');

        template = _template;
        gnosisSingleton = _gnosisSingleton;
        gnosisFallbackLibrary = _gnosisFallbackLibrary;
        gnosisMultisendLibrary = _gnosisMultisendLibrary;
        gnosisSafeProxyFactory = GnosisSafeProxyFactory(_gnosisSafeProxyFactory);
        moduleProxyFactory = ModuleProxyFactory(_moduleProxyFactory);
        lootSingleton = _lootSingleton;
        sharesSingleton = _sharesSingleton;

        emit SetAddrsVersion(
        addrsVersion++
        );
        
    }

    function encodeMultisend(bytes[] memory _calls, address _target)
        public
        pure
        returns (bytes memory encodedMultisend)
    {
        bytes memory encodedActions;
        for (uint256 i = 0; i < _calls.length; i++) {
            encodedActions = abi.encodePacked(
                encodedActions,
                uint8(0),
                _target,
                uint256(0),
                uint256(_calls[i].length),
                bytes(_calls[i])
            );
        }
        encodedMultisend = abi.encodeWithSignature(
            "multiSend(bytes)",
            encodedActions
        );
    }

    function summonBaal(
        bytes calldata initializationParams,
        bytes[] calldata initializationActions,
        uint256 _saltNonce
    ) external returns (address) {
        
        return
            _summonBaal(
                initializationParams,
                initializationActions,
                _saltNonce
            );
    }

    // Add a referrer to help keep track of where deploies are coming from
    function summonBaalFromReferrer(
        bytes calldata initializationParams,
        bytes[] calldata initializationActions,
        uint256 _saltNonce,
        bytes32 referrer
    ) external payable returns (address) {
        address daoAddress;

        daoAddress = _summonBaal(
            initializationParams,
            initializationActions,
            _saltNonce
        );

        emit DaoReferral(referrer, daoAddress);
        return daoAddress;
    }

    // deploy new share and loot contracts
    function deployTokens(string memory _name, string memory _symbol) 
        public 
        returns (address lootToken, address sharesToken) 
    {
        lootToken = address(new ERC1967Proxy(
            lootSingleton,
            abi.encodeWithSelector(
                IBaalToken(lootSingleton).setUp.selector, 
                string(abi.encodePacked(_name, " LOOT")), 
                string(abi.encodePacked(_symbol, "-LOOT")))
        ));

        sharesToken = address(new ERC1967Proxy(
            sharesSingleton,
            abi.encodeWithSelector(
                IBaalToken(sharesSingleton).setUp.selector, 
                _name, 
                _symbol)
        ));

        emit DeployBaalTokens(lootToken, sharesToken);

    }

    function configureSafe(address _moduleAddr, GnosisSafe _safe) internal {
        // Generate delegate calls so the safe calls enableModule on itself during setup
        bytes memory _enableBaal = abi.encodeWithSignature(
            "enableModule(address)",
            address(_moduleAddr)
        );
        bytes memory _enableBaalMultisend = abi.encodePacked(
            uint8(0),
            address(_safe),
            uint256(0),
            uint256(_enableBaal.length),
            bytes(_enableBaal)
        );

        bytes memory _multisendAction = abi.encodeWithSignature(
            "multiSend(bytes)",
            _enableBaalMultisend
        );

        // Workaround for solidity dynamic memory array
        address[] memory _owners = new address[](1);
        _owners[0] = address(_moduleAddr);

        // Call setup on safe to enable our new module and set the module as the only signer
        _safe.setup(
            _owners,
            1,
            gnosisMultisendLibrary,
            _multisendAction,
            gnosisFallbackLibrary,
            address(0),
            0,
            payable(address(0))
        );
    }


    // deploy a safe with module and single module signer setup
    // with nounce
    function deployAndSetupSafe(address _moduleAddr, uint256 _saltNonce)
        public
        returns (address)
    {
        GnosisSafe _safe = GnosisSafe(
            payable(
                gnosisSafeProxyFactory.createProxyWithNonce(
                    gnosisSingleton,
                    bytes(""),
                    _saltNonce
                )
            )
        );
        
        configureSafe(_moduleAddr, _safe);

        emit DeployBaalSafe(address(_safe), address(_moduleAddr));

        return address(_safe);
    }


    // deploy a safe with module and single module signer setup
    function deployAndSetupSafe(address _moduleAddr)
        public
        returns (address)
    {
        GnosisSafe _safe = GnosisSafe(
            payable(
        gnosisSafeProxyFactory.createProxy(
                    gnosisSingleton,
                    bytes("")
                )
                )
        );

        configureSafe(_moduleAddr, _safe);

        emit DeployBaalSafe(address(_safe), address(_moduleAddr));

        return address(_safe);
    }

    // advanced summon baal with different configurations
    // name and symbol can be blank if bringing own baal tokens
    // zero address for either loot or shares token will summon new ones
    // if bringing own tokens the ownership must be transfered to the new DAO
    // zero address for Safe with summon and setup a new Safe
    // if bringing existing safe the new dao must be enabled as a module
    // todo: add a simple summon that just creates a dao with a single summoner
    function _summonBaal(
        bytes calldata initializationParams,
        bytes[] calldata initializationActions,
        uint256 _saltNonce
    ) internal returns (address) {
        uint256 existingAddrs; // 1 tokens, 2 safe, 3 both
        (
            string memory _name, /*_name Name for erc20 `shares` accounting, empty if token */
            string memory _symbol, /*_symbol Symbol for erc20 `shares` accounting, empty if token*/
            address _safeAddr, /*address of safe, 0 addr if new*/
            address _forwarder, /*Trusted forwarder address for meta-transactions (EIP 2771), 0 addr if initially disabled*/
            address _lootToken, /*predeployed loot token, 0 addr if new*/
            address _sharesToken /*predeployed shares token, 0 addr if new*/
        ) = abi.decode(initializationParams, (string, string, address, address, address, address));

        Baal _baal = Baal(
            moduleProxyFactory.deployModule(
                template, 
                abi.encodeWithSignature("avatar()"), 
                _saltNonce
            )
        );

        // if loot or shares are zero address new tokens are deployed
        // tokens need to be baalTokens
        if(_lootToken == address(0) || _sharesToken == address(0)){
            (_lootToken, _sharesToken) = deployTokens(_name, _symbol);
            // pause tokens by default and transfer to the DAO
            IBaalToken(_lootToken).pause();
            IBaalToken(_sharesToken).pause();
            IBaalToken(_lootToken).transferOwnership(address(_baal));
            IBaalToken(_sharesToken).transferOwnership(address(_baal));
        } else {
            existingAddrs += 1;
        }

        // if zero address deploy a new safe
        // Needs to be a valid zodiac treasury
        if(_safeAddr == address(0)){
            _safeAddr = deployAndSetupSafe(address(_baal), _saltNonce);
        } else {
            existingAddrs += 2;
        }

        bytes memory _initializationMultisendData = encodeMultisend(
            initializationActions,
            address(_baal)
        );
        bytes memory _initializer = abi.encode(
            _lootToken,
            _sharesToken,
            gnosisMultisendLibrary,
            _safeAddr,
            _forwarder,
            _initializationMultisendData
        );
        // can run the actions now because we have a baal
        _baal.setUp(_initializer);

        emit SummonBaal(
            address(_baal),
            address(_baal.lootToken()),
            address(_baal.sharesToken()),
            _safeAddr,
            _forwarder,
            existingAddrs
        );

        return (address(_baal));
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}
}
