/*******************************************************************************
 * Copyright (c) 2017 École Polytechnique de Montréal
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.internal.datastore.core.historytree;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.datastore.core.interval.HTInterval;
import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.HTNode;
import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.HistoryTreeStub;
import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.HtTestUtils;
import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.IHTNode.NodeType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Test the {@link HtIo} class
 *
 * @author Geneviève Bastien
 */
public class HtIoTest {

    private static final int BLOCKSIZE = HtTestUtils.BLOCKSIZE;
    private static final int NB_CHILDREN = 3;

    private @Nullable HtIo<HTInterval, HTNode<HTInterval>> fHtIo;
    private @Nullable File fStateFile;

    /**
     * Construct the tree IO Object
     *
     * @throws IOException
     *             Exception with the file
     */
    @Before
    public void setUp() throws IOException {
        File file = File.createTempFile("tmp", null);
        assertNotNull(file);
        fStateFile = file;

        fHtIo = new HtIo<>(file,
                HtTestUtils.BLOCKSIZE,
                NB_CHILDREN,
                true,
                HtTestUtils.READ_FACTORY,
                HistoryTreeStub.NODE_FACTORY);
    }

    /**
     * Delete the file after test
     */
    @After
    public void cleanUp() {
        HtIo<HTInterval, HTNode<HTInterval>> htIo = fHtIo;
        if (htIo != null) {
            htIo.deleteFile();
        }
    }

    private static HTNode<HTInterval> createCoreNode(int seqNum, int parentNum) {
        HTNode<HTInterval> node = HistoryTreeStub.NODE_FACTORY.createNode(NodeType.CORE,
                BLOCKSIZE, NB_CHILDREN, seqNum, parentNum, 0L);
        return node;
    }

    private static HTNode<HTInterval> createLeafNode(int seqNum, int parentNum) {
        HTNode<HTInterval> node = HistoryTreeStub.NODE_FACTORY.createNode(NodeType.LEAF,
                BLOCKSIZE, NB_CHILDREN, seqNum, parentNum, 0L);
        return node;
    }

    /**
     * Test reading and writing nodes
     *
     * @throws IOException
     *             Exception thrown by the file
     */
    @Test
    public void testReadWriteNode() throws IOException {
        HtIo<HTInterval, HTNode<HTInterval>> htio = fHtIo;
        assertNotNull(htio);

        int coreNodeSeqNum = 0;
        int leafNodeSeqNum = 1;

        // Add a core node and a leaf node
        HTNode<HTInterval> coreNode = createCoreNode(coreNodeSeqNum, -1);
        assertFalse(HtIo.isInCache(htio, coreNodeSeqNum));
        htio.writeNode(coreNode);
        assertTrue(HtIo.isInCache(htio, coreNodeSeqNum));

        HTNode<HTInterval> leafNode = createLeafNode(leafNodeSeqNum, coreNodeSeqNum);
        assertFalse(HtIo.isInCache(htio, leafNodeSeqNum));
        htio.writeNode(leafNode);
        assertTrue(HtIo.isInCache(htio, leafNodeSeqNum));

        // Now read the nodes from the same htio object, they should be in cache
        HTNode<HTInterval> coreRead = htio.readNode(coreNodeSeqNum);
        assertEquals(coreNode, coreRead);
        HTNode<HTInterval> leafRead = htio.readNode(leafNodeSeqNum);
        assertEquals(leafNode, leafRead);

        // Invalidate the cache
        HtIo.clearCache();

        // Re-read the nodes, they should now be read from disk and be in the
        // cache after
        assertFalse(HtIo.isInCache(htio, coreNodeSeqNum));
        coreRead = htio.readNode(coreNodeSeqNum);
        assertEquals(coreNode, coreRead);
        assertTrue(HtIo.isInCache(htio, coreNodeSeqNum));

        // Read the leaf node from disk
        assertFalse(HtIo.isInCache(htio, leafNodeSeqNum));
        leafRead = htio.readNode(leafNodeSeqNum);
        assertEquals(leafNode, leafRead);
        assertTrue(HtIo.isInCache(htio, leafNodeSeqNum));

        // Close the file and reopen a new htio object
        htio.closeFile();

        assertNotNull(fStateFile);
        htio = new HtIo<>(fStateFile,
                BLOCKSIZE,
                NB_CHILDREN,
                false,
                HtTestUtils.READ_FACTORY,
                HistoryTreeStub.NODE_FACTORY);

        fHtIo = htio;

        // Read the core node from the disk
        assertFalse(HtIo.isInCache(htio, coreNodeSeqNum));
        coreRead = htio.readNode(coreNodeSeqNum);
        assertEquals(coreNode, coreRead);

        // Read the leaf node from the disk
        assertFalse(HtIo.isInCache(htio, leafNodeSeqNum));
        leafRead = htio.readNode(leafNodeSeqNum);
        assertEquals(leafNode, leafRead);

        // Re-read the nodes, they should have been read from the cache
        assertTrue(HtIo.isInCache(htio, coreNodeSeqNum));
        coreRead = htio.readNode(coreNodeSeqNum);
        assertEquals(coreNode, coreRead);

        // Read the leaf node from cache
        assertTrue(HtIo.isInCache(htio, leafNodeSeqNum));
        leafRead = htio.readNode(leafNodeSeqNum);
        assertEquals(leafNode, leafRead);
    }

    /**
     * Test that the section at the end of the file where extra data can be
     * written works well
     *
     * @throws IOException
     *             Exception thrown by the file
     */
    @Test
    public void testExtraDataSave() throws IOException {
        writeBufferAtNodePos(2);
    }

    /**
     * Test that writing at the beginning of the file works well
     *
     * @throws IOException
     *             Exception thrown by the file
     */
    @Test
    public void testHeaderDataSave() throws IOException {
        writeBufferAtNodePos(-1);
    }

    /**
     * Test that writing data far beyond the node section end works.
     *
     * @throws IOException
     *             Exception thrown by the file
     */
    @Test
    public void testTooFarData() throws IOException {
        writeBufferAtNodePos(6);
    }

    private void writeBufferAtNodePos(int nodeOffset) throws IOException {
        HtIo<HTInterval, HTNode<HTInterval>> htio = fHtIo;
        assertNotNull(htio);

        int coreNodeSeqNum = 0;
        int leafNodeSeqNum = 1;

        // Add a core node and a leaf node
        HTNode<HTInterval> coreNode = createCoreNode(coreNodeSeqNum, -1);
        assertFalse(HtIo.isInCache(htio, coreNodeSeqNum));
        htio.writeNode(coreNode);
        assertTrue(HtIo.isInCache(htio, coreNodeSeqNum));

        HTNode<HTInterval> leafNode = createLeafNode(leafNodeSeqNum, coreNodeSeqNum);
        assertFalse(HtIo.isInCache(htio, leafNodeSeqNum));
        htio.writeNode(leafNode);
        assertTrue(HtIo.isInCache(htio, leafNodeSeqNum));

        // Write 3 integers at some position of the file
        ByteBuffer buffer = ByteBuffer.allocate(12);
        buffer.putInt(32);
        buffer.putInt(33);
        buffer.putInt(232);
        buffer.flip();

        try (FileOutputStream fcOut = htio.getFileWriter(nodeOffset)) {
            fcOut.write(buffer.array());
        }

        // Close the file and reopen a new htio object
        htio.closeFile();
        assertNotNull(fStateFile);
        htio = new HtIo<>(fStateFile,
                BLOCKSIZE,
                NB_CHILDREN,
                false,
                HtTestUtils.READ_FACTORY,
                HistoryTreeStub.NODE_FACTORY);

        fHtIo = htio;

        // Read the same 3 integers at the same position in the file
        byte[] bytes = new byte[12];
        htio.supplyATReader(nodeOffset).read(bytes);
        buffer = ByteBuffer.wrap(bytes);
        assertEquals(32, buffer.getInt());
        assertEquals(33, buffer.getInt());
        assertEquals(232, buffer.getInt());
    }
}
