/*******************************************************************************
 * Copyright (c) 2013, 2017 Ericsson
 *
 * 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
 *
 * Contributors:
 *   Matthew Khouzam - Initial API and implementation
 *   Marc-Andre Laperle - Move generation to traces folder
 *******************************************************************************/

package org.eclipse.tracecompass.ctf.core.tests.shared;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.eclipse.tracecompass.ctf.core.tests.CtfCoreTestPlugin;

/**
 * Generate a lttng trace (kernel or ust)
 *
 * @author Matthew Khouzam
 */
public class LttngTraceGenerator {

    private static final String metadataKernel = "/* CTF 1.8 */ \n" +
            "typealias integer { size = 8; align = 8; signed = false; } := uint8_t;\n" +
            "typealias integer { size = 16; align = 8; signed = false; } := uint16_t;\n" +
            "typealias integer { size = 32; align = 8; signed = false; } := uint32_t;\n" +
            "typealias integer { size = 64; align = 8; signed = false; } := uint64_t;\n" +
            "typealias integer { size = 32; align = 8; signed = false; } := unsigned long;\n" +
            "typealias integer { size = 5; align = 1; signed = false; } := uint5_t;\n" +
            "typealias integer { size = 27; align = 1; signed = false; } := uint27_t;\n" +
            "\n" +
            "trace {\n" +
            "   major = 1;\n" +
            "   minor = 8;\n" +
            "   uuid = \"11111111-1111-1111-1111-111111111111\";\n" +
            "   byte_order = le;\n" +
            "   packet.header := struct {\n" +
            "       uint32_t magic;\n" +
            "       uint8_t  uuid[16];\n" +
            "       uint32_t stream_id;\n" +
            "   };\n" +
            "};\n" +
            "\n" +
            "env {\n" +
            "   hostname = \"synthetic-host\";\n" +
            "   domain = \"kernel\";\n" +
            "   sysname = \"FakeLinux\";\n" +
            "   kernel_release = \"1.0\";\n" +
            "   kernel_version = \"Fake Os Synthetic Trace\";\n" +
            "   tracer_name = \"lttng-modules\";\n" +
            "   tracer_major = 2;\n" +
            "   tracer_minor = 1;\n" +
            "   tracer_patchlevel = 0;\n" +
            "};\n" +
            "\n" +
            "clock {\n" +
            "   name = monotonic;\n" +
            "   uuid = \"bbff68f0-c633-4ea1-92cd-bd11024ec4de\";\n" +
            "   description = \"Monotonic Clock\";\n" +
            "   freq = 1000000000; /* Frequency, in Hz */\n" +
            "   /* clock value offset from Epoch is: offset * (1/freq) */\n" +
            "   offset = 1368000272650993664;\n" +
            "};\n" +
            "\n" +
            "typealias integer {\n" +
            "   size = 27; align = 1; signed = false;\n" +
            "   map = clock.monotonic.value;\n" +
            "} := uint27_clock_monotonic_t;\n" +
            "\n" +
            "typealias integer {\n" +
            "   size = 32; align = 8; signed = false;\n" +
            "   map = clock.monotonic.value;\n" +
            "} := uint32_clock_monotonic_t;\n" +
            "\n" +
            "typealias integer {\n" +
            "   size = 64; align = 8; signed = false;\n" +
            "   map = clock.monotonic.value;\n" +
            "} := uint64_clock_monotonic_t;\n" +
            "\n" +
            "struct packet_context {\n" +
            "   uint64_clock_monotonic_t timestamp_begin;\n" +
            "   uint64_clock_monotonic_t timestamp_end;\n" +
            "   uint64_t content_size;\n" +
            "   uint64_t packet_size;\n" +
            "   unsigned long events_discarded;\n" +
            "   uint32_t cpu_id;\n" +
            "};\n" +
            "\n" +
            "struct event_header_compact {\n" +
            "   enum : uint5_t { compact = 0 ... 30, extended = 31 } id;\n" +
            "   variant <id> {\n" +
            "       struct {\n" +
            "           uint27_clock_monotonic_t timestamp;\n" +
            "       } compact;\n" +
            "       struct {\n" +
            "           uint32_t id;\n" +
            "           uint64_clock_monotonic_t timestamp;\n" +
            "       } extended;\n" +
            "   } v;\n" +
            "} align(8);\n" +
            "\n" +
            "struct event_header_large {\n" +
            "   enum : uint16_t { compact = 0 ... 65534, extended = 65535 } id;\n" +
            "   variant <id> {\n" +
            "       struct {\n" +
            "           uint32_clock_monotonic_t timestamp;\n" +
            "       } compact;\n" +
            "       struct {\n" +
            "           uint32_t id;\n" +
            "           uint64_clock_monotonic_t timestamp;\n" +
            "       } extended;\n" +
            "   } v;\n" +
            "} align(8);\n" +
            "\n" +
            "stream {\n" +
            "   id = 0;\n" +
            "   event.header := struct event_header_compact;\n" +
            "   packet.context := struct packet_context;\n" +
            "};\n" +
            "\n" +
            "event {\n" +
            "   name = sched_switch;\n" +
            "   id = 0;\n" +
            "   stream_id = 0;\n" +
            "   fields := struct {\n" +
            "       integer { size = 8; align = 8; signed = 1; encoding = UTF8; base = 10; } _prev_comm[16];\n" +
            "       integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _prev_tid;\n" +
            "       integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _prev_prio;\n" +
            "       integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _prev_state;\n" +
            "       integer { size = 8; align = 8; signed = 1; encoding = UTF8; base = 10; } _next_comm[16];\n" +
            "       integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _next_tid;\n" +
            "       integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _next_prio;\n" +
            "   };\n" +
            "};\n" +
            "\n";

    private final List<String> fProcesses;
    private final long fDuration;
    private final long fNbEvents;
    private final int fNbChans;

    private final String metadata;

    private static final String[] sfProcesses = {
            "IDLE",
            "gnuplot",
            "starcraft 2:pt3",
            "bash",
            "smash",
            "thrash",
            "fireball",
            "Half-life 3",
            "ST: The game"
    };

    private static final String TRACES_DIRECTORY = "traces";
    private static final String TRACE_NAME = "synthetic-trace";

    /**
     * Main, not always needed
     *
     * @param args
     *            args
     */
    public static void main(String[] args) {
        // not using createTempFile as this is a directory
        String path = CtfCoreTestPlugin.getTemporaryDirPath() + File.separator + TRACE_NAME;
        generateLttngTrace(new File(path));
    }

    /**
     * Gets the name of the trace (top directory name)
     *
     * @return the name of the trace
     */
    public static String getName() {
        return TRACE_NAME;
    }

    /**
     * Get the path
     *
     * @return the path
     */
    public static String getPath() {
        CtfCoreTestPlugin plugin = CtfCoreTestPlugin.getDefault();
        if (plugin == null) {
            return null;
        }
        Path tracePath = Paths.get("..", "..", "ctf", "org.eclipse.tracecompass.ctf.core.tests", TRACES_DIRECTORY, TRACE_NAME);
        tracePath = tracePath.toAbsolutePath();
        File file = tracePath.toFile();
        generateLttngTrace(file);
        return file.getAbsolutePath();
    }

    /**
     * Generate a trace
     *
     * @param file
     *            the file to write the trace to
     */
    public static void generateLttngTrace(File file) {
        final int cpus = 25;
        LttngTraceGenerator gt = new LttngTraceGenerator(Integer.MAX_VALUE / 12L, 250000 / 12L, cpus);
        gt.writeTrace(file);
    }

    /**
     * Make a lttng trace
     *
     * @param duration
     *            the duration of the trace
     * @param events
     *            the number of events in a trace
     * @param nbChannels
     *            the number of channels in the trace
     */
    public LttngTraceGenerator(long duration, long events, int nbChannels) {
        this(duration, events, nbChannels, true);
    }

    /**
     * Make a lttng trace
     *
     * @param duration
     *            the duration of the trace
     * @param events
     *            the number of events in a trace
     * @param nbChannels
     *            the number of channels in the trace
     * @param isKernel
     *            true for kernel, false for ust
     */
    public LttngTraceGenerator(long duration, long events, int nbChannels, boolean isKernel) {
        fProcesses = Arrays.asList(sfProcesses);
        fDuration = duration;
        fNbEvents = events;
        fNbChans = nbChannels;
        metadata = isKernel ? metadataKernel : getMetadataUST();
    }

    /**
     * Write the trace to a file
     *
     * @param file
     *            the file to write the trace to
     */
    public void writeTrace(File file) {

        if (file.exists()) {
            deleteDirectory(file);
        }
        file.mkdir();

        File metadataFile = new File(file.getPath() + File.separator + "metadata");
        File[] streams = new File[fNbChans];
        FileChannel[] channels = new FileChannel[fNbChans];

        try {
            for (int i = 0; i < fNbChans; i++) {
                streams[i] = new File(file.getPath() + File.separator + "channel" + i);
                channels[i] = new FileOutputStream(streams[i]).getChannel();
            }
        } catch (FileNotFoundException e) {
        }
        // determine the number of events per channel
        long evPerChan = fNbEvents / fNbChans;
        final int evPerPacket = PacketWriter.CONTENT_SIZE / EventWriter.SIZE;
        long delta = (int) (fDuration / evPerChan);
        long offsetTime = 0;
        Random rndLost = new Random(1337);
        for (int chan = 0; chan < fNbChans; chan++) {
            int currentSpace = 0;
            ByteBuffer bb = ByteBuffer.allocate(65536);
            bb.order(ByteOrder.LITTLE_ENDIAN);
            Random rnd = new Random(1337);
            int rnd0 = rnd.nextInt(fProcesses.size());
            String prevComm = fProcesses.get(rnd0);
            int prevPID = rnd0 + chan * fProcesses.size();
            if (rnd0 == 0) {
                prevPID = 0;
            }
            int prevPrio = 0;
            int prevPos = -1;
            int discarded = 0;
            int discardedTotal = 0;
            for (int eventNb = 0; eventNb < evPerChan; eventNb++) {
                if (EventWriter.SIZE > currentSpace) {
                    eventNb += discarded;
                }
                long ts = eventNb * delta + delta / (fNbChans + 1) * chan;

                int pos = rnd.nextInt((int) (fProcesses.size() * 1.5));
                if (pos >= fProcesses.size()) {
                    pos = 0;
                }
                while (pos == prevPos) {
                    pos = rnd.nextInt((int) (fProcesses.size() * 1.5));
                    if (pos >= fProcesses.size()) {
                        pos = 0;
                    }
                }
                String nextComm = fProcesses.get(pos);
                int nextPID = pos + fProcesses.size() * chan;
                if (pos == 0) {
                    nextPID = 0;
                }
                int nextPrio = 0;
                if (EventWriter.SIZE > currentSpace) {
                    // pad to end
                    for (int i = 0; i < currentSpace; i++) {
                        bb.put((byte) 0x00);
                    }
                    // write new packet
                    PacketWriter pw = new PacketWriter(bb);
                    long tsBegin = ts;
                    offsetTime = ts;
                    int eventCount = Math.min(evPerPacket, (int) evPerChan - eventNb);
                    discarded = rndLost.nextInt(10 * fNbChans) == 0 ? rndLost.nextInt(evPerPacket) : 0;
                    discarded = Math.min(discarded, (int) evPerChan - eventNb - eventCount);
                    discardedTotal += discarded;
                    long tsEnd = (eventNb + eventCount + discarded) * delta;
                    pw.writeNewHeader(tsBegin, tsEnd, chan, eventCount, discardedTotal);
                    currentSpace = PacketWriter.CONTENT_SIZE;
                }
                EventWriter ew = new EventWriter(bb);
                int prev_state = rnd.nextInt(100);
                if (prev_state != 0) {
                    prev_state = 1;
                }
                final long shrunkenTimestamp = ts - offsetTime;
                final int tsMask = (1 << 27) - 1;
                if (shrunkenTimestamp > ((1 << 27) + tsMask)) {
                    /* allow only one compact timestamp overflow per packet */
                    throw new IllegalStateException("Invalid timestamp overflow:" + shrunkenTimestamp);
                }
                final int clampedTs = (int) (ts & tsMask);
                int evSize = ew.writeEvent(clampedTs, prevComm, prevPID, prevPrio, prev_state, nextComm, nextPID, nextPrio);
                currentSpace -= evSize;
                prevComm = nextComm;
                prevPID = nextPID;
                prevPrio = nextPrio;
                if (bb.position() > 63000) {
                    writeToDisk(channels, chan, bb);
                }
            }
            for (int i = 0; i < currentSpace; i++) {
                bb.put((byte) 0x00);
            }
            writeToDisk(channels, chan, bb);
            try {
                channels[chan].close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try (FileOutputStream fos = new FileOutputStream(metadataFile);) {
            fos.write(metadata.getBytes());
        } catch (IOException e) {
        }
    }

    private static void deleteDirectory(File directory) {
        try {
            Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    // If a file failed to delete, it's more useful to throw
                    // this instead
                    if (exc != null) {
                        throw exc;
                    }
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void writeToDisk(FileChannel[] channels, int chan, ByteBuffer bb) {
        try {
            bb.flip();
            channels[chan].write(bb);
            bb.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String getMetadataUST() {
        String metadata = metadataKernel.replace("\"kernel\"", "\"ust\"");
        return metadata.replace("lttng-modules", "lttng-ust");
    }

    private class EventWriter {
        public static final int SIZE = 4 + // timestamp
                16 + // prev_comm
                4 + // prev_tid
                4 + // prev_prio
                4 + // prev_state
                16 + // current_comm
                4 + // next_tid
                4; // next_prio
        private final ByteBuffer data;

        public EventWriter(ByteBuffer bb) {
            data = bb;
        }

        public int writeEvent(int ts, String prev_comm, int prev_tid, int prev_prio, int prev_state, String next_comm, int next_tid, int next_prio) {
            byte[] bOut = new byte[16];
            byte[] bIn = new byte[16];
            byte[] temp = prev_comm.getBytes();
            for (int i = 0; i < Math.min(temp.length, 16); i++) {
                bOut[i] = temp[i];
            }
            temp = next_comm.getBytes();
            for (int i = 0; i < Math.min(temp.length, 16); i++) {
                bIn[i] = temp[i];
            }

            int timestamp = ts << 5;

            data.putInt(timestamp);
            data.put(bOut);
            data.putInt(prev_tid);
            data.putInt(prev_prio);
            data.putInt(prev_state);
            data.put(bIn);
            data.putInt(next_tid);
            data.putInt(next_prio);
            return SIZE;
        }

    }

    private class PacketWriter {
        private static final int SIZE = 4096;
        private static final int HEADER_SIZE = 64;
        private static final int CONTENT_SIZE = SIZE - HEADER_SIZE;

        private final ByteBuffer data;

        public PacketWriter(ByteBuffer bb) {
            data = bb;
        }

        public void writeNewHeader(long tsBegin, long tsEnd, int cpu, int eventCount, int discarded) {
            final int magicLE = 0xC1FC1FC1;
            byte uuid[] = {
                    0x11, 0x11, 0x11, 0x11,
                    0x11, 0x11, 0x11, 0x11,
                    0x11, 0x11, 0x11, 0x11,
                    0x11, 0x11, 0x11, 0x11 };
            // packet header

            // magic number 4
            data.putInt(magicLE);
            // uuid 16
            data.put(uuid);
            // stream ID 4
            data.putInt(0);

            // packet context
            // timestamp_begin 8
            data.putLong(tsBegin);

            // timestamp_end 8
            data.putLong(tsEnd);

            // content_size 8
            data.putLong((eventCount * EventWriter.SIZE + HEADER_SIZE) * 8);

            // packet_size 8
            data.putLong((SIZE) * 8);

            // events_discarded 4
            data.putInt(discarded);

            // cpu_id 4
            data.putInt(cpu);

        }

    }

}
