/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nifi.provenance.toc;

import org.apache.nifi.stream.io.StreamUtils;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * Standard implementation of TocReader.
 *
 * Expects .toc file to be in the following format;
 *
 * byte 0: version
 * byte 1: boolean: compressionFlag -> 0 = journal is NOT compressed, 1 = journal is compressed
 * byte 2-9: long: offset of block 0
 * byte 10-17: long: offset of block 1
 * ...
 * byte (N*8+2)-(N*8+9): long: offset of block N
 */
public class StandardTocReader implements TocReader {
    private final boolean compressed;
    private final long[] offsets;
    private final long[] firstEventIds;
    private final File file;

    public StandardTocReader(final File file) throws IOException {
        this.file = file;
        final long fileLength = file.length();
        if (fileLength < 2) {
            if (file.exists()) {
                throw new EOFException();
            } else {
                throw new FileNotFoundException();
            }
        }

        try (final FileInputStream fis = new FileInputStream(file)) {
            final byte[] buffer = new byte[(int) fileLength];
            StreamUtils.fillBuffer(fis, buffer);

            final int version = buffer[0];
            final int compressionFlag = buffer[1];

            if ( compressionFlag == 0 ) {
                compressed = false;
            } else if ( compressionFlag == 1 ) {
                compressed = true;
            } else {
                throw new IOException("Table of Contents file " + file + " appears to be corrupt: could not read 'compression flag' from header; "
                    + "expected value of 0 or 1 but got " + compressionFlag);
            }

            final int blockInfoBytes = switch (version) {
                case 1 -> 8;
                default -> 16;
            };

            final int numBlocks = (buffer.length - 2) / blockInfoBytes;
            offsets = new long[numBlocks];

            if ( version > 1 ) {
                firstEventIds = new long[numBlocks];
            } else {
                firstEventIds = new long[0];
            }

            int index = 2;
            for (int i = 0; i < numBlocks; i++) {
                offsets[i] = readLong(buffer, index);
                index += 8;

                if ( version > 1 ) {
                    firstEventIds[i] = readLong(buffer, index);
                    index += 8;
                }
            }
        }
    }

    private long readLong(final byte[] buffer, final int offset) {
        return ((long) buffer[offset] << 56) +
            ((long) (buffer[offset + 1] & 0xFF) << 48) +
            ((long) (buffer[offset + 2] & 0xFF) << 40) +
            ((long) (buffer[offset + 3] & 0xFF) << 32) +
            ((long) (buffer[offset + 4] & 0xFF) << 24) +
            ((long) (buffer[offset + 5] & 0xFF) << 16) +
            ((long) (buffer[offset + 6] & 0xFF) << 8) +
            (buffer[offset + 7] & 0xFF);
    }

    @Override
    public boolean isCompressed() {
        return compressed;
    }

    @Override
    public File getFile() {
        return file;
    }

    @Override
    public long getBlockOffset(final int blockIndex) {
        if ( blockIndex >= offsets.length ) {
            return -1L;
        }
        return offsets[blockIndex];
    }

    @Override
    public long getFirstEventIdForBlock(final int blockIndex) {
        if (blockIndex >= firstEventIds.length) {
            return -1L;
        }

        return firstEventIds[blockIndex];
    }

    @Override
    public long getLastBlockOffset() {
        if ( offsets.length == 0 ) {
            return 0L;
        }
        return offsets[offsets.length - 1];
    }

    @Override
    public void close() {
    }

    @Override
    public int getBlockIndex(final long blockOffset) {
        for (int i = 0; i < offsets.length; i++) {
            if ( offsets[i] > blockOffset ) {
                // if the offset is less than the offset of our first block,
                // just return 0 to indicate the first block. Otherwise,
                // return i-1 because i represents the first block whose offset is
                // greater than 'blockOffset'.
                return (i == 0) ? 0 : i - 1;
            }
        }

        // None of the blocks have an offset greater than the provided offset.
        // Therefore, if the event is present, it must be in the last block.
        return offsets.length - 1;
    }

    @Override
    public Integer getBlockIndexForEventId(final long eventId) {
        // if we don't have event ID's stored in the TOC (which happens for version 1 of the TOC),
        // or if the event ID is less than the first Event ID in this TOC, then the Event ID
        // is unknown -- return null.
        if ( firstEventIds.length == 0 || eventId < firstEventIds[0] ) {
            return null;
        }

        for (int i = 1; i < firstEventIds.length; i++) {
            if ( firstEventIds[i] > eventId ) {
                return i - 1;
            }
        }

        // None of the blocks start with an Event ID greater than the provided ID.
        // Therefore, if the event is present, it must be in the last block.
        return firstEventIds.length - 1;
    }

    @Override
    public String toString() {
        return "StandardTocReader[file=" + file + ", compressed=" + compressed + "]";
    }
}
