// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.0;

import "../libraries/external/UnsafeBytesLib.sol";
import "../libraries/external/UnsafeCalldataBytesLib.sol";
import "@pythnetwork/pyth-sdk-solidity/AbstractPyth.sol";
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";

import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
import "./PythGetters.sol";
import "./PythSetters.sol";
import "./PythInternalStructs.sol";

import "../libraries/MerkleTree.sol";

abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
    uint32 constant ACCUMULATOR_MAGIC = 0x504e4155; // Stands for PNAU (Pyth Network Accumulator Update)
    uint32 constant ACCUMULATOR_WORMHOLE_MAGIC = 0x41555756; // Stands for AUWV (Accumulator Update Wormhole Verficiation)
    uint8 constant MINIMUM_ALLOWED_MINOR_VERSION = 0;
    uint8 constant MAJOR_VERSION = 1;

    enum UpdateType {
        WormholeMerkle
    }

    enum MessageType {
        PriceFeed
    }

    // This method is also used by batch attestation but moved here
    // as the batch attestation will deprecate soon.
    function parseAndVerifyPythVM(
        bytes calldata encodedVm
    ) internal view returns (IWormhole.VM memory vm) {
        {
            bool valid;
            (vm, valid, ) = wormhole().parseAndVerifyVM(encodedVm);
            if (!valid) revert PythErrors.InvalidWormholeVaa();
        }

        if (!isValidDataSource(vm.emitterChainId, vm.emitterAddress))
            revert PythErrors.InvalidUpdateDataSource();
    }

    function extractUpdateTypeFromAccumulatorHeader(
        bytes calldata accumulatorUpdate
    ) internal pure returns (uint offset, UpdateType updateType) {
        unchecked {
            offset = 0;

            {
                uint32 magic = UnsafeCalldataBytesLib.toUint32(
                    accumulatorUpdate,
                    offset
                );
                offset += 4;

                if (magic != ACCUMULATOR_MAGIC)
                    revert PythErrors.InvalidUpdateData();

                uint8 majorVersion = UnsafeCalldataBytesLib.toUint8(
                    accumulatorUpdate,
                    offset
                );

                offset += 1;

                if (majorVersion != MAJOR_VERSION)
                    revert PythErrors.InvalidUpdateData();

                uint8 minorVersion = UnsafeCalldataBytesLib.toUint8(
                    accumulatorUpdate,
                    offset
                );

                offset += 1;

                // Minor versions are forward compatible, so we only check
                // that the minor version is not less than the minimum allowed
                if (minorVersion < MINIMUM_ALLOWED_MINOR_VERSION)
                    revert PythErrors.InvalidUpdateData();

                // This field ensure that we can add headers in the future
                // without breaking the contract (future compatibility)
                uint8 trailingHeaderSize = UnsafeCalldataBytesLib.toUint8(
                    accumulatorUpdate,
                    offset
                );
                offset += 1;

                // We use another offset for the trailing header and in the end add the
                // offset by trailingHeaderSize to skip the future headers.
                //
                // An example would be like this:
                // uint trailingHeaderOffset = offset
                // uint x = UnsafeBytesLib.ToUint8(accumulatorUpdate, trailingHeaderOffset)
                // trailingHeaderOffset += 1

                offset += trailingHeaderSize;
            }

            updateType = UpdateType(
                UnsafeCalldataBytesLib.toUint8(accumulatorUpdate, offset)
            );

            offset += 1;

            if (accumulatorUpdate.length < offset)
                revert PythErrors.InvalidUpdateData();
        }
    }

    function extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
        bytes calldata accumulatorUpdate,
        uint encodedOffset
    )
        internal
        view
        returns (
            uint offset,
            bytes20 digest,
            uint8 numUpdates,
            bytes calldata encoded
        )
    {
        unchecked {
            encoded = UnsafeCalldataBytesLib.slice(
                accumulatorUpdate,
                encodedOffset,
                accumulatorUpdate.length - encodedOffset
            );
            offset = 0;

            uint16 whProofSize = UnsafeCalldataBytesLib.toUint16(
                encoded,
                offset
            );
            offset += 2;

            {
                bytes memory encodedPayload;
                {
                    IWormhole.VM memory vm = parseAndVerifyPythVM(
                        UnsafeCalldataBytesLib.slice(
                            encoded,
                            offset,
                            whProofSize
                        )
                    );
                    offset += whProofSize;

                    // TODO: Do we need to emit an update for accumulator update? If so what should we emit?
                    // emit AccumulatorUpdate(vm.chainId, vm.sequence);
                    encodedPayload = vm.payload;
                }

                uint payloadOffset = 0;
                {
                    uint32 magic = UnsafeBytesLib.toUint32(
                        encodedPayload,
                        payloadOffset
                    );
                    payloadOffset += 4;

                    if (magic != ACCUMULATOR_WORMHOLE_MAGIC)
                        revert PythErrors.InvalidUpdateData();

                    UpdateType updateType = UpdateType(
                        UnsafeBytesLib.toUint8(encodedPayload, payloadOffset)
                    );
                    ++payloadOffset;

                    if (updateType != UpdateType.WormholeMerkle)
                        revert PythErrors.InvalidUpdateData();

                    // This field is not used
                    // uint64 slot = UnsafeBytesLib.toUint64(encodedPayload, payloadoffset);
                    payloadOffset += 8;

                    // This field is not used
                    // uint32 ringSize = UnsafeBytesLib.toUint32(encodedPayload, payloadoffset);
                    payloadOffset += 4;

                    digest = bytes20(
                        UnsafeBytesLib.toAddress(encodedPayload, payloadOffset)
                    );
                    payloadOffset += 20;

                    // We don't check equality to enable future compatibility.
                    if (payloadOffset > encodedPayload.length)
                        revert PythErrors.InvalidUpdateData();
                }
            }

            numUpdates = UnsafeCalldataBytesLib.toUint8(encoded, offset);
            offset += 1;
        }
    }

    function parseWormholeMerkleHeaderNumUpdates(
        bytes calldata wormholeMerkleUpdate,
        uint offset
    ) internal pure returns (uint8 numUpdates) {
        uint16 whProofSize = UnsafeCalldataBytesLib.toUint16(
            wormholeMerkleUpdate,
            offset
        );
        offset += 2;
        offset += whProofSize;
        numUpdates = UnsafeCalldataBytesLib.toUint8(
            wormholeMerkleUpdate,
            offset
        );
    }

    function extractPriceInfoFromMerkleProof(
        bytes20 digest,
        bytes calldata encoded,
        uint offset
    )
        internal
        pure
        returns (
            uint endOffset,
            PythInternalStructs.PriceInfo memory priceInfo,
            bytes32 priceId,
            uint64 prevPublishTime
        )
    {
        unchecked {
            bytes calldata encodedMessage;
            uint16 messageSize = UnsafeCalldataBytesLib.toUint16(
                encoded,
                offset
            );
            offset += 2;

            encodedMessage = UnsafeCalldataBytesLib.slice(
                encoded,
                offset,
                messageSize
            );
            offset += messageSize;

            bool valid;
            (valid, endOffset) = MerkleTree.isProofValid(
                encoded,
                offset,
                digest,
                encodedMessage
            );
            if (!valid) {
                revert PythErrors.InvalidUpdateData();
            }

            MessageType messageType = MessageType(
                UnsafeCalldataBytesLib.toUint8(encodedMessage, 0)
            );
            if (messageType == MessageType.PriceFeed) {
                (priceInfo, priceId, prevPublishTime) = parsePriceFeedMessage(
                    encodedMessage,
                    1
                );
            } else {
                revert PythErrors.InvalidUpdateData();
            }

            return (endOffset, priceInfo, priceId, prevPublishTime);
        }
    }

    function parsePriceFeedMessage(
        bytes calldata encodedPriceFeed,
        uint offset
    )
        private
        pure
        returns (
            PythInternalStructs.PriceInfo memory priceInfo,
            bytes32 priceId,
            uint64 prevPublishTime
        )
    {
        unchecked {
            priceId = UnsafeCalldataBytesLib.toBytes32(
                encodedPriceFeed,
                offset
            );
            offset += 32;

            priceInfo.price = int64(
                UnsafeCalldataBytesLib.toUint64(encodedPriceFeed, offset)
            );
            offset += 8;

            priceInfo.conf = UnsafeCalldataBytesLib.toUint64(
                encodedPriceFeed,
                offset
            );
            offset += 8;

            priceInfo.expo = int32(
                UnsafeCalldataBytesLib.toUint32(encodedPriceFeed, offset)
            );
            offset += 4;

            // Publish time is i64 in some environments due to the standard in that
            // environment. This would not cause any problem because since the signed
            // integer is represented in two's complement, the value would be the same
            // in both cases (for a million year at least)
            priceInfo.publishTime = UnsafeCalldataBytesLib.toUint64(
                encodedPriceFeed,
                offset
            );
            offset += 8;

            // We do not store this field because it is not used on the latest feed queries.
            prevPublishTime = UnsafeBytesLib.toUint64(encodedPriceFeed, offset);
            offset += 8;

            priceInfo.emaPrice = int64(
                UnsafeCalldataBytesLib.toUint64(encodedPriceFeed, offset)
            );
            offset += 8;

            priceInfo.emaConf = UnsafeCalldataBytesLib.toUint64(
                encodedPriceFeed,
                offset
            );
            offset += 8;

            if (offset > encodedPriceFeed.length)
                revert PythErrors.InvalidUpdateData();
        }
    }

    function updatePriceInfosFromAccumulatorUpdate(
        bytes calldata accumulatorUpdate
    ) internal returns (uint8 numUpdates) {
        (
            uint encodedOffset,
            UpdateType updateType
        ) = extractUpdateTypeFromAccumulatorHeader(accumulatorUpdate);

        if (updateType != UpdateType.WormholeMerkle) {
            revert PythErrors.InvalidUpdateData();
        }

        uint offset;
        bytes20 digest;
        bytes calldata encoded;
        (
            offset,
            digest,
            numUpdates,
            encoded
        ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedFromAccumulatorUpdate(
            accumulatorUpdate,
            encodedOffset
        );

        unchecked {
            for (uint i = 0; i < numUpdates; i++) {
                PythInternalStructs.PriceInfo memory priceInfo;
                bytes32 priceId;
                uint64 prevPublishTime;
                (
                    offset,
                    priceInfo,
                    priceId,
                    prevPublishTime
                ) = extractPriceInfoFromMerkleProof(digest, encoded, offset);
                updateLatestPriceIfNecessary(priceId, priceInfo);
            }
        }
        if (offset != encoded.length) revert PythErrors.InvalidUpdateData();
    }
}
