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

import {IWeaver} from "../interfaces/IWeaver.sol";

import {Hashes} from "../libraries/Hashes.sol";
import {Checkpoints} from "../libraries/Checkpoints.sol";

abstract contract Weaver is IWeaver {
    using Checkpoints for Checkpoints.Trace256;
    using Checkpoints for Checkpoints.Trace160;

    uint256 public constant DEPTH = 8;
    uint256 public constant SLOTS = 2 ** DEPTH;
    uint256 public constant ENTRIES = DEPTH * SLOTS;

    uint256 public override currentWeavingSession;
    uint256 public override startWeavingSession;

    mapping(uint256 => mapping(uint256 => Checkpoints.Trace256)) internal _treeNodes;
    mapping(uint256 => mapping(uint256 => Checkpoints.Trace160)) internal _slotLenghts;
    mapping(uint256 => mapping(uint256 => bytes32[])) internal _slotValues;
    mapping(uint256 => uint256) internal _filledEntries;

    constructor(address _previousAddress) {
        if (_previousAddress != address(0)) {
            uint256 newSession = IWeaver(_previousAddress).currentWeavingSession() + 1;
            currentWeavingSession = newSession;
            startWeavingSession = newSession;
        }
    }

    function previousAddress() external virtual view returns (address);

    function getSlotValues(
        uint256 globalIndex,
        uint256 session,
        uint256 atBlock
    ) external override view returns (bytes32[] memory) {
        if (session < startWeavingSession) {
            return IWeaver(this.previousAddress()).getSlotValues(globalIndex, session, atBlock);
        }

        uint256 slotIndex = globalIndex % SLOTS;
        // forge-lint: disable-next-line(unsafe-typecast)
        uint256 length = _slotLenghts[session][slotIndex].upperLookupRecent(uint96(atBlock));
        bytes32[] memory values = new bytes32[](length);

        uint256 i;
        for (; i < length; ) {
            values[i] = _slotValues[session][slotIndex][i];
            unchecked { ++i; }
        }

        return values;
    }

    function getRoot(uint256 session, uint256 atBlock) public override view returns (bytes32, uint256) {
        if (session < startWeavingSession) {
            return IWeaver(this.previousAddress()).getRoot(session, atBlock);
        }

        uint256 currentLevelCount = SLOTS >> 1;
        bytes32[] memory currentLevel = new bytes32[](currentLevelCount);

        uint256 i;
        for (; i < currentLevelCount;) {
            uint256 j = i << 1;
            bytes32 left = bytes32(_treeNodes[session][j].upperLookup(atBlock));
            bytes32 right = bytes32(_treeNodes[session][j | 1].upperLookup(atBlock));

            bytes32 leftHash = Hashes.efficientKeccak256(left);
            bytes32 rightHash = Hashes.efficientKeccak256(right);

            currentLevel[i] = Hashes.efficientKeccak256(leftHash, rightHash);
            unchecked { ++i; }
        }

        while (currentLevelCount > 1) {
            currentLevelCount >>= 1;
            i = 0;
            for (; i < currentLevelCount;) {
                uint256 j = i << 1;
                bytes32 left = currentLevel[j];
                bytes32 right = currentLevel[j | 1];

                currentLevel[i] = Hashes.efficientKeccak256(left, right);
                unchecked { ++i; }
            }
        }

        if (session < currentWeavingSession) {
            unchecked { ++session; }
        }

        return (currentLevel[0], session);
    }

    function getProof(
        uint256 globalIndex,
        uint256 session,
        uint256 atBlock
    ) external override view returns (bytes32[] memory) {
        if (session < startWeavingSession) {
            return IWeaver(this.previousAddress()).getProof(globalIndex, session, atBlock);
        }

        uint256 currentLevelCount = SLOTS;
        uint256 currentIndex = globalIndex % SLOTS;

        bytes32[] memory proof = new bytes32[](DEPTH);
        bytes32[] memory currentLevel = new bytes32[](currentLevelCount);
        uint256 i;

        for (; i < currentLevelCount;) {
            bytes32 leaf = bytes32(_treeNodes[session][i].upperLookup(atBlock));
            currentLevel[i] = Hashes.efficientKeccak256(leaf);
            unchecked { ++i; }
        }

        uint256 proofIndex;
        while (currentLevelCount > 1) {
            currentLevelCount >>= 1;
            uint256 siblingIndex = currentIndex ^ 1;

            proof[proofIndex] = currentLevel[siblingIndex];
            i = 0;

            for (; i < currentLevelCount;) {
                uint256 j = i << 1;
                bytes32 left = currentLevel[j];
                bytes32 right = currentLevel[j | 1];

                currentLevel[i] = Hashes.efficientKeccak256(left, right);
                unchecked { ++i; }
            }

            currentIndex >>= 1;
            unchecked { ++proofIndex; }
        }

        return proof;
    }

    function _insertTreeNode(bytes32 who, uint256 amount) internal returns (uint256 globalIndex) {
        uint256 session = currentWeavingSession;
        globalIndex = _filledEntries[session];
        if (globalIndex >= ENTRIES) {
            unchecked { ++session; }
            currentWeavingSession = session;
            globalIndex = 0;
        }

        uint256 slotIndex = globalIndex % SLOTS;
        uint256 valueIndex = globalIndex / SLOTS;
        uint160 length = uint160(_slotValues[session][slotIndex].length + 1);

        bytes32 preimage = _computeArgumentsHash(valueIndex, amount, who);
        bytes32 currHash = Hashes.efficientKeccak256(preimage);
        bytes32 slotHash = bytes32(_treeNodes[session][slotIndex].latest());

        slotHash = Hashes.efficientKeccak256(slotHash, currHash);

        _treeNodes[session][slotIndex].push(block.number, uint256(slotHash));
        _slotLenghts[session][slotIndex].push(uint96(block.number), length);
        _slotValues[session][slotIndex].push(preimage);

        unchecked { ++_filledEntries[session]; }
    }

    function _computeArgumentsHash(uint256 i, uint256 a, bytes32 r) internal pure returns (bytes32) {
        return Hashes.efficientKeccak256(Hashes.efficientKeccak256(bytes32(a), r), bytes32(i));
    }
}
