// Contract: TimeCapsule
// Target function: function recoverVault(
) — 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
     */
    /// @dev we consider it unsafe to trust a miner's clock for lock/unlock times
    function createBehaviorLock(
        bytes32 _tag,
        address _tokenAddress,
        uint256 _lockAmount,
        uint256 _fee,
        uint64  _lockTime,
        bytes6 _unlockHash
    )
        public
        payable
        _onlyCapsuleOwner
        _notPanicState
        returns (uint256 lockIndex)
    {
        lockIndex = _createLock(
            _tag,
            _tokenAddress,
            _lockAmount,
            _fee,
            _lockTime,
            _lockTime + BEHAVIORLOCK_SECONDS,
            LockType.BEHAVIORAL
        );
        _locks[_tokenAddress][lockIndex].unlockHash = _unlockHash;
    }

    /**
     * Intended for a bahioral lock 'friend' to check that the code they have is valid for a given lock.
     * @notice Behavioral locks are only minimally secure *intentionally*. The user who created
     * the lock may 'cheat' or 'fail to achieve their goal'. Thus for example, potential use
     * of this function to brute force the inlock code, while discouraged, is an acceptable
     * trade-off. After all, it is the vault owner's money and only their own self worth at stake.
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockIndex lock index
     * @param _unlockCode clear text unlock code
     * @return correct bool
     */
    function checkBehaviorLockCode(
        address _tokenAddress,
        uint256 _lockIndex,
        string memory _unlockCode
    )
        public
        view
        returns (bool correct)
    {
        bytes32 hashedUnlockCode = keccak256(abi.encodePacked(_unlockCode));
        correct = bytes6(hashedUnlockCode) == _locks[_tokenAddress][_lockIndex].unlockHash;
    }

    /**
     * Releases funds from a behavior lock if; a) given correct code OR b) a year has passed since the lock was cretaed.
     * @notice Funds are return to the vault's lockable balance. Withdrawal from the vault requires a new lock be created
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockIndex lock index (for this token)
     * @param _unlockCode clear text 'secret' unlock code (cAse senstitve)
     */
    function claimBehaviorLock(
        address _tokenAddress,
        uint256 _lockIndex,
        string memory _unlockCode
    )
        public
        payable
        _onlyCapsuleOwner
        _notPanicState
    {
        if (_locks[_tokenAddress][_lockIndex].lockTime > uint64(block.timestamp) - BEHAVIORLOCK_SECONDS) {
            require(
                checkBehaviorLockCode(_tokenAddress, _lockIndex, _unlockCode) == true,
                "Invalid code"
            );
        }
        _totalLockedAmount[_tokenAddress] -= _locks[_tokenAddress][_lockIndex].lockedAmount;
        _locks[_tokenAddress][_lockIndex].unlockTime = uint64(block.timestamp);
        releaseLock(_tokenAddress, _lockIndex);
    }

    /**
     * Returns value of token or coin realeasable NOW — aka the current DRIP amount.
     * @notice The actual value released by initiateWithdrawal() may differ — slightly higher.
     * However, making this a public function allows users to get an idea of current drip
     * amounts.
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _index lock index
     * @return withdrawalAllowance currently available withdrawable balance (drip amount)
     */
    function calculateAllowance(
        address _tokenAddress,
        uint256 _index
    )
        public
        view
        returns (uint256 withdrawalAllowance)
    {
        Lock memory lock = lockData(_tokenAddress, _index);

        if (lock.withdrawnToDate >= lock.lockedAmount) {
            withdrawalAllowance = 0;
        } else if (block.timestamp < (lock.lockTime - ONE_HOUR) || block.timestamp >= lock.unlockTime) {
            if (lock.withdrawnToDate >= lock.lockedAmount) {
                withdrawalAllowance = 0;
            } else {
                withdrawalAllowance = lock.lockedAmount - lock.withdrawnToDate;
            }
        } else {
            uint256 lockTerm = lock.unlockTime - lock.lockTime;
            uint256 termServed = ((block.timestamp - lock.lockTime) * 10000 ) / lockTerm ; // 10000 => 100.00% of term

            uint256 totalPortionFromStart = lock.lockedAmount * termServed;
            unchecked { // division undeflow acceptable (discard remainer)
                totalPortionFromStart /= 10000;
            }

            if (lock.withdrawnToDate >= totalPortionFromStart) withdrawalAllowance = 0;
            else withdrawalAllowance = totalPortionFromStart - lock.withdrawnToDate;

            // Web UI uses this function to display currently available "drip" amounts. So, we
            // account for a pending withdrawal by not including that amount in the returned allowance.
            if (lock.timelockedAmount >= withdrawalAllowance) withdrawalAllowance = 0;
            else withdrawalAllowance -= lock.timelockedAmount;
        }
    }

    /**
     * Releases any remaining balance in a lock without need of withdrawal.
     * Reverts if unlock time not yet reached.
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockIndex lock index (for this token)
     * @custom:emits LockReleased(tokenAddress, lockIndex)
     */
    function releaseLock(
        address _tokenAddress,
        uint256 _lockIndex
    )
        internal
    {
        Lock[] storage locksRef = _locks[_tokenAddress];
        Lock storage lock = locksRef[_lockIndex];

        require (
            block.timestamp >= lock.unlockTime,
            "Before unlock time"
        );

        // gas efficient removal of array element
        uint256 lastIndex = locksRef.length - 1;
        if (_lockIndex != lastIndex) locksRef[_lockIndex] = locksRef[lastIndex];
        locksRef.pop();

        emit LockReleased(
            _tokenAddress,
            _lockIndex
        );
    }

    /**
     * Initiates a seven day withdrawal timelock, after which sendWithdrawal() can be
     * called to retrieve the funds.
     * @notice Only ONE withdrawl may be active at a time for each token lock. (See `cancelWithdrawal()`, bellow.)
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockIndex lock index (for this token)
     * @custom:emits WithdrawalInitiated(tokenAddress, lockIndex, withdrawalAllowance)
     */
    function initiateWithdrawal(
        address _tokenAddress,
        uint256 _lockIndex
    )
        public
        _onlyCapsuleOwner
        _notPanicState
        returns (uint256 amount)
    {
        Lock[] storage locksRef = _locks[_tokenAddress];

        require(locksRef.length > 0,
            "Lock not found"
        );
        require(locksRef.length > _lockIndex,
            "Invalid lock index"
        );
        require(locksRef[_lockIndex].state != LockState.WITHDRAWAL_PENDING,
            "Withdrawal already pending"
        );

        Lock storage lock = locksRef[_lockIndex];

        require(
            lock.lockType != LockType.BEHAVIORAL,
            "Behavioral lock"
        );

        if (lock.lockType == LockType.HARD && uint64(block.timestamp) < lock.unlockTime) {
            revert HardLocked({
                unlockTime: lock.unlockTime
            });
        }

        uint256 withdrawalAllowance = calculateAllowance(
            _tokenAddress,
            _lockIndex
        );
        require(withdrawalAllowance > 0,
            "None available"
        );

        lock.state = LockState.WITHDRAWAL_PENDING;
        lock.timelockedAmount = withdrawalAllowance;
        lock.releaseTime = uint64(block.timestamp) + TIMELOCK_SECONDS;

        emit WithdrawalInitiated(
            _tokenAddress,
            _lockIndex,
            withdrawalAllowance
        );

        amount = withdrawalAllowance;
    }

    /**
     * Cancels a pending withdrawal
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockIndex lock index (for this token)
     * @custom:emits WithdrawalCancelled(tokenAddress, lockIndex)
     */
    function cancelWithdrawal(
        address _tokenAddress,
        uint256 _lockIndex
    )
        public
        _onlyCapsuleOwner
    {
        Lock[] storage locks = _locks[_tokenAddress];
        require(locks.length > 0,
            "No matching lock found"
        );
        require(locks.length > _lockIndex,
            "Invalid lock index"
        );
        Lock storage lock = locks[_lockIndex];
        require(lock.state == LockState.WITHDRAWAL_PENDING,
            "No withdrawal pending"
        );
        lock.timelockedAmount = 0;
        lock.state = LockState.INERT;

        emit WithdrawalCancelled(
            _tokenAddress,
            _lockIndex
        );
    }

    /**
     * Sends a pending seven day timelocked withdrawal to the vault's owner (reverts if not yet time)
     * @param _tokenAddress token contract address or address(0) for native coin
     * @param _lockIndex lock index (for this token)
     * @custom:emits Withdrawal(tokenAddress, lockIndex, withdrawalAmount)
     */
    function sendWithdrawal(
        address _tokenAddress,
        uint256 _lockIndex
    )
        public
        _onlyCapsuleOwner
        _notPanicState
    {
        Lock[] storage locksRef = _locks[_tokenAddress];
        Lock storage lock = locksRef[_lockIndex];

        require(
            lock.state == LockState.WITHDRAWAL_PENDING,
            "No withdrawal pending"
        );
        require(
            lock.timelockedAmount > 0,
            "No withdrawal pending"
        );

        require(
            block.timestamp >= lock.releaseTime,
            "Timelocked"
        );

        /**
         * @dev !IMPORTANT!: Clear the balance owed BEFORE calling sender's
         * potential contract address to avoid ye olde DAO dance (reentrance hack)
         */
        uint256 withdrawalAmount = lock.timelockedAmount;
        lock.timelockedAmount = 0;

        lock.withdrawnToDate += withdrawalAmount;

        bool withdrawalTransferred = (_tokenAddress == NATIVE_COIN)
            ?  _transferNative( owner, withdrawalAmount )
            :  _transferToken( _tokenAddress, owner, withdrawalAmount )
        ;
        require (
            withdrawalTransferred,
            "Withdrawal transfer failed"
        );

        _totalLockedAmount[_tokenAddress] -= withdrawalAmount;

        // check for last (emptying) withdrawal
        if (lock.withdrawnToDate >= lock.lockedAmount) {
            releaseLock(_tokenAddress, _lockIndex);
        } else {
            lock.releaseTime = 0; // gas discount
            lock.state = LockState.INERT;
        }

        emit Withdrawal(
            _tokenAddress,
            _lockIndex,
            withdrawalAmount
        );
    }

    /**
     * Splits a 65 byte (130 nibble) 'raw' signature into R, S, V components
     * @param _signature 65 byte (130 nibble) 'raw' signature
     * @return r signature R component
     * @return s signature S component
     * @return v signature V component
     */
    function _splitSignature(bytes memory _signature) private pure returns (bytes32 r, bytes32 s, uint8 v) {
        assembly {
            r := mload(add(_signature, 32))
            s := mload(add(_signature, 64))
            v := byte(0, mload(add(_signature, 96)))
        }
    }

    /**
     * Recovers the address of the signer of a message hash
     * @param _messageHash keccak256 (sha3) hash of the message signed
     * @param _signature  65 byte (130 nibble) signature
     */
    function __recoverSignerAddress(
        bytes32 _messageHash,
        bytes memory _signature
    )
        private
        pure
        returns (address signerAddress)
    {
        (bytes32 r,bytes32 s, uint8 v) = _splitSignature(_signature);
        signerAddress = ecrecover(_messageHash, v, r, s);
    }

    /**
     * Recovers the address of the signer of a arbitrary length message
     * @param _message the signed message
     * @param _signature signature
     */
    function _recoverSignerAddress(
        string memory _message,
        bytes memory _signature
    )
        private
        pure
        returns (address signerAddress)
    {
        if (_signature.length != 65) return INVALID_SIGNER_ADDRESS;

        bytes32 _messageHash = keccak256(
            abi.encodePacked(
                "\x19Ethereum Signed Message:\n",
                Strings.toString(bytes(_message).length),
                bytes(_message)
            )
        );

        signerAddress = __recoverSignerAddress(_messageHash, _signature);
    }

    /**
     * @notice internal function
     */
    function _panic() internal {
        require(
            _vaultStatus == VaultStatus.NOMINAL,
            "Panic is once only. Good luck!"
        );
        require(
            _capsuleFactory.isRecoveryHashValidated(owner) == true,
            "No recovery address"
        );

        _vaultStatus = VaultStatus.PANIC;

        emit RecoveryInitiated();
    }

    /**
     * Places the vault into PANIC state. NON-REVERSIBLE — vault must be recovered to.
     * NON-REVERSIBLE — vault must be "recovered" to regain functional access — see recoverVault()
     * @notice Two versions — see also panic(bytes memory _signature)
     */
    function panic() public _onlyCapsuleOwner {
         _panic();
    }

    /**
     * Allows anyone with an off-chain signed message from the original owner to place
     * the vault into PANIC state. NON-REVERSIBLE — vault must be "recovered" to regain
     * functional access — see recoverVault()
     * @notice Two versions — see also panic()
     */
    function panic(
        bytes memory _signature
    )
        public
    {
        address signerAddress = _recoverSignerAddress(VAULT_RECOVERY_AUTHORIZATION, _signature);
        require(signerAddress == owner, "Invalid signature");
        _panic();
    }

    /**
     * Recovers a vault from PANIC state — see panic() & panic(bytes memory _signature. (Also known as "undo a hack".)
     * Recovery can only be executed by the recovery address, which will become the vault's effective new owner.
     * The PANIC / RECOVERY process can only be executed ONCE. Don't get hacked again!
     * @notice internal function
     * @param _originalOwner address of original owner
     */
    function _recoverVault(
        address _originalOwner
    )
        internal
    {
        address _newOwner = msg.sender; // set local owner variable to new owner

        // update the factory contract's recovery and owner address mappings
        // reverts if vault's recoveryAddressHash does not match keccak256(_newOwner)
        _capsuleFactory.recoverOwnership(
            _originalOwner,
            _newOwner
        );
        owner = _newOwner;

        _vaultStatus = VaultStatus.RECOVERED;
        emit Recovered();
    }

    /**
     * Recovers an EXPIRED vault — one that has not had any lock/unlock activity for
     * a full year after the latest (most future) lock end time ever created.
     * @notice Only the recovery account can do this.
     * @param _originalOwner original owner's address
     */
    function recoverExpiredVault(
        address _originalOwner
    )
        public
    {
        uint64 deadmanRecoveryTime = _deadmanTime + ONE_YEAR;  // latest ever unlockTime plus one year
        uint64 blockTime = (uint64)(block.timestamp);
        require(blockTime >= deadmanRecoveryTime, "Vault not expired");
         _recoverVault(_originalOwner);
    }