/*******************************************************************************
 * Copyright (c) 2014, 2019 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:
 *   Vincent Perot - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.internal.pcap.core.protocol.tcp;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.pcap.core.packet.BadPacketException;
import org.eclipse.tracecompass.internal.pcap.core.packet.Packet;
import org.eclipse.tracecompass.internal.pcap.core.protocol.PcapProtocol;
import org.eclipse.tracecompass.internal.pcap.core.protocol.unknown.UnknownPacket;
import org.eclipse.tracecompass.internal.pcap.core.trace.PcapFile;
import org.eclipse.tracecompass.internal.pcap.core.util.ConversionHelper;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;

/**
 * Class that represents a TCP packet.
 *
 * @author Vincent Perot
 */
public class TCPPacket extends Packet {

    private final @Nullable Packet fChildPacket;
    private final @Nullable ByteBuffer fPayload;

    private final int fSourcePort;
    private final int fDestinationPort;
    private final long fSequenceNumber;
    private final long fAcknowledgmentNumber;
    private final int fDataOffset; // in 4 bytes block
    private final byte fReservedField;
    private final boolean fNSFlag;
    private final boolean fCWRFlag;
    private final boolean fECEFlag;
    private final boolean fURGFlag;
    private final boolean fACKFlag;
    private final boolean fPSHFlag;
    private final boolean fRSTFlag;
    private final boolean fSYNFlag;
    private final boolean fFINFlag;
    private final int fWindowSize;
    private final int fChecksum;
    private final int fUrgentPointer;
    private final byte @Nullable [] fOptions; // TODO Interpret options.

    private @Nullable TCPEndpoint fSourceEndpoint;
    private @Nullable TCPEndpoint fDestinationEndpoint;

    private @Nullable Map<String, String> fFields;

    /**
     * Constructor of the TCP Packet class.
     *
     * @param file
     *            The file that contains this packet.
     * @param parent
     *            The parent packet of this packet (the encapsulating packet).
     * @param packet
     *            The entire packet (header and payload).
     * @throws BadPacketException
     *             Thrown when the packet is erroneous.
     */
    public TCPPacket(PcapFile file, @Nullable Packet parent, ByteBuffer packet) throws BadPacketException {
        super(file, parent, PcapProtocol.TCP);

        // The endpoints are lazy loaded. They are defined in the get*Endpoint()
        // methods.
        fSourceEndpoint = null;
        fDestinationEndpoint = null;

        fFields = null;

        packet.order(ByteOrder.BIG_ENDIAN);
        packet.position(0);

        fSourcePort = ConversionHelper.unsignedShortToInt(packet.getShort());
        fDestinationPort = ConversionHelper.unsignedShortToInt(packet.getShort());
        fSequenceNumber = ConversionHelper.unsignedIntToLong(packet.getInt());
        fAcknowledgmentNumber = ConversionHelper.unsignedIntToLong(packet.getInt());

        byte storage = packet.get();
        fDataOffset = ((storage & 0b11110000) >>> 4) & 0x000000FF;
        fReservedField = (byte) ((storage & 0b00001110) >>> 1);
        fNSFlag = isBitSet(storage, 0);

        storage = packet.get();
        fCWRFlag = isBitSet(storage, 7);
        fECEFlag = isBitSet(storage, 6);
        fURGFlag = isBitSet(storage, 5);
        fACKFlag = isBitSet(storage, 4);
        fPSHFlag = isBitSet(storage, 3);
        fRSTFlag = isBitSet(storage, 2);
        fSYNFlag = isBitSet(storage, 1);
        fFINFlag = isBitSet(storage, 0);

        fWindowSize = ConversionHelper.unsignedShortToInt(packet.getShort());
        fChecksum = ConversionHelper.unsignedShortToInt(packet.getShort());
        fUrgentPointer = ConversionHelper.unsignedShortToInt(packet.getShort());

        // Get options if any
        if (fDataOffset > TCPValues.DEFAULT_HEADER_LENGTH) {
            fOptions = new byte[(fDataOffset - TCPValues.DEFAULT_HEADER_LENGTH) * TCPValues.BLOCK_SIZE];
            packet.get(fOptions);
        } else {
            fOptions = null;
        }

        // Get payload if any.
        if (packet.remaining() > 0) {
            ByteBuffer payload = packet.slice();
            payload.order(ByteOrder.BIG_ENDIAN);
            fPayload = payload;
        } else {
            fPayload = null;
        }

        // find child packet
        fChildPacket = findChildPacket();

    }

    @Override
    public @Nullable Packet getChildPacket() {
        return fChildPacket;
    }

    @Override
    public @Nullable ByteBuffer getPayload() {
        return fPayload;
    }

    /**
     * {@inheritDoc}
     *
     * See http://www.iana.org/assignments/service-names-port-numbers/service-
     * names-port-numbers.xhtml or
     * http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
     */
    @Override
    protected @Nullable Packet findChildPacket() throws BadPacketException {
        // TODO implement further protocols and update this
        ByteBuffer payload = fPayload;
        if (payload == null) {
            return null;
        }

        return new UnknownPacket(getPcapFile(), this, payload);
    }

    @Override
    public String toString() {
        final ByteBuffer payload = fPayload;
        int length = 0;
        if (payload != null) {
            length = payload.limit();
        }

        String flagString = ""; // TODO Finish it. Im just too lazy. //$NON-NLS-1$
        String string = getProtocol().getName() + ", Source Port: " + fSourcePort + ", Destination Port: " + fDestinationPort + //$NON-NLS-1$ //$NON-NLS-2$
                "\nSequence Number: " + fSequenceNumber + ", Acknowledgment Number: " + fAcknowledgmentNumber + //$NON-NLS-1$ //$NON-NLS-2$
                "\nHeader length: " + fDataOffset * TCPValues.BLOCK_SIZE + " bytes, Data length: " + length + //$NON-NLS-1$ //$NON-NLS-2$
                "\n" + flagString + "Window size value: " + fWindowSize + ", Urgent Pointer: " + String.format("%s%04x", "0x", fUrgentPointer) + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
                "\nChecksum: " + String.format("%s%04x", "0x", fChecksum) + "\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
        final Packet child = fChildPacket;
        if (child != null) {
            return string + child.toString();
        }
        return string;
    }

    /**
     * Getter method that returns the TCP Source Port.
     *
     * @return The source Port.
     */
    public int getSourcePort() {
        return fSourcePort;
    }

    /**
     * Getter method that returns the TCP Destination Port.
     *
     * @return The destination Port.
     */
    public int getDestinationPort() {
        return fDestinationPort;
    }

    /**
     * Getter method that returns the Sequence Number. The sequence number has a
     * dual role:
     * <ul>
     * <li>If the SYN flag is set (1), then this is the initial sequence number.
     * The sequence number of the actual first data byte and the acknowledged
     * number in the corresponding ACK are then this sequence number plus 1.</li>
     * <li>If the SYN flag is clear (0), then this is the accumulated sequence
     * number of the first data byte of this segment for the current session.</li>
     * </ul>
     *
     * Source: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
     *
     * @return The Sequence Number.
     */
    public long getSequenceNumber() {
        return fSequenceNumber;
    }

    /**
     * Getter method that returns the Acknowledgment Number.
     *
     * If the ACK flag is set then the value of this field is the next sequence
     * number that the receiver is expecting. This acknowledges receipt of all
     * prior bytes (if any). The first ACK sent by each end acknowledges the
     * other end's initial sequence number itself, but no data.
     *
     * Source: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
     *
     * @return The Acknowledgment Number.
     */
    public long getAcknowledgmentNumber() {
        return fAcknowledgmentNumber;
    }

    /**
     * Getter method that returns the size of the TCP header in 4 bytes data
     * block. The minimum size is 5 words and the maximum is 15 words.
     *
     * @return The Data Offset.
     */
    public int getDataOffset() {
        return fDataOffset;
    }

    /**
     * Getter method that returns the Reserved field. This field is for future
     * use and should always be zero. In this library, it is used as a mean to
     * verify the validity of a TCP packet.
     *
     * @return The Reserved Field.
     */
    public byte getReservedField() {
        return fReservedField;
    }

    /**
     * Getter method that returns the state of the NS flag.
     *
     * @return The state of the NS flag.
     */
    public boolean isNSFlagSet() {
        return fNSFlag;
    }

    /**
     * Getter method that returns the state of the CWR flag.
     *
     * @return The state of the CWR flag.
     */
    public boolean isCongestionWindowReducedFlagSet() {
        return fCWRFlag;
    }

    /**
     * Getter method that returns the state of the ECE flag.
     *
     * @return The state of the ECE flag.
     */
    public boolean isECNEchoFlagSet() {
        return fECEFlag;
    }

    /**
     * Getter method that returns the state of the URG flag.
     *
     * @return The state of the URG flag.
     */
    public boolean isUrgentFlagSet() {
        return fURGFlag;
    }

    /**
     * Getter method that returns the state of the ACK flag.
     *
     * @return The state of the ACK flag.
     */
    public boolean isAcknowledgeFlagSet() {
        return fACKFlag;
    }

    /**
     * Getter method that returns the state of the PSH flag.
     *
     * @return The state of the PSH flag.
     */
    public boolean isPushFlagSet() {
        return fPSHFlag;
    }

    /**
     * Getter method that returns the state of the RST flag.
     *
     * @return The state of the RST flag.
     */
    public boolean isResetFlagSet() {
        return fRSTFlag;
    }

    /**
     * Getter method that returns the state of the SYN flag.
     *
     * @return The state of the SYN flag.
     */
    public boolean isSynchronizationFlagSet() {
        return fSYNFlag;
    }

    /**
     * Getter method that returns the state of the FIN flag.
     *
     * @return The state of the FIN flag.
     */
    public boolean isFinalFlagSet() {
        return fFINFlag;
    }

    /**
     * Getter method that returns the size of the windows, in windows size unit
     * (by default, bytes), that the sender of this packet is willing to
     * receive.
     *
     * @return The Window Size.
     */
    public int getWindowSize() {
        return fWindowSize;
    }

    /**
     * Getter method that returns the checksum of this packet. This checksum may
     * be wrong if the packet is erroneous.
     *
     * @return The data and header checksum.
     */
    public int getChecksum() {
        return fChecksum;
    }

    /**
     * Getter method that returns the Urgent Pointer. If the URG flag is set,
     * this field is an offset from the sequence number indicating the last
     * urgent data byte.
     *
     * @return The Urgent Pointer.
     */
    public int getUrgentPointer() {
        return fUrgentPointer;
    }

    /**
     * Getter method that returns the options. This method returns null if no
     * options are present.
     *
     * @return The options of the packet.
     */
    public byte @Nullable [] getOptions() {
        byte[] options = fOptions;
        if (options == null) {
            return null;
        }
        return Arrays.copyOf(options, options.length);
    }

    @Override
    public boolean validate() {
        // Not yet implemented. ATM, we consider that all packets are valid.
        // This is the case for all packets.
        // TODO Implement it.
        return true;
    }

    @Override
    public TCPEndpoint getSourceEndpoint() {
        @Nullable
        TCPEndpoint endpoint = fSourceEndpoint;
        if (endpoint == null) {
            endpoint = new TCPEndpoint(this, true);
        }
        fSourceEndpoint = endpoint;
        return fSourceEndpoint;
    }

    @Override
    public TCPEndpoint getDestinationEndpoint() {
        @Nullable
        TCPEndpoint endpoint = fDestinationEndpoint;

        if (endpoint == null) {
            endpoint = new TCPEndpoint(this, false);
        }
        fDestinationEndpoint = endpoint;
        return fDestinationEndpoint;
    }

    @Override
    public Map<String, String> getFields() {
        Map<String, String> map = fFields;
        if (map == null) {
            Builder<String, String> builder = ImmutableMap.<@NonNull String, @NonNull String> builder()
                    .put("Source Port", String.valueOf(fSourcePort)) //$NON-NLS-1$
                    .put("Destination Port", String.valueOf(fDestinationPort)) //$NON-NLS-1$
                    .put("Sequence Number", String.valueOf(fSequenceNumber)) //$NON-NLS-1$
                    .put("Acknowledgement Number", String.valueOf(fAcknowledgmentNumber)) //$NON-NLS-1$
                    .put("Length", String.valueOf(fDataOffset * TCPValues.BLOCK_SIZE) + " bytes") //$NON-NLS-1$ //$NON-NLS-2$
                    .put("ECN-Nonce Flag", String.valueOf(fNSFlag)) //$NON-NLS-1$
                    .put("Congestion Window Reduced Flag", String.valueOf(fCWRFlag)) //$NON-NLS-1$
                    .put("ECN-Echo Flag", String.valueOf(fECEFlag)) //$NON-NLS-1$
                    .put("Urgent Flag", String.valueOf(fURGFlag)) //$NON-NLS-1$
                    .put("ACK Flag", String.valueOf(fACKFlag)) //$NON-NLS-1$
                    .put("PSH Flag", String.valueOf(fPSHFlag)) //$NON-NLS-1$
                    .put("RST Flag", String.valueOf(fRSTFlag)) //$NON-NLS-1$
                    .put("SYN Flag", String.valueOf(fSYNFlag)) //$NON-NLS-1$
                    .put("FIN Flag", String.valueOf(fFINFlag)) //$NON-NLS-1$
                    .put("Window Size Value", String.valueOf(fWindowSize)) //$NON-NLS-1$
                    .put("Checksum", String.format("%s%04x", "0x", fChecksum)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .put("Urgent Pointer", String.format("%s%04x", "0x", fUrgentPointer)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            byte[] options = fOptions;
            if (options == null) {
                builder.put("Options", EMPTY_STRING); //$NON-NLS-1$
            } else {
                builder.put("Options", ConversionHelper.bytesToHex(options, true)); //$NON-NLS-1$

            }
            fFields = builder.build();
            return fFields;
        }
        return map;
    }

    @Override
    public String getLocalSummaryString() {
        return "Src Port: " + fSourcePort + ", Dst Port: " + fDestinationPort + //$NON-NLS-1$ //$NON-NLS-2$
                ", Seq: " + fSequenceNumber + ", Ack: " + fAcknowledgmentNumber + //$NON-NLS-1$ //$NON-NLS-2$
                ", Len: " + (fDataOffset * TCPValues.BLOCK_SIZE); //$NON-NLS-1$
    }

    @Override
    protected String getSignificationString() {
        StringBuilder sb = new StringBuilder();
        sb.append(fSourcePort)
                .append(" > ") //$NON-NLS-1$
                .append(fDestinationPort);

        if (!(generateFlagString().equals(EMPTY_STRING))) {
            sb.append(' ')
                    .append('[')
                    .append(generateFlagString())
                    .append(']');
        }
        sb.append(" Seq=") //$NON-NLS-1$
        .append(fSequenceNumber);

        if (fACKFlag) {
            sb.append(" Ack=") //$NON-NLS-1$
            .append(fAcknowledgmentNumber);
        }

        sb.append(" Len=") //$NON-NLS-1$
        .append((fDataOffset * TCPValues.BLOCK_SIZE));

        return NonNullUtils.nullToEmptyString(sb);    }

    private String generateFlagString() {
        StringBuilder sb = new StringBuilder();
        boolean start = true;

        if (fSYNFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("SYN"); //$NON-NLS-1$
            start = false;
        }
        if (fACKFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("ACK"); //$NON-NLS-1$
            start = false;
        }
        if (fFINFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("FIN"); //$NON-NLS-1$
            start = false;
        }
        if (fRSTFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("RST"); //$NON-NLS-1$
            start = false;
        }
        if (fPSHFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("PSH"); //$NON-NLS-1$
            start = false;
        }
        if (fURGFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("URG"); //$NON-NLS-1$
            start = false;
        }
        if (fNSFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("NS"); //$NON-NLS-1$
            start = false;
        }
        if (fCWRFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("CWR"); //$NON-NLS-1$
            start = false;
        }
        if (fECEFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("ECE"); //$NON-NLS-1$
            start = false;
        }
        return NonNullUtils.nullToEmptyString(sb);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (fACKFlag ? 1231 : 1237);
        result = prime * result + (int) (fAcknowledgmentNumber ^ (fAcknowledgmentNumber >>> 32));
        result = prime * result + (fCWRFlag ? 1231 : 1237);
        result = prime * result + fChecksum;
        final Packet child = fChildPacket;
        if (child != null) {
            result = prime * result + child.hashCode();
        } else {
            result = prime * result;
        }
        result = prime * result + fDataOffset;
        result = prime * result + fDestinationPort;
        result = prime * result + (fECEFlag ? 1231 : 1237);
        result = prime * result + (fFINFlag ? 1231 : 1237);
        result = prime * result + (fNSFlag ? 1231 : 1237);
        result = prime * result + Arrays.hashCode(fOptions);
        result = prime * result + (fPSHFlag ? 1231 : 1237);
        if (child == null) {
            result = prime * result + payloadHashCode(fPayload);
        }
        result = prime * result + (fRSTFlag ? 1231 : 1237);
        result = prime * result + fReservedField;
        result = prime * result + (fSYNFlag ? 1231 : 1237);
        result = prime * result + (int) (fSequenceNumber ^ (fSequenceNumber >>> 32));
        result = prime * result + fSourcePort;
        result = prime * result + (fURGFlag ? 1231 : 1237);
        result = prime * result + fUrgentPointer;
        result = prime * result + fWindowSize;
        return result;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        TCPPacket other = (TCPPacket) obj;
        if (fACKFlag != other.fACKFlag) {
            return false;
        }
        if (fAcknowledgmentNumber != other.fAcknowledgmentNumber) {
            return false;
        }
        if (fCWRFlag != other.fCWRFlag) {
            return false;
        }
        if (fChecksum != other.fChecksum) {
            return false;
        }
        if(!Objects.equals(fChildPacket, other.fChildPacket)){
            return false;
        }
        if (fDataOffset != other.fDataOffset) {
            return false;
        }
        if (fDestinationPort != other.fDestinationPort) {
            return false;
        }
        if (fECEFlag != other.fECEFlag) {
            return false;
        }
        if (fFINFlag != other.fFINFlag) {
            return false;
        }
        if (fNSFlag != other.fNSFlag) {
            return false;
        }
        if (!Arrays.equals(fOptions, other.fOptions)) {
            return false;
        }
        if (fPSHFlag != other.fPSHFlag) {
            return false;
        }
        if (fChildPacket == null && !payloadEquals(fPayload, other.fPayload)) {
            return false;
        }
        if (fRSTFlag != other.fRSTFlag) {
            return false;
        }
        if (fReservedField != other.fReservedField) {
            return false;
        }
        if (fSYNFlag != other.fSYNFlag) {
            return false;
        }
        if (fSequenceNumber != other.fSequenceNumber) {
            return false;
        }
        if (fSourcePort != other.fSourcePort) {
            return false;
        }
        if (fURGFlag != other.fURGFlag) {
            return false;
        }
        if (fUrgentPointer != other.fUrgentPointer) {
            return false;
        }
        return (fWindowSize == other.fWindowSize) ;
    }

}
