/*******************************************************************************
 * Copyright (c) 2015, 2018 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:
 *   Bernd Hufmann - Initial API and implementation
 *******************************************************************************/
package org.eclipse.tracecompass.ctf.core.trace;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.StandardOpenOption;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.ctf.core.CTFException;
import org.eclipse.tracecompass.ctf.core.CTFStrings;
import org.eclipse.tracecompass.ctf.core.event.IEventDefinition;
import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer;
import org.eclipse.tracecompass.ctf.core.event.types.Definition;
import org.eclipse.tracecompass.ctf.core.event.types.FloatDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.FloatDefinition;
import org.eclipse.tracecompass.ctf.core.event.types.ICompositeDefinition;
import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.ISimpleDatatypeDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.SimpleDatatypeDefinition;
import org.eclipse.tracecompass.ctf.core.event.types.StringDefinition;
import org.eclipse.tracecompass.ctf.core.event.types.StructDefinition;
import org.eclipse.tracecompass.internal.ctf.core.Activator;
import org.eclipse.tracecompass.internal.ctf.core.SafeMappedByteBuffer;
import org.eclipse.tracecompass.internal.ctf.core.trace.CTFPacketReader;
import org.eclipse.tracecompass.internal.ctf.core.trace.StreamInputPacketIndexEntry;

/**
 * CTF trace packet writer.
 *
 * @author Bernd Hufmann
 * @since 1.0
 */
public class CTFStreamPacketOutputWriter {

    private CTFStreamInput fStreamInput;

    /**
     * Default constructor
     */
    public CTFStreamPacketOutputWriter() {
        throw new UnsupportedOperationException();
    }

    /**
     * Writer output
     *
     * @param streamInput
     *            the reference input stream to copy
     * @since 3.0
     */
    public CTFStreamPacketOutputWriter(@NonNull CTFStreamInput streamInput) {
        fStreamInput = streamInput;
    }

    /**
     * Writes a stream packet to the output file channel based on the packet
     * descriptor information.
     *
     * @param byteBuffer
     *            a byte buffer with packet to write
     * @param output
     *            a file channel
     * @throws IOException
     *             if a reading or writing error occurs
     */
    public void writePacket(ByteBuffer byteBuffer, FileChannel output) throws IOException {
        output.write(byteBuffer);
    }

    /**
     * Writes a stream packet to the output file channel based on the packet
     * descriptor information.
     *
     * @param entry
     *            the packet descriptor
     * @param output
     *            the file channel to write to
     * @param initialLost
     *            the offset initial lost events as the field is cumulative
     *
     * @throws IOException
     *             if a reading or writing error occurs
     * @throws CTFException
     *             ctf reading error
     * @since 3.0
     */
    public void writePacket(ICTFPacketDescriptor entry, FileChannel output, long initialLost) throws IOException, CTFException {
        long startOffsetBits = entry.getPayloadStartBits();
        long endOffsetBits = entry.getContentSizeBits();
        StructDefinition context = null;
        File file = fStreamInput.getFile();
        try (FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.READ);) {
            ByteBuffer bb = SafeMappedByteBuffer.map(fc, FileChannel.MapMode.READ_ONLY, entry.getOffsetBytes(), (long) Math.ceil(entry.getContentSizeBits() / (double) Byte.SIZE));
            BitBuffer bbInput = new BitBuffer(bb);
            bbInput.position(entry.getPayloadStartBits());
            ICTFStream stream = fStreamInput.getStream();
            CTFTrace trace = stream.getTrace();
            CTFPacketReader currentPacketReader = new CTFPacketReader(
                    bbInput,
                    entry,
                    stream.getEventDeclarations(),
                    stream.getEventHeaderDeclaration(),
                    stream.getEventContextDecl(),
                    trace.getPacketHeaderDef(),
                    trace);
            if (entry instanceof StreamInputPacketIndexEntry) {
                context = ((StreamInputPacketIndexEntry) entry).getStreamPacketContextDef();
            }
            if (context == null) {
                return;
            }
            ICompositeDefinition tracePacketHeader = currentPacketReader.getTracePacketHeader();
            writeCustomPacket(entry, fc, output, startOffsetBits, endOffsetBits, context, tracePacketHeader, entry.getTimestampBegin(), initialLost);
        }
    }

    /**
     * Make a packet and write it to the fileChannel
     *
     * @param entry
     *            the reference packet descriptor
     * @param startTime
     *            startTime of the packet
     * @param endTime
     *            endTime of the packet
     * @param initialLost
     *            Initial quantity of lost events
     * @param output
     *            the file channel to write to
     * @throws IOException
     *             if a reading or writing error occurs
     * @throws CTFException
     *             ctf reading error
     * @since 3.0
     */
    public void writePacket(ICTFPacketDescriptor entry, long startTime, long endTime, long initialLost, FileChannel output) throws IOException, CTFException {
        if (entry.getTimestampBegin() > endTime || entry.getTimestampEnd() < startTime || startTime > endTime) {
            throw new IllegalStateException();
        }
        long startOffsetBits = entry.getPayloadStartBits();
        long endOffsetBits = entry.getContentSizeBits();
        StructDefinition context = null;
        File file = fStreamInput.getFile();
        try (FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.READ);) {
            ByteBuffer bb = SafeMappedByteBuffer.map(fc, FileChannel.MapMode.READ_ONLY, entry.getOffsetBytes(), bitsToBytes(entry.getContentSizeBits()));
            BitBuffer bbInput = new BitBuffer(bb);
            bbInput.position(entry.getPayloadStartBits());
            ICTFStream stream = fStreamInput.getStream();
            CTFTrace trace = stream.getTrace();
            CTFPacketReader currentPacketReader = new CTFPacketReader(
                    bbInput,
                    entry,
                    stream.getEventDeclarations(),
                    stream.getEventHeaderDeclaration(),
                    stream.getEventContextDecl(),
                    trace.getPacketHeaderDef(),
                    trace);
            long lastLargeTimestamp = entry.getTimestampBegin();
            if (entry instanceof StreamInputPacketIndexEntry) {
                context = ((StreamInputPacketIndexEntry) entry).getStreamPacketContextDef();
            }
            if (context == null) {
                return;
            }
            boolean startIsSet = false;
            while (currentPacketReader.hasMoreEvents()) {

                IEventDefinition event = currentPacketReader.readNextEvent();
                long timestampInCycles = event.getTimestamp();
                if (!startIsSet) {
                    /*
                     * handle "P" and "I" framed timestamps. overwrite the
                     * timestamp if there is one before the trim
                     */
                    ICompositeDefinition eventHeader = event.getEventHeader();
                    Definition def = null;
                    if (eventHeader != null) {
                        def = eventHeader.getDefinition(CTFStrings.TIMESTAMP);
                    }
                    if (def == null && eventHeader instanceof StructDefinition) {
                        StructDefinition structDefinition = (StructDefinition) eventHeader;
                        def = structDefinition.lookupDefinition(CTFStrings.TIMESTAMP);
                    }
                    Definition definition = ((StreamInputPacketIndexEntry) entry).getStreamPacketContextDef().getDefinition(CTFStrings.TIMESTAMP_BEGIN);
                    if (definition != null && def != null && def.size() == definition.size()) {
                        lastLargeTimestamp = timestampInCycles;
                    }
                }
                if (timestampInCycles >= startTime && !startIsSet) {
                    startOffsetBits = currentPacketReader.getLocation();
                    startIsSet = true;
                }
                if (timestampInCycles > endTime + 1) {
                    endOffsetBits = currentPacketReader.getLocation();
                    break;
                }
            }
            if (startOffsetBits >= endOffsetBits) {
                return;
            }
            ICompositeDefinition tracePacketHeader = currentPacketReader.getTracePacketHeader();
            writeCustomPacket(entry, fc, output, startOffsetBits, endOffsetBits, context, tracePacketHeader, lastLargeTimestamp, initialLost);
        }
    }

    private static void writeCustomPacket(ICTFPacketDescriptor entry, FileChannel source, FileChannel output, long startOffsetBits, long endOffsetBits, StructDefinition packetContext, ICompositeDefinition tracePacketHeader, long startTime,
            long initialLost)
            throws IOException, CTFException {
        ByteBuffer inBuffer = SafeMappedByteBuffer.map(source, MapMode.READ_ONLY, entry.getOffsetBytes(), bitsToBytes(entry.getContentSizeBits()));
        int headerSizeBits = (int) (tracePacketHeader == null ? 0 : tracePacketHeader.size());
        int packetSizeBits = (int) (headerSizeBits + packetContext.size() + endOffsetBits - startOffsetBits);
        int alignmentSize = Byte.SIZE;
        int lastBitsOfBody = (int) (endOffsetBits % alignmentSize);
        int contentSizeBits = (lastBitsOfBody != 0) ? (packetSizeBits - alignmentSize + lastBitsOfBody) : packetSizeBits;
        byte[] toWrite = new byte[(int) bitsToBytes(packetSizeBits)];
        ByteBuffer buffer = ByteBuffer.wrap(toWrite);
        byte[] header = new byte[(int) bitsToBytes(headerSizeBits)];
        byte[] body = new byte[(int) bitsToBytes(endOffsetBits - startOffsetBits)];
        inBuffer.get(header);
        inBuffer.position((int) bitsToBytes(startOffsetBits));
        inBuffer.get(body);
        buffer.put(header);
        writeContext(startTime, packetContext, contentSizeBits, (int) bitsToBytes(packetSizeBits) * Byte.SIZE, initialLost, buffer);
        buffer.position((int) bitsToBytes(headerSizeBits + packetContext.size()));
        buffer.put(body);
        output.write(ByteBuffer.wrap(toWrite));
    }

    private static long bitsToBytes(long bits) {
        return (long) Math.ceil(bits / (double) Byte.SIZE);
    }

    private static void writeContext(long startTime, StructDefinition context, int newContentSize, int newPacketSize, long initialLost, @NonNull ByteBuffer buffer) throws CTFException {
        BitBuffer bb = new BitBuffer(buffer);
        bb.position(buffer.position() * Byte.SIZE);
        for (String field : context.getFieldNames()) {
            Definition def = context.getDefinition(field);
            IDeclaration declaration = def.getDeclaration();
            align(declaration.getAlignment(), bb);
            if (declaration instanceof ISimpleDatatypeDeclaration) {
                bb.setByteOrder(((ISimpleDatatypeDeclaration) declaration).getByteOrder());
            }
            if (def instanceof SimpleDatatypeDefinition) {
                SimpleDatatypeDefinition simpleDef = (SimpleDatatypeDefinition) def;
                int size = (int) simpleDef.size();
                if (field.equals(CTFStrings.PACKET_SIZE)) {
                    bb.putLong(size, newPacketSize);
                } else if (field.equals(CTFStrings.CONTENT_SIZE)) {
                    bb.putLong(size, newContentSize);
                } else if (field.equals(CTFStrings.TIMESTAMP_BEGIN)) {
                    bb.putLong(size, startTime);
                } else if (field.equals(CTFStrings.EVENTS_DISCARDED)) {
                    bb.putLong(size, simpleDef.getIntegerValue() - initialLost);
                } else {
                    bb.putLong(size, simpleDef.getIntegerValue());
                }
            } else if (def instanceof StringDefinition) {
                writeString(bb, def);
            } else if (def instanceof FloatDefinition) {
                writeFloat(bb, def);
            } else {
                buffer.put(new byte[(int) bitsToBytes(def.size())]);
                bb.position(buffer.position() * 8L);
            }
        }
        buffer.position((int) bitsToBytes(bb.position()));
    }

    private static void align(long align, BitBuffer input) throws CTFException {
        long mask = align - 1;
        /*
         * The alignment is a power of 2
         */
        long pos = input.position();
        if ((pos & mask) == 0) {
            return;
        }
        pos = (pos + mask) & ~mask;
        input.position(pos);
    }

    private static void writeFloat(BitBuffer bb, Definition def) throws CTFException {
        FloatDefinition floatDefinition = (FloatDefinition) def;
        double value = floatDefinition.getValue();
        FloatDeclaration declaration = floatDefinition.getDeclaration();
        int exponent = declaration.getExponent();
        int mantissa = declaration.getMantissa();
        if (mantissa == 23 && exponent == 8) {
            bb.putInt(Float.SIZE, Float.floatToIntBits((float) value));
        } else if (mantissa == 52 && exponent == 11) {
            bb.putLong(Double.SIZE, Double.doubleToLongBits(value));
        } else {
            // Inaccurate, but won't break the trace
            bb.putLong((int) floatDefinition.size(), Double.doubleToRawLongBits(value));
        }
    }

    private static void writeString(BitBuffer bb, Definition def) throws CTFException {
        StringDefinition stringDefinition = (StringDefinition) def;
        // may be encoded differently, but play it safe first.
        byte[] bytes = stringDefinition.toString().getBytes();
        try {
            bytes = stringDefinition.toString().getBytes(stringDefinition.getDeclaration().getEncoding().toString());
        } catch (UnsupportedEncodingException e) {
            Activator.log("Writing packet made an encoding error! " + e.getMessage()); //$NON-NLS-1$
        }
        bb.put(bytes);
        bb.put((byte) 0);
    }

}
