// Contract: Factory
// Target function: function payout(uint _id) external
// SPDX-License-Identifier: None
pragma solidity ^0.8.14;

// @openzepplin/contracts/utils/Strings
// License: MIT
library Utils {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
    uint8 private constant _ADDRESS_LENGTH = 20;
    
    function toString(uint256 value) internal pure returns (string memory) {
        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }

    function toHexString(address addr) internal pure returns (string memory) {
        return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
    }
}

// @openzepplin/contracts/token/ERC20/IERC20
// License: MIT
interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

/**
* @title Squid Contract Factory.
* @author Dream Kollab.
* @notice You can use this factory to create and interface with squids.
* @dev All function calls are currently implemented without side effects.
* @custom:developer Etienne Cellier-Clarke.
*/
contract Factory {

    address payable private owner;
    address private manager;

    uint public creation_fee;
    uint public transaction_fee;
    uint public id_counter;

    mapping(uint => Squid) private squids;
    mapping(address => uint) private squid_ids;
    mapping(address => uint[]) private asoc_squids;
    mapping(address => uint[]) private created_squids;

    modifier onlyOwner {
        require(msg.sender == owner, "Error: 1000");
        _;
    }

    modifier onlyManagement {
        require(msg.sender == owner || msg.sender == manager, "Error: 1000");
        _;
    }

    fallback() external payable {}

    constructor() {
        owner = payable(msg.sender);
        manager = msg.sender;
        creation_fee = 10000000000000000; // Wei
        transaction_fee = 1000000; // Fraction of 100000000
        id_counter = 5457;
    }

    /**
    * @notice Use this method to create a new Squid smart contract.
    * @param _name Name of the contract.
    * @param _description Description of the contract.
    * @param _payees Array of addresses
    * @param _shares Array of integers which define how much each address owns.
    */
    function create(
        string memory _name,
        string memory _description,
        address[] memory _payees,
        uint[] memory _shares
    ) external payable {

        uint id = ++id_counter;

        require(!exists(id), 'Error: 1001');
        require(msg.value >= creation_fee, 'Error: 1002');
        require(bytes(_name).length <= 20, 'Error: 1003');
        require(bytes(_description).length <= 50, 'Error: 1003');
        require(_payees.length == _shares.length, 'Error: 1004');

        // Calc total shares within contract
        uint total_shares;
        for(uint16 i = 0; i < _shares.length; i++) {
            total_shares += _shares[i];
        }

        // Not allowed more than one trillion shares total
        require(total_shares <= 1000000000000, 'Error: 1005');

        // Create new Squid contract
        squids[id] = new Squid(
            _name,
            _description,
            _payees,
            _shares,
            total_shares,
            transaction_fee,
            msg.sender,
            address(this)
        );

        // Log squid creator and payees
        for(uint16 i = 0; i < _payees.length; i++) {
            asoc_squids[_payees[i]].push(id);
        }

        created_squids[msg.sender].push(id);

    }

    /**
    * @notice Retrieve all data (except shareholders) about a specific Squid.
    * @param _id UUID used to identify which squid to access.
    * @param _account Address of payee.
    * @return result An array of strings which contains the collated data.
    */
    function getShareData(uint _id, address _account) public view returns (string[] memory) {
        string[] memory result;
        if(!exists(_id)) { return result; }
        return squids[_id].getData(_account);
    }

    /**
    * @notice Allows the retrieval of all UUIDs associated with an address
    * where the address is a payee of a squid.
    * @param _account Address of account to retrieve UUIDs for.
    * @return UUIDs An array of UUIDs.
    */
    function getIds(address _account) public view returns (uint[] memory) {
        return asoc_squids[_account];
    }

    /**
    * @notice Allows the retrieval of all UUIDs associated with an address
    * where the address is a creator of a Squid.
    * @param _account Address of account to retrieve UUIDs for.
    * @return UUIDs An array of UUIDs.
    */
    function getCreatedIds(address _account) public view returns (uint[] memory) {
        return created_squids[_account];
    }

    /**
    * @notice Retrieves all shareholders and their number of assigned shares.
    * @param _id UUID of a Squid.
    * @return shareholders An array containing each address and associated shares
    * founnd within a Squid. 
    */
    function getShareholders(uint _id) public view returns (string[] memory) {
        return squids[_id].getShareholders();
    }

    /**
    * @notice The owner of the contract factory can set a new owner.
    */
    function changeOwner(address _account) onlyOwner public {
        owner = payable(_account);
    }

    /**
    * @notice The owner of the contract factory can set a new manager.
    */
    function changeManager(address _account) onlyOwner public {
        manager = _account;
    }

    /**
    * @notice Allows only the contract owner to change the creation fee
    * as long as the new fee is not less than 0.
    * @param _fee The new fee value which will be used henceforth.
    */
    function changeCreationFee(uint _fee) onlyOwner public {
        require(_fee >= 0, 'Error: 1006');
        creation_fee = _fee;
    }

    /**
    * @notice Allows only the contract owner to change the transaction fee
    * as long as the new fee is not less than 0. When a fee is changed it will
    * only apply to new squids that are created.
    * @param _fee The new fee value which will be used henceforth.
    */
    function changeTransactionFee(uint _fee) onlyOwner public {
        require(_fee >= 0, 'Error: 1006');
        transaction_fee = _fee;
    }

    /**
    * @notice The owner of the contract can release accumulated fees to their wallet.
    * @param _token Smart contract address for token when releasing ERC-20 Tokens.
    * @param _eth Boolean where true is to handle eth, and false is to
    * handle other tokens.
    */
    function releaseFunds(IERC20 _token, bool _eth) onlyOwner public {
        if(!_eth) {
            _token.transfer(owner, _token.balanceOf(address(this)));
        } else {
            owner.transfer(address(this).balance);
        }
    }

    /**
    * @notice Owner and assigned manager can enter an array of ids to initiate global
    * payouts. If an ID does not exist it will be skipped.
    * @param _ids Array of UUIDS
    * @param _token Smart contract address for token when releasing ERC-20 Tokens.
    * @param _eth Boolean where true is to handle eth, and false is to
    * handle other tokens.
    */
    function flush(uint[] memory _ids, IERC20 _token, bool _eth) onlyManagement public {
        for(uint i = 0; i < _ids.length; i++) {
            if(!exists(_ids[i])) { continue; }
            if(!_eth) {
                squids[_ids[i]].payoutAllTokenized(_token);
            } else {
                squids[_ids[i]].payoutAll();
            }
        }
    }

    /**
    * @notice Locks a squid, meaning it can no longer recieve or release funds.
    * @param _id UUID of a squid.
    * @param _state True to lock, False to unlock.
    */
    function lock(uint _id, bool _state) onlyOwner public {
        squids[_id].lock(_state);
    }