// Contract: TimeCapsule
// Target function: function createBehaviorLock(

        bytes32 tag;                // <= 31 character descriptor string
        uint64 lockTime;            // UTC
        uint64 unlockTime;          // UTC
        uint64 releaseTime;         // UTC timestamp when time-locked funds release for sending
        LockType lockType;          // uint8
        bytes6 unlockHash;          // first 6 bytes of keccak256 hash
        // 256 bit boundary
        uint256 lockedAmount;       // in ERC20 token's base units
        uint256 withdrawnToDate;    // in ERC20 token's base units
        uint256 timelockedAmount;   // amount of any pending, time-locked withdrawal
    }
}

/**
 * @title TimeCapsule
 */
contract TimeCapsule is TimeCapsuleContext{

    address public owner;
    ITimeCapsuleFactory private _capsuleFactory;

    mapping(address => Lock[])  private _locks; // address is 'rc20 token contract address or address(0)
    mapping(address => uint256) private _totalLockedAmount;

    uint64 private _deadmanTime;

    VaultStatus private _vaultStatus;

    IFeeSplitter private _feeSplitter;

    constructor() {
        // Contructor not used by factory. We use it here only to prevent master copy hijacks
        // the factory will call this.initialize() for one-off setup tasks.
        _disableInitializers();
    }

    /**
     * @dev Because this contract is a proxied clone (oppenzep'Clone.sol)
     *      functions herein get called **twice**. The first call has `msg.sender`
     *      set as the address of the caller, as usual. The second _proxy_ call
     *      has `msg.sender` set to this contract's own address. Hence, we roll
     *      our own 'Ownable' (minus transferability) and with a different name
     *      than 'onlyOwner' to avoid confusion.
     */
    modifier _onlyCapsuleOwner() {
        if (address(this) != msg.sender) {
            require(owner == msg.sender, "Not owner");
            _;
        }
    }

    /**
     * @dev This modifier blocks calls to functions when the vault is in the 'panic' state.
     */
    modifier _notPanicState() {
        if (address(this) != msg.sender) {
            require(_vaultStatus != VaultStatus.PANIC, "Forbidden");
            _;
        }
    }

    /**
     * Called once by factory directly after cloning to initialize vault.
     * @param _newOwner the vaults's owner address
     * @param _factoryAddress used to manage recovery from panic state aka "undo a hack"
     * @param _feeSplitterContract the [contract] address to send fees
     */
    function initialize(
        address _newOwner,
        address _factoryAddress,
        IFeeSplitter _feeSplitterContract
    )
        initializer
        public
    {
        owner = _newOwner;
        _capsuleFactory = ITimeCapsuleFactory(_factoryAddress);
        _deadmanTime = (uint64)(block.timestamp);
        _vaultStatus = VaultStatus.NOMINAL;
        _feeSplitter = _feeSplitterContract;
    }

    /**
     * When native coin is sent directly to this vault (as if it were a standard wallet)
     * the funds are auto-locked for seven days (and the fee is paid accordingly.) This is
     * intended as a safety feature and not to be advertized as good practice.
     */
    receive() external payable { }

    event Locked(
        bytes32 tag,
        address tokenAddress,
        uint256 lockIndex,
        uint256 amount,
        uint256 fee,
        uint256 lockTime,
        uint256 unlockTime,
        LockType lockType
    );

    event WithdrawalInitiated(
        address tokenAddress,
        uint256 lockIndex,
        uint256 amount
    );

    event WithdrawalCancelled(
        address tokenAddress,
        uint256 lockIndex
    );

    event Withdrawal(
        address tokenAddress,
        uint256 lockIndex,
        uint256 amount
    );

    event LockReleased(
        address tokenAddress,
        uint256 lockIndex
    );

    event RecoveryInitiated();

    event Recovered();

    error HardLocked(
        uint64 unlockTime
    );

    error InsufficientFunds(
        uint256 grossBalance,
        uint256 alreadyLocked,
        uint256 availableToLock
    );

    /**
     * Transfers nativer coin from the vault to a recipient.
     * @notice Do not use simple trasfer(<maxgas: 2300!>) — Gnosis safe (for example) infamously incompatible
     * @param _to recipient address
     * @param _value base unit amount to transfer
     */
    function _transferNative(
        address _to,
        uint256 _value
    )
        internal
        returns (bool success)
    {
        (success, ) = _to.call{ value: _value }("");
    }

    /**
     * Transfers 'RC20 tokens from the vault to a recipient.
     * @notice internal function
     * @param _tokenAddress 'RC20 token contract address
     * @param _to recipient address
     * @param _value base unit amount to transfer
     * @return bool
     */
    function _transferToken(
        address _tokenAddress,
        address _to,
        uint256 _value
    )
        internal
        returns (bool)
    {
        return IERC20(_tokenAddress).transfer(
            _to,
            _value
        );
    }

    /**
     * Returns the current "state" of the vault. See `enum VaultStatus {...}`
     * @return status enum VaultStatus.<NOMINAL|PANIC|RECOVERED>
     */
    function vaultStatus()
        public
        view
        returns (VaultStatus status)
    {
        status = _vaultStatus;
    }

    /**
     * Validates recovery address. Validation is required as a separate step to account for possibily
     * of mistake or bad actor using the same recovery address as an attempt to 'block' geniune use.
     * @param _recoveryAddressHash keccak256 (sha3) hash of recovery address (so as not to 'dox' the address)
     * @param _signature personal_sign of the string: "CONFIRMING RECOVERY ADDRESS"
     */
    function validateRecoveryAddressHash(
        bytes32 _recoveryAddressHash,
        bytes memory _signature
    )
        public
        _onlyCapsuleOwner
        _notPanicState
    {
        _capsuleFactory.validateRecoveryAddressHash(
            owner,
            _recoveryAddressHash,
            _signature
        );
    }

    /**
     * Used to test if a given recovery address hash is valid for (matches) the hash stored for this vault.
     * @param _recoveryAddressHash keccak256 hash of the recovery address / public key
     * @return confirmed bool
     */
    function checkRecoveryAddress(bytes32 _recoveryAddressHash)
        public
        view
        returns (bool confirmed)
    {
        confirmed = _capsuleFactory.checkRecoveryAddress(owner, _recoveryAddressHash);
    }

    /**
     * Returns true if tha vault's recovery address hash has been validated — see validateRecoveryAddressHash(...).
     * @return validated bool
     */
    function isRecoveryHashValidated()
        public view
        returns (bool validated)
    {
        return _capsuleFactory.isRecoveryHashValidated(owner);
    }

    /**
     * Returns the number of active locks for a given token contract address (or address(0) for
     * native coin.) Used in conjunction with lockData() to retrieve list of locks for a spcific token.
     * @param _tokenAddress 'RC2- contract address
     * @return count number of active locks for the specified token
     */
    function lockCount(address _tokenAddress)
        public
        view
        returns (uint256 count)
    {
        count = _locks[_tokenAddress].length;
    }

    /**
     * Returns Lock record for an existing lock.
     * @param _tokenAddress 'RC20 token contract address
     * @param _index lock index
     * @return lock Lock record
     */
    function lockData(
        address _tokenAddress,
        uint256 _index
    )
        public
        view
        returns (Lock memory lock)
    {
        Lock[] memory locks = _locks[_tokenAddress];
        require(locks.length > 0,
            "No locks for given token address"
        );
        require(locks.length > _index,
            "Invalid lock index"
        );

        lock = locks[_index];
    }

    /**
     * Returns available/lockable balance (owned by this vault and not tied up in locks)
     * @param _tokenAddress token contract address or address(0) for native coin
     */
    function availableBalance(
        address _tokenAddress   // use address(0) for native coin
    )
        public
        view
        returns (uint256 available)
    {
        available = 0;
        uint256 _grossBalance;
        if (_tokenAddress == NATIVE_COIN) {
            _grossBalance = address(this).balance;
        } else {
            require(
                _tokenAddress.code.length > 0,
                "not a token"
            );
            _grossBalance = IERC20(_tokenAddress).balanceOf(address(this));
        }
        if (_grossBalance > _totalLockedAmount[_tokenAddress]) {
            available = _grossBalance - _totalLockedAmount[_tokenAddress];
        }
    }

    /**
     * @notice internal function
     * @param _tag bytes32String: up to 31 characters of user input as a label for the lock
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockAmount unit value of amount to be locked
     * @param _lockTime UTC unix timestamp (seconds)
     * @param _unlockTime UTC unix timestamp (seconds)
     * @param _lockType enum LockType.<DRIP|HARD|BEHAVIORAL>
     * @return lockIndex array index of created lock
     */
    function _storeNewLock(
        bytes32 _tag,
        address _tokenAddress,
        uint256 _lockAmount,
        uint64 _lockTime,
        uint64 _unlockTime,
        LockType _lockType
    )
        internal
        returns (uint256 lockIndex)
    {
        Lock[] storage locksRef = _locks[_tokenAddress];
        Lock memory lock = Lock({
            state: LockState.INERT,
            tag: _tag,
            lockTime: _lockTime,
            unlockTime: _unlockTime,
            lockType: _lockType,
            unlockHash: 0,
            lockedAmount: _lockAmount,
            withdrawnToDate: 0,
            timelockedAmount: 0,
            releaseTime: 0
        });
        locksRef.push(lock);
        lockIndex = locksRef.length - 1;
    }

    /**
     * @notice internal function
     * @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockAmount unit value of amount to be locked
     * @param _lockTime UTC unix timestamp (seconds)
     * @param _unlockTime UTC unix timestamp (seconds)
     * @param _lockType LockType.<DRIP|HARD|BEHAVIORAL>
     * @return lockIndex array index of created lock
     */
    function _createLock(
        bytes32 _tag,
        address _tokenAddress,
        uint256 _lockAmount,
        uint256 _fee,
        uint64 _lockTime,   // UTC unix timestamp (seconds)
        uint64 _unlockTime, // UTC unix timestamp (seconds)
        LockType _lockType
    )
        internal
        returns (uint256 lockIndex)
    {
        require(_lockTime >= (uint64)(block.timestamp) - (6 * ONE_HOUR),
            "Lock time must not be more than six hours in the past"
        );
        require(_unlockTime > _lockTime,
            "Unlock time must be greater than lock time"
        );
        require((_unlockTime - _lockTime) < MAX_LOCK_SECONDS, // built-in "safe math" solidity@>0.8.0
            "Unlock time must be within 55 years of lock time"
        );

        unchecked { // division underflow acceptable (decimal truncation)
            uint256 _feeCheck = _lockAmount / FEE_DIVISOR;
            require(_feeCheck == _fee,
                "Fee calc audit failed"
            );
        }

        if (_tokenAddress == NATIVE_COIN) { // native coin

            uint256 _availableToLock = availableBalance(_tokenAddress); // any msg.value is already included in balance

            if (_availableToLock < (_lockAmount + _fee)) {
                revert InsufficientFunds(
                    address(this).balance - msg.value, // grossBalance (before msg.value, being 'refunded' by this revert)
                    _totalLockedAmount[_tokenAddress], // alreadyLocked
                    _availableToLock                   // availableToLock (includes any incoming msg.value (now being reverted))
                );
            }

            if (_fee > 0) {
                (bool feeTransferred, ) = address(_feeSplitter).call{value: _fee}("");
                require(
                    feeTransferred,
                    "Fee transfer rejected by external contract"
                );
            }

        } else { // 'RC20

            IERC20 tokenContract = IERC20(_tokenAddress);

            uint256 _availableToLock = availableBalance(_tokenAddress);

            if (_availableToLock < (_lockAmount + _fee)) {
                uint256 _tokenBalance = IERC20(tokenContract).balanceOf(address(this));
                revert InsufficientFunds(
                    _tokenBalance,                      // grossBalance
                    _totalLockedAmount[_tokenAddress],  // alreadyLocked
                    _availableToLock                    // availableToLock)
                );
            }

            if (_fee > 0) {
                bool feeTransferred = IERC20(tokenContract).transfer(
                    address(_feeSplitter),
                    _fee
                );
                require(
                    feeTransferred,
                    "Fee transfer rejected by external contract"
                );
                require(
                    // @dev splitERC20 splits and sends on the _feeSplitter's entire current balance
                    _feeSplitter.splitERC20(address(tokenContract)),
                    "Fee split callout failed"
                );
            }
        }

        lockIndex = _storeNewLock(
            _tag,
            _tokenAddress,
            _lockAmount,
            _lockTime,
            _unlockTime,
            _lockType
        );

        _totalLockedAmount[_tokenAddress] += _lockAmount;

        // update 'watchdog' deadman timer
        if (_deadmanTime < _unlockTime) _deadmanTime = _unlockTime;

        emit Locked(
            _tag,
            _tokenAddress,
            lockIndex,
            _lockAmount,
            _fee,
            _lockTime,
            _unlockTime,
            _lockType
        );
    }

    /**
     * Creates a new standard (LockType.DRIP) time lock.
     * @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockAmount unit value of amount to be locked
     * @param _lockTime UTC unix timestamp (seconds)
     * @param _unlockTime UTC unix timestamp (seconds)
     * @return lockIndex array index of created lock
     */
    function createLock(
        bytes32 _tag,
        address _tokenAddress,
        uint256 _lockAmount,
        uint256 _fee,
        uint64 _lockTime,
        uint64 _unlockTime
    )
        public
        payable
        _onlyCapsuleOwner
        _notPanicState
        returns (uint256 lockIndex)
    {
        lockIndex = _createLock(
            _tag,
            _tokenAddress,
            _lockAmount,
            _fee,
            _lockTime,
            _unlockTime,
            LockType.DRIP
        );
    }

    /**
     * Creates a new hard lock (LockType.HARD) — a lock that does not drip and must complete full term before withdrawal.
     * @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockAmount unit value of amount to be locked
     * @param _lockTime UTC unix timestamp (seconds)
     * @param _unlockTime UTC unix timestamp (seconds)
     * @return lockIndex array index of created lock
     */
    function createHardLock(
        bytes32 _tag,
        address _tokenAddress,
        uint256 _lockAmount,
        uint256 _fee,
        uint64 _lockTime,
        uint64 _unlockTime
    )
        public
        payable
        _onlyCapsuleOwner
        _notPanicState
        returns (uint256 lockIndex)
    {
        lockIndex = _createLock(
            _tag,
            _tokenAddress,
            _lockAmount,
            _fee,
            _lockTime,
            _unlockTime,
            LockType.HARD
        );
    }

    /**
     * Creates a behavioral lock (LockType.BAHVIORAL) which lasts up to a maximum of ONE YEAR from _lockStart
     * or immediately released when the correct unlock code is passed to releaseBehaviorLock().
     * @notice UI implementations should limit the unlock code to a simple six digit number.
     * @param _tag bytes32String: up to 31 characters of user input as a label for the lock (may be "")
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockAmount unit value of amount to be locked
     * @param _lockTime UTC unix timestamp (seconds)
     * @param _unlockHash first six bytes of a keccak256 (sha3) of a 'secret' unlock text (ideally UI limited to simple 6 digits)
     * @return lockIndex array index of created lock
     */