/*******************************************************************************
 * 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.ipv4;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
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.tcp.TCPPacket;
import org.eclipse.tracecompass.internal.pcap.core.protocol.udp.UDPPacket;
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 org.eclipse.tracecompass.internal.pcap.core.util.IPProtocolNumberHelper;

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

/**
 * Class that represents an Ethernet II packet.
 *
 * @author Vincent Perot
 */
public class IPv4Packet extends Packet {

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

    private final int fVersion;
    private final int fInternetHeaderLength; // in 4 bytes blocks
    private final int fDSCP;
    private final int fExplicitCongestionNotification;
    private final int fTotalLength; // in bytes
    private final int fIdentification;
    private final boolean fReservedFlag;
    private final boolean fDontFragmentFlag;
    private final boolean fMoreFragmentFlag;
    private final int fFragmentOffset;
    private final int fTimeToLive;
    private final int fIpDatagramProtocol;
    private final int fHeaderChecksum;
    private final Inet4Address fSourceIpAddress;
    private final Inet4Address fDestinationIpAddress;
    private final byte @Nullable [] fOptions;

    private @Nullable IPv4Endpoint fSourceEndpoint;
    private @Nullable IPv4Endpoint fDestinationEndpoint;

    private @Nullable Map<String, String> fFields;

    // TODO Interpret options. See
    // http://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml

    /**
     * Constructor of the IPv4 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 IPv4Packet(PcapFile file, @Nullable Packet parent, ByteBuffer packet) throws BadPacketException {
        super(file, parent, PcapProtocol.IPV4);

        // 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);

        byte storage = packet.get();
        fVersion = ((storage & 0xF0) >> 4) & 0x000000FF;
        fInternetHeaderLength = storage & 0x0F;

        storage = packet.get();
        fDSCP = ((storage & 0b11111100) >> 2) & 0x000000FF;
        fExplicitCongestionNotification = storage & 0b00000011;

        fTotalLength = ConversionHelper.unsignedShortToInt(packet.getShort());
        fIdentification = ConversionHelper.unsignedShortToInt(packet.getShort());

        storage = packet.get();
        fReservedFlag = isBitSet(storage, 7);
        fDontFragmentFlag = isBitSet(storage, 6);
        fMoreFragmentFlag = isBitSet(storage, 5);
        int msb = ((storage & 0b00011111) << 8);
        int lsb = ConversionHelper.unsignedByteToInt(packet.get());
        fFragmentOffset = msb + lsb;

        fTimeToLive = ConversionHelper.unsignedByteToInt(packet.get());
        fIpDatagramProtocol = ConversionHelper.unsignedByteToInt(packet.get());
        fHeaderChecksum = ConversionHelper.unsignedShortToInt(packet.getShort());

        byte[] source = new byte[IPv4Values.IP_ADDRESS_SIZE];
        byte[] destination = new byte[IPv4Values.IP_ADDRESS_SIZE];
        packet.get(source);
        packet.get(destination);

        try {
            fSourceIpAddress = (Inet4Address) checkNotNull(InetAddress.getByAddress(source));
            fDestinationIpAddress = (Inet4Address) checkNotNull(InetAddress.getByAddress(destination));
        } catch (UnknownHostException e) {
            throw new BadPacketException("The IP Address size is not valid!"); //$NON-NLS-1$
        }

        // Get options if there are any
        if (fInternetHeaderLength > IPv4Values.DEFAULT_HEADER_LENGTH) {
            fOptions = new byte[(fInternetHeaderLength - IPv4Values.DEFAULT_HEADER_LENGTH) * IPv4Values.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
        fChildPacket = findChildPacket();

    }

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

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

    /**
     * {@inheritDoc}
     *
     * See http://en.wikipedia.org/wiki/List_of_IP_protocol_numbers
     */
    @Override
    protected @Nullable Packet findChildPacket() throws BadPacketException {
        // TODO Implement more protocols
        ByteBuffer payload = fPayload;
        if (payload == null) {
            return null;
        }

        switch (fIpDatagramProtocol) {
        case IPProtocolNumberHelper.PROTOCOL_NUMBER_TCP:
            return new TCPPacket(getPcapFile(), this, payload);
        case IPProtocolNumberHelper.PROTOCOL_NUMBER_UDP:
            return new UDPPacket(getPcapFile(), this, payload);
        default:
            return new UnknownPacket(getPcapFile(), this, payload);
        }

    }

    @Override
    public String toString() {
        // Generate flagString
        // This is very ugly.
        String flagString = null;

        if (fReservedFlag && fDontFragmentFlag && fMoreFragmentFlag) { // 111
            flagString = "Flags: 0x07 (Invalid)"; //$NON-NLS-1$
        } else if (fReservedFlag && fDontFragmentFlag && !fMoreFragmentFlag) { // 110
            flagString = "Flags: 0x06 (Invalid)"; //$NON-NLS-1$
        } else if (fReservedFlag && !fDontFragmentFlag && fMoreFragmentFlag) { // 101
            flagString = "Flags: 0x05 (Invalid)"; //$NON-NLS-1$
        } else if (fReservedFlag && !fDontFragmentFlag && !fMoreFragmentFlag) { // 100
            flagString = "Flags: 0x04 (Invalid)"; //$NON-NLS-1$
        } else if (!fReservedFlag && fDontFragmentFlag && fMoreFragmentFlag) { // 011
            flagString = "Flags: 0x03 (Invalid)"; //$NON-NLS-1$
        } else if (!fReservedFlag && fDontFragmentFlag && !fMoreFragmentFlag) { // 010
            flagString = "Flags: 0x02 (Don't fragment)"; //$NON-NLS-1$
        } else if (!fReservedFlag && !fDontFragmentFlag && fMoreFragmentFlag) { // 001
            flagString = "Flags: 0x01 (More fragments)"; //$NON-NLS-1$
        } else if (!fReservedFlag && !fDontFragmentFlag && !fMoreFragmentFlag) { // 000
            flagString = "Flags: 0x00 (Don't have more fragments)"; //$NON-NLS-1$
        }

        flagString += ", Fragment Offset: " + fFragmentOffset; //$NON-NLS-1$

        // Generate checksum string
        // TODO calculate the expected checksum from packet
        String checksumString = "Header Checksum: " + String.format("%s%04x", "0x", fHeaderChecksum); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

        String string = getProtocol().getName() + ", Source: " + fSourceIpAddress.getHostAddress() + ", Destination: " + fDestinationIpAddress.getHostAddress() + //$NON-NLS-1$ //$NON-NLS-2$
                "\nVersion: " + fVersion + ", Identification: " + String.format("%s%04x", "0x", fIdentification) + ", Header Length: " + getHeaderLength() + " bytes, Total Length: " + getTotalLength() + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
                " bytes\nDifferentiated Services Code Point: " + String.format("%s%02x", "0x", fDSCP) + "; Explicit Congestion Notification: " + String.format("%s%02x", "0x", fExplicitCongestionNotification) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
                + "\n" + flagString + "\nTime to live: " + fTimeToLive + //$NON-NLS-1$ //$NON-NLS-2$
                "\nProtocol: " + fIpDatagramProtocol + "\n" //$NON-NLS-1$ //$NON-NLS-2$
                + checksumString + "\n"; //$NON-NLS-1$
        final Packet child = fChildPacket;
        if (child != null) {
            return string + child.toString();
        }
        return string;
    }

    /**
     * Getter method that returns the version of the IP protocol used. This
     * should always be set to 4 as IPv6 has its own class.
     *
     * @return The version of the IP used.
     */
    public int getVersion() {
        return fVersion;
    }

    /**
     * Getter method that returns the header length in bytes. In the IPv4
     * packet, this is specified in 4-bytes data block. By default, this method
     * returns 20 if there are no options present. Otherwise, it will return a
     * higher number.
     *
     * @return The header length in bytes.
     */
    public int getHeaderLength() {
        return fInternetHeaderLength * IPv4Values.BLOCK_SIZE;
    }

    /**
     * Getter method that returns the Differentiated Services Code Point (a.k.a.
     * the Type of Service). This is useful for some technologies that require
     * real-time data exchange.
     *
     * @return The DSCP
     */
    public int getDSCP() {
        return fDSCP;
    }

    /**
     * Getter method that returns the Explicit Congestion Notification (ECN).
     * This allows end-to-end communication without dropping packets.
     *
     * @return The ECN.
     */
    public int getExplicitCongestionNotification() {
        return fExplicitCongestionNotification;
    }

    /**
     * Getter method to retrieve the length of the entire packet, in bytes. This
     * number is according to the packet, and might not be true if the packet is
     * erroneous.
     *
     * @return The total length (packet and payload) in bytes.
     */
    public int getTotalLength() {
        return fTotalLength;
    }

    /**
     * Getter method to retrieve the Identification. This is a field that is
     * used to uniquely identify the packets, thus allowing the reconstruction
     * of fragmented IP packets.
     *
     * @return The packet identification.
     */
    public int getIdentification() {
        return fIdentification;
    }

    /**
     * Getter method that returns the state of the Reserved flag. This must
     * always be zero.
     *
     * @return The state of the Reserved flag.
     */
    public boolean getReservedFlag() {
        return fReservedFlag;
    }

    /**
     * Getter method that indicates if the packet can be fragmented or not.
     *
     * @return Whether the packet can be fragmented or not.
     */
    public boolean getDontFragmentFlag() {
        return fDontFragmentFlag;
    }

    /**
     * Getter method that indicates if the packet has more fragments or not.
     *
     * @return Whether the packet has more fragments or not.
     */
    public boolean getHasMoreFragment() {
        return fMoreFragmentFlag;
    }

    /**
     * Getter method that specify the offset of a particular fragment relative
     * to the original unfragmented packet, in 8-bytes blocks. *
     *
     * @return The fragment offset.
     */
    public int getFragmentOffset() {
        return fFragmentOffset;
    }

    /**
     * Getter method that returns the time to live in seconds. In practice, this
     * is a hop count. This is used to prevent packets from persisting.
     *
     * @return The time left to live for the packet.
     */
    public int getTimeToLive() {
        return fTimeToLive;
    }

    /**
     * Getter method that returns the encapsulated protocol.
     *
     * See http://en.wikipedia.org/wiki/List_of_IP_protocol_numbers
     *
     * @return The encapsulated protocol.
     */
    public int getIpDatagramProtocol() {
        return fIpDatagramProtocol;
    }

    /**
     * Getter method that returns the checksum, according to the packet. This
     * checksum might be wrong if the packet is erroneous.
     *
     * @return The header checksum.
     */
    public int getHeaderChecksum() {
        return fHeaderChecksum;
    }

    /**
     * Getter method that returns the source IP address.
     *
     * @return The source IP address, as a byte array in big-endian.
     */
    public Inet4Address getSourceIpAddress() {
        return fSourceIpAddress;
    }

    /**
     * Getter method that returns the destination IP address.
     *
     * @return The destination IP address, as a byte array in big-endian.
     */
    public Inet4Address getDestinationIpAddress() {
        return fDestinationIpAddress;
    }

    /**
     * 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() {
        final 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 IPv4Endpoint getSourceEndpoint() {
        @Nullable
        IPv4Endpoint endpoint = fSourceEndpoint;
        if (endpoint == null) {
            endpoint = new IPv4Endpoint(this, true);
        }
        fSourceEndpoint = endpoint;
        return fSourceEndpoint;
    }

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

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

    @Override
    public Map<String, String> getFields() {
        Map<String, String> map = fFields;
        if (map == null) {
            Builder<@NonNull String, @NonNull String> builder = ImmutableMap.<@NonNull String, @NonNull String> builder()
                    .put("Version", String.valueOf(fVersion)) //$NON-NLS-1$
                    .put("Header Length", String.valueOf(getHeaderLength()) + " bytes") //$NON-NLS-1$ //$NON-NLS-2$
                    .put("Differentiated Services Field", String.format("%s%02x", "0x", fDSCP)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .put("Explicit Congestion Notification", String.format("%s%02x", "0x", fExplicitCongestionNotification)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .put("Total Length", String.valueOf(fTotalLength) + " bytes") //$NON-NLS-1$ //$NON-NLS-2$
                    .put("Identification", String.format("%s%04x", "0x", fIdentification)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .put("Don't Fragment Flag", String.valueOf(fDontFragmentFlag)) //$NON-NLS-1$
                    .put("More Fragment Flag", String.valueOf(fMoreFragmentFlag)) //$NON-NLS-1$
                    .put("Fragment Offset", String.valueOf(fFragmentOffset)) //$NON-NLS-1$
                    .put("Time to live", String.valueOf(fTimeToLive)) //$NON-NLS-1$
                    .put("Protocol", IPProtocolNumberHelper.toString(fIpDatagramProtocol) + " (" + String.valueOf(fIpDatagramProtocol) + ")") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .put("Checksum", String.format("%s%04x", "0x", fHeaderChecksum)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .put("Source IP Address", nullToEmptyString(fSourceIpAddress.getHostAddress())) //$NON-NLS-1$
                    .put("Destination IP Address", nullToEmptyString(fDestinationIpAddress.getHostAddress())); //$NON-NLS-1$
            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: " + fSourceIpAddress.getHostAddress() + " , Dst: " + fDestinationIpAddress.getHostAddress(); //$NON-NLS-1$ //$NON-NLS-2$
    }

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

        String flags = generateFlagString();
        if (!(flags.equals(""))) { //$NON-NLS-1$
            sb.append(' ')
                    .append('[')
                    .append(flags)
                    .append(']');
        }
        sb.append(" Id=") //$NON-NLS-1$
        .append(fIdentification);

        final ByteBuffer payload = fPayload;
        if (payload != null) {
            sb.append(" Len=") //$NON-NLS-1$
            .append(payload.limit());
        } else {
            sb.append(" Len=0"); //$NON-NLS-1$
        }
        return NonNullUtils.nullToEmptyString(sb);
    }

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

        if (fDontFragmentFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("DF"); //$NON-NLS-1$
            start = false;
        }
        if (fMoreFragmentFlag) {
            if (!start) {
                sb.append(", "); //$NON-NLS-1$
            }
            sb.append("MF"); //$NON-NLS-1$
            start = false;
        }
        return NonNullUtils.nullToEmptyString(sb);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        final Packet child = fChildPacket;
        if (child != null) {
            result = prime * result + child.hashCode();
        } else {
            result = prime * result;
        }
        result = prime * result + fDSCP;
        result = prime * result + fDestinationIpAddress.hashCode();
        result = prime * result + (fDontFragmentFlag ? 1231 : 1237);
        result = prime * result + fExplicitCongestionNotification;
        result = prime * result + fFragmentOffset;
        result = prime * result + fHeaderChecksum;
        result = prime * result + fIdentification;
        result = prime * result + fInternetHeaderLength;
        result = prime * result + fIpDatagramProtocol;
        result = prime * result + (fMoreFragmentFlag ? 1231 : 1237);
        result = prime * result + Arrays.hashCode(fOptions);
        if (child == null) {
            result = prime * result + payloadHashCode(fPayload);
        }
        result = prime * result + (fReservedFlag ? 1231 : 1237);
        result = prime * result + fSourceIpAddress.hashCode();
        result = prime * result + fTimeToLive;
        result = prime * result + fTotalLength;
        result = prime * result + fVersion;
        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;
        }
        IPv4Packet other = (IPv4Packet) obj;
        if (!Objects.equals(fChildPacket, other.fChildPacket)) {
            return false;
        }
        if (fDSCP != other.fDSCP) {
            return false;
        }
        if (!(fDestinationIpAddress.equals(other.fDestinationIpAddress))) {
            return false;
        }
        if (fDontFragmentFlag != other.fDontFragmentFlag) {
            return false;
        }
        if (fExplicitCongestionNotification != other.fExplicitCongestionNotification) {
            return false;
        }
        if (fFragmentOffset != other.fFragmentOffset) {
            return false;
        }
        if (fHeaderChecksum != other.fHeaderChecksum) {
            return false;
        }
        if (fIdentification != other.fIdentification) {
            return false;
        }
        if (fInternetHeaderLength != other.fInternetHeaderLength) {
            return false;
        }
        if (fIpDatagramProtocol != other.fIpDatagramProtocol) {
            return false;
        }
        if (fMoreFragmentFlag != other.fMoreFragmentFlag) {
            return false;
        }
        if (!Arrays.equals(fOptions, other.fOptions)) {
            return false;
        }
        if (fChildPacket == null && !payloadEquals(fPayload, other.fPayload)) {
            return false;
        }
        if (fReservedFlag != other.fReservedFlag) {
            return false;
        }
        if (!(fSourceIpAddress.equals(other.fSourceIpAddress))) {
            return false;
        }
        if (fTimeToLive != other.fTimeToLive) {
            return false;
        }
        if (fTotalLength != other.fTotalLength) {
            return false;
        }
        return (fVersion == other.fVersion);
    }
}
