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

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

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.Nullable;
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.ipv4.IPv4Packet;
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.EthertypeHelper;

import com.google.common.collect.ImmutableMap;

/**
 * Class that represents an Ethernet II packet. This should be called an
 * Ethernet frame, but in order to keep the nomenclature consistent, this is
 * called a packet.
 *
 * @author Vincent Perot
 */
public class EthernetIIPacket extends Packet {

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

    /* We store MAC addresses as byte arrays since
     * there is no standard java class to store them. */
    private final byte[] fSourceMacAddress;
    private final byte[] fDestinationMacAddress;

    private final int fType;

    private @Nullable EthernetIIEndpoint fSourceEndpoint;
    private @Nullable EthernetIIEndpoint fDestinationEndpoint;

    private @Nullable Map<String, String> fFields;

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

        if (packet.limit() <= EthernetIIValues.ETHERNET_II_MIN_SIZE) {
            throw new BadPacketException("An Ethernet II packet can't be smaller than 14 bytes."); //$NON-NLS-1$
        }

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

        fFields = null;

        fDestinationMacAddress = new byte[EthernetIIValues.MAC_ADDRESS_SIZE];
        fSourceMacAddress = new byte[EthernetIIValues.MAC_ADDRESS_SIZE];
        packet.order(ByteOrder.BIG_ENDIAN);
        packet.position(0);
        packet.get(fDestinationMacAddress);
        packet.get(fSourceMacAddress);
        fType = ConversionHelper.unsignedShortToInt(packet.getShort());

        // Get payload if it exists.
        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;
    }

    /**
     * Getter method for the source MAC Address.
     *
     * @return The source MAC address.
     */
    public byte[] getSourceMacAddress() {
        return checkNotNull(Arrays.copyOf(fSourceMacAddress, fSourceMacAddress.length));
    }

    /**
     * Getter method for the destination MAC Address.
     *
     * @return The destination MAC address.
     */
    public byte[] getDestinationMacAddress() {
        return checkNotNull(Arrays.copyOf(fDestinationMacAddress, fDestinationMacAddress.length));
    }

    /**
     * Getter method for Ethertype. See
     * http://standards.ieee.org/develop/regauth/ethertype/eth.txt
     *
     * @return The Ethertype. This is used to determine the child packet..
     */
    public int getEthertype() {
        return fType;
    }

    @Override
    protected @Nullable Packet findChildPacket() throws BadPacketException {
        // TODO Add more protocols.
        ByteBuffer payload = fPayload;
        if (payload == null) {
            return null;
        }
        switch (fType) {
        case EthertypeHelper.ETHERTYPE_IPV4:
            return new IPv4Packet(getPcapFile(), this, payload);
        default:
            return new UnknownPacket(getPcapFile(), this, payload);
        }
    }

    @Override
    public String toString() {
        String string = getProtocol().getName() + ", Source: " + ConversionHelper.toMacAddress(fSourceMacAddress) + //$NON-NLS-1$
                ", Destination: " + ConversionHelper.toMacAddress(fDestinationMacAddress) + ", Type: " + //$NON-NLS-1$ //$NON-NLS-2$
                EthertypeHelper.toEtherType(fType) + "\n"; //$NON-NLS-1$
        final Packet child = fChildPacket;
        if (child != null) {
            return string + child.toString();
        }
        return string;
    }

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

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

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

    @Override
    public Map<String, String> getFields() {
        Map<String, String> map = fFields;
        if (map == null) {
            ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
            builder.put("Source MAC Address", ConversionHelper.toMacAddress(fSourceMacAddress)); //$NON-NLS-1$
            builder.put("Destination MAC Address", ConversionHelper.toMacAddress(fDestinationMacAddress)); //$NON-NLS-1$
            builder.put("Ethertype", String.valueOf(EthertypeHelper.toEtherType(fType))); //$NON-NLS-1$

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

    @Override
    public String getLocalSummaryString() {
        return "Src: " + ConversionHelper.toMacAddress(fSourceMacAddress) + " , Dst: " + ConversionHelper.toMacAddress(fDestinationMacAddress); //$NON-NLS-1$ //$NON-NLS-2$
    }

    @Override
    protected String getSignificationString() {
        return "Source MAC: " + ConversionHelper.toMacAddress(fSourceMacAddress) + " , Destination MAC: " + ConversionHelper.toMacAddress(fDestinationMacAddress); //$NON-NLS-1$ //$NON-NLS-2$
    }

    @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 + Arrays.hashCode(fDestinationMacAddress);
        if (child == null) {
            result = prime * result + payloadHashCode(fPayload);
        }
        result = prime * result + Arrays.hashCode(fSourceMacAddress);
        result = prime * result + fType;
        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;
        }
        EthernetIIPacket other = (EthernetIIPacket) obj;
        if(!Objects.equals(fChildPacket, other.fChildPacket)) {
            return false;
        }
        if (!Arrays.equals(fDestinationMacAddress, other.fDestinationMacAddress)) {
            return false;
        }
        if (fChildPacket == null && !payloadEquals(fPayload, other.fPayload)) {
            return false;
        }
        if (!Arrays.equals(fSourceMacAddress, other.fSourceMacAddress)) {
            return false;
        }
        return (fType == other.fType);
    }

}
