contract MetadataOracle is ICoreMetadataOracleReader, Ownable {
    using ABDKMath64x64 for int128;

    // Signed 64.64 fixed point number 1.002 gives us minimal distinguishable price change of 0.2%.
    int128 public constant DELTA_BASE = int128(uint128(1002 * 2 ** 64) / uint128(1000));

    // min delta is DELTA_BASE ** -512, max delta is DELTA_BASE ** 511
    uint256 public constant DELTA_BITS = 10;

    // keccak256("MetadataOracle.deltas")
    uint256 private constant DELTAS_LOCATION = 0x13564db6094c800c2f97ce40db2ed0766673b9ca1c1adc61480074f2544395d8;

    uint256 private constant NO_DELTA = 0;
    uint256 private constant DELTA_MODULO = 1 << DELTA_BITS;        // module for two's complement arithmetic
    uint256 private constant DELTA_MASK = DELTA_MODULO - 1;         // mask to extract a delta
    uint256 private constant DELTAS_PER_SLOT = 256 / DELTA_BITS;    // note that there may be unused bits in a slot
    uint256 private constant SLOT_PADDING_BITS = 256 - DELTAS_PER_SLOT * DELTA_BITS;    // unused bits in a slot

    int128 private constant ABDK_ONE = int128(int256(1 << 64));


    struct Status {
        // TODO replace with per-asset update timestamps
        // and
        // externally set epoch ts for replay attack protection: current epoch < new epoch <= now.
        uint32 updateTS;    // last unix timestamp of ANY update

        // pricesHash was used instead of nonces since the same nonce may be associated with different
        // base prices arrays in different chain forks. That is unacceptable for the delta application.
        // So we had to check the entire prices content for equality.
        // TODO replace with current epoch as a collision preventer.
        // Now this is acceptable since prices for an epoch are deterministic.
        uint64 pricesHash;  // for updater only!
    }

    Status internal status;


    address[] internal assets;
    mapping(address => uint) internal assetId;

    uint[] internal prices;


    /// @inheritdoc ICoreMetadataOracleReader
    function getAssets() external view override returns (address[] memory) {
        return assets;
    }

    function getStatus() external view returns (Status memory) {
        return status;
    }

    // prices WITHOUT deltas applied! for updater only!
    function getBasePrices() external view returns (uint[] memory) {
        return prices;
    }

    // for updater only!
    function getCurrentDeltas() external view returns (uint[] memory result) {
        result = new uint[](assets.length);
        // TODO optimize excess sload-s
        for (uint i = 0; i < result.length; i++)
            result[i] = getDelta(i);
    }

    /// @inheritdoc ICoreMetadataOracleReader
    function hasAsset(address asset) public view override returns (bool) {
        return assetId[asset] != 0 || (assets.length != 0 && assets[0] == asset);
    }


    /// @inheritdoc ICoreMetadataOracleReader
    function quoteAssets(address[] calldata assets_) external view override returns (Quote[] memory quotes) {
        uint32 updateTS = status.updateTS;
        uint256 length = assets_.length;
        quotes = new Quote[](length);
        for (uint i = 0; i < length; i++) {
            address asset = assets_[i];
            uint256 id = assetId[asset];

            require(id != 0 || (assets.length != 0 && assets[0] == asset), "MetadataOracle: ASSET_NOT_FOUND");

            // TODO optimize excess sload-s
            quotes[i] = Quote(getPrice(id), updateTS);
        }
    }


    function addAsset(address asset, uint256 currentPrice) external onlyOwner {
        require(!hasAsset(asset), "MetadataOracle: ASSET_EXISTS");

        assetId[asset] = assets.length;
        assets.push(asset);
        prices.push(currentPrice);
        setDelta(assetId[asset], NO_DELTA);

        updated();
        addToPricesHash(asset, currentPrice);
    }

    function addAssets(address[] calldata assets_, uint256[] calldata prices_) external onlyOwner {
        require(assets_.length != 0 && assets_.length == prices_.length, "MetadataOracle: BAD_LENGTH");

        uint256 length = assets_.length;
        uint64 pricesHash = status.pricesHash;
        uint256 id = assets.length;
        for (uint256 i = 0; i < length; i++) {
            address asset = assets_[i];
            uint256 currentPrice = prices_[i];
            require(!hasAsset(asset), "MetadataOracle: ASSET_EXISTS");

            assetId[asset] = id;
            assets.push(asset);
            prices.push(currentPrice);
            setDelta(id, NO_DELTA);
            id++;

            pricesHash = uint64(uint256(keccak256(abi.encodePacked(pricesHash, asset, currentPrice))));
        }

        status.pricesHash = pricesHash;
        updated();
    }

    function setPrice(address asset, uint256 price) external onlyOwner {
        require(hasAsset(asset), "MetadataOracle: ASSET_NOT_FOUND");

        uint256 id = assetId[asset];
        prices[id] = price;
        setDelta(id, NO_DELTA);

        updated();
        addToPricesHash(asset, price);
    }

    function setPrices(address[] calldata assets_, uint256[] calldata prices_) external onlyOwner {
        require(assets_.length != 0 && assets_.length == prices_.length, "MetadataOracle: BAD_LENGTH");

        uint256 length = assets_.length;
        uint64 pricesHash = status.pricesHash;
        for (uint256 i = 0; i < length; i++) {
            address asset = assets_[i];
            uint256 price = prices_[i];
            require(hasAsset(asset), "MetadataOracle: ASSET_NOT_FOUND");

            uint256 id = assetId[asset];
            prices[id] = price;
            setDelta(id, NO_DELTA);

            pricesHash = uint64(uint256(keccak256(abi.encodePacked(pricesHash, asset, price))));
        }

        status.pricesHash = pricesHash;
        updated();
    }

    // deltas := [slot], [slot ...]
    // slot := delta, [delta ...], zero padding up to 256 bits
    // delta := signed DELTA_BITS-bit number, to be used as an exponent of DELTA_BASE
    function updateDeltas(bytes calldata deltas, uint64 pricesHashCheck) external onlyOwner {
        uint256 assetsNum = assets.length;
        if (0 == assetsNum) {
            require(0 == deltas.length, "MetadataOracle: WRONG_LENGTH");
            return;
        }
        uint256 slots = (assetsNum - 1) / DELTAS_PER_SLOT + 1;
        require(deltas.length == 32 * slots, "MetadataOracle: WRONG_LENGTH");

        uint256 currentPricesHash = status.pricesHash;
        require(pricesHashCheck == currentPricesHash, "MetadataOracle: STALE_UPDATE");
        status.pricesHash = uint64(uint256(keccak256(abi.encodePacked(currentPricesHash, deltas))));
        updated();

        // calldata pointer: selector + deltas position + uint(pricesHashCheck) + length word
        uint256 srcOffset = 4 + 32 + 32 + 32;
        uint256 dstSlot = DELTAS_LOCATION;  // storage pointer
        for (uint256 i = 0; i < slots; i++) {
            assembly {
                sstore(dstSlot, calldataload(srcOffset))
            }
            srcOffset += 32;
            dstSlot++;
        }
    }


    /// @dev Gets raw asset delta (signed logarithm encoded as two's complement)
    function getDelta(uint256 id) internal view returns (uint256) {
        uint256 slot = DELTAS_LOCATION + id / DELTAS_PER_SLOT;
        uint256 deltaBlock;
        assembly {
            deltaBlock := sload(slot)
        }

        // Unpack one delta from the slot contents
        return (deltaBlock >> getBitsAfterDelta(id)) & DELTA_MASK;
    }

    /// @dev Sets raw asset delta (signed logarithm encoded as two's complement)
    function setDelta(uint256 id, uint256 delta) internal {
        uint256 dstSlot = DELTAS_LOCATION + id / DELTAS_PER_SLOT;
        uint256 current;
        assembly {
            current := sload(dstSlot)
        }

        // Clear the delta & overwrite it with new content keeping others intact
        uint256 bitsAfterDelta = getBitsAfterDelta(id);
        current &= ~(DELTA_MASK << bitsAfterDelta);     // setting zeroes
        current |= delta << bitsAfterDelta;       // writing delta

        assembly {
            sstore(dstSlot, current)
        }
    }

    function getBitsAfterDelta(uint256 id) internal pure returns (uint256) {
        uint256 deltaIdx = id % DELTAS_PER_SLOT;
        return (DELTAS_PER_SLOT - 1 - deltaIdx) * DELTA_BITS + SLOT_PADDING_BITS;
    }

    function getPrice(uint256 id) internal view returns (uint256) {
        uint256 rawDelta = getDelta(id);
        int128 delta;
        if (0 == rawDelta & (1 << (DELTA_BITS - 1))) {
            // Non-negative power
            delta = DELTA_BASE.pow(rawDelta);
        }
        else {
            // Negative power, converting from two's complement
            delta = ABDK_ONE.div(DELTA_BASE.pow(DELTA_MODULO - rawDelta));
        }

        uint256 basePrice = prices[id];
        return delta.mulu(basePrice);
    }

    function addToPricesHash(address asset, uint256 currentPrice) internal {
        status.pricesHash = uint64(uint256(keccak256(abi.encodePacked(status.pricesHash, asset, currentPrice))));
    }

    function updated() internal virtual {
        uint256 ts = block.timestamp;
        assert(ts <= type(uint32).max);
        status.updateTS = uint32(ts);
    }
}
/**
 * @notice Write-efficient oracle
 */
function updateDeltas(bytes calldata deltas, uint64 pricesHashCheck) external onlyOwner {
    uint256 assetsNum = assets.length;
    if (0 == assetsNum) {
        require(0 == deltas.length, "MetadataOracle: WRONG_LENGTH");
        return;
    }
    uint256 slots = (assetsNum - 1) / DELTAS_PER_SLOT + 1;
    require(deltas.length == 32 * slots, "MetadataOracle: WRONG_LENGTH");

    uint256 currentPricesHash = status.pricesHash;
    require(pricesHashCheck == currentPricesHash, "MetadataOracle: STALE_UPDATE");
    status.pricesHash = uint64(uint256(keccak256(abi.encodePacked(currentPricesHash, deltas))));
    updated();

    // calldata pointer: selector + deltas position + uint(pricesHashCheck) + length word
    uint256 srcOffset = 4 + 32 + 32 + 32;
    uint256 dstSlot = DELTAS_LOCATION;  // storage pointer
    for (uint256 i = 0; i < slots; i++) {
        assembly {
            sstore(dstSlot, calldataload(srcOffset))
        }
        srcOffset += 32;
        dstSlot++;
    }
}

// deltas := [slot], [slot ...]
// slot := delta, [delta ...], zero padding up to 256 bits
// delta := signed DELTA_BITS-bit number, to be used as an exponent of DELTA_BASE