/**
 *    Copyright 2011, Big Switch Networks, Inc.
 *    Originally created by David Erickson, Stanford University
 *
 *    Licensed under the Apache License, Version 2.0 (the "License"); you may
 *    not use this file except in compliance with the License. You may obtain
 *    a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *    License for the specific language governing permissions and limitations
 *    under the License.
 **/

package net.floodlightcontroller.packet;

import java.nio.ByteBuffer;

import org.projectfloodlight.openflow.types.ArpOpcode;
import org.projectfloodlight.openflow.types.IPv4Address;
import org.projectfloodlight.openflow.types.MacAddress;

/**
 *
 * @author David Erickson (daviderickson@cs.stanford.edu)
 */
public class ARP extends BasePacket {
	public static short HW_TYPE_ETHERNET = 0x1;

	public static short PROTO_TYPE_IP = 0x800;

	public static ArpOpcode OP_REQUEST = ArpOpcode.REQUEST;
	public static ArpOpcode OP_REPLY = ArpOpcode.REPLY;
	public static ArpOpcode OP_RARP_REQUEST = ArpOpcode.REQUEST_REVERSE;
	public static ArpOpcode OP_RARP_REPLY = ArpOpcode.REPLY_REVERSE;

	protected short hardwareType;
	protected short protocolType;
	protected byte hardwareAddressLength;
	protected byte protocolAddressLength;
	protected ArpOpcode opCode;
	protected MacAddress senderHardwareAddress;
	protected IPv4Address senderProtocolAddress;
	protected MacAddress targetHardwareAddress;
	protected IPv4Address targetProtocolAddress;

	/**
	 * @return the hardwareType
	 */
	public short getHardwareType() {
		return hardwareType;
	}

	/**
	 * @param hardwareType the hardwareType to set
	 */
	public ARP setHardwareType(short hardwareType) {
		this.hardwareType = hardwareType;
		return this;
	}

	/**
	 * @return the protocolType
	 */
	public short getProtocolType() {
		return protocolType;
	}

	/**
	 * @param protocolType the protocolType to set
	 */
	public ARP setProtocolType(short protocolType) {
		this.protocolType = protocolType;
		return this;
	}

	/**
	 * @return the hardwareAddressLength
	 */
	public byte getHardwareAddressLength() {
		return hardwareAddressLength;
	}

	/**
	 * @param hardwareAddressLength the hardwareAddressLength to set
	 */
	public ARP setHardwareAddressLength(byte hardwareAddressLength) {
		this.hardwareAddressLength = hardwareAddressLength;
		return this;
	}

	/**
	 * @return the protocolAddressLength
	 */
	public byte getProtocolAddressLength() {
		return protocolAddressLength;
	}

	/**
	 * @param protocolAddressLength the protocolAddressLength to set
	 */
	public ARP setProtocolAddressLength(byte protocolAddressLength) {
		this.protocolAddressLength = protocolAddressLength;
		return this;
	}

	/**
	 * @return the opCode
	 */
	public ArpOpcode getOpCode() {
		return opCode;
	}

	/**
	 * @param opCode the opCode to set
	 */
	public ARP setOpCode(ArpOpcode opCode) {
		this.opCode = opCode;
		return this;
	}

	/**
	 * @return the senderHardwareAddress
	 */
	public MacAddress getSenderHardwareAddress() {
		return senderHardwareAddress;
	}

	/**
	 * @param senderHardwareAddress the senderHardwareAddress to set
	 */
	public ARP setSenderHardwareAddress(MacAddress senderHardwareAddress) {
		this.senderHardwareAddress = senderHardwareAddress;
		return this;
	}

	/**
	 * @return the senderProtocolAddress
	 */
	public IPv4Address getSenderProtocolAddress() {
		return senderProtocolAddress;
	}

	/**
	 * @param senderProtocolAddress the senderProtocolAddress to set
	 */
	public ARP setSenderProtocolAddress(IPv4Address senderProtocolAddress) {
		this.senderProtocolAddress = senderProtocolAddress;
		return this;
	}

	/**
	 * @return the targetHardwareAddress
	 */
	public MacAddress getTargetHardwareAddress() {
		return targetHardwareAddress;
	}

	/**
	 * @param targetHardwareAddress the targetHardwareAddress to set
	 */
	public ARP setTargetHardwareAddress(MacAddress targetHardwareAddress) {
		this.targetHardwareAddress = targetHardwareAddress;
		return this;
	}

	/**
	 * @return the targetProtocolAddress
	 */
	public IPv4Address getTargetProtocolAddress() {
		return targetProtocolAddress;
	}

	/**
	 * @return True if gratuitous ARP (SPA = TPA), false otherwise
	 */
	public boolean isGratuitous() {
		assert(senderProtocolAddress.getLength() == targetProtocolAddress.getLength());

		return senderProtocolAddress.equals(targetProtocolAddress);
	}

	/**
	 * @param targetProtocolAddress the targetProtocolAddress to set
	 */
	public ARP setTargetProtocolAddress(IPv4Address targetProtocolAddress) {
		this.targetProtocolAddress = targetProtocolAddress;
		return this;
	}

	@Override
	public byte[] serialize() {
		int length = 8 + (2 * (0xff & this.hardwareAddressLength))
				+ (2 * (0xff & this.protocolAddressLength));
		byte[] data = new byte[length];
		ByteBuffer bb = ByteBuffer.wrap(data);
		bb.putShort(this.hardwareType);
		bb.putShort(this.protocolType);
		bb.put(this.hardwareAddressLength);
		bb.put(this.protocolAddressLength);
		bb.putShort((short) this.opCode.getOpcode());
		bb.put(this.senderHardwareAddress.getBytes());
		bb.put(this.senderProtocolAddress.getBytes());
		bb.put(this.targetHardwareAddress.getBytes());
		bb.put(this.targetProtocolAddress.getBytes());
		return data;
	}

	@Override
	public IPacket deserialize(byte[] data, int offset, int length)
			throws PacketParsingException {
		ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
		this.hardwareType = bb.getShort();
		this.protocolType = bb.getShort();
		this.hardwareAddressLength = bb.get();
		this.protocolAddressLength = bb.get();
		if (this.hardwareAddressLength != 6) {
			String msg = "Incorrect ARP hardware address length: " +
					hardwareAddressLength;
			throw new PacketParsingException(msg);
		}
		if (this.protocolAddressLength != 4) {
			String msg = "Incorrect ARP protocol address length: " +
					protocolAddressLength;
			throw new PacketParsingException(msg);
		}
		this.opCode = ArpOpcode.of(bb.getShort());

		byte[] tmpMac = new byte[0xff & this.hardwareAddressLength];
		byte[] tmpIp = new byte[0xff & this.protocolAddressLength];

		bb.get(tmpMac, 0, this.hardwareAddressLength);
		this.senderHardwareAddress = MacAddress.of(tmpMac);
		bb.get(tmpIp, 0, this.protocolAddressLength);
		this.senderProtocolAddress = IPv4Address.of(tmpIp);

		bb.get(tmpMac, 0, this.hardwareAddressLength);
		this.targetHardwareAddress = MacAddress.of(tmpMac);
		bb.get(tmpIp, 0, this.protocolAddressLength);
		this.targetProtocolAddress = IPv4Address.of(tmpIp);

		return this;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = super.hashCode();
		result = prime * result + hardwareAddressLength;
		result = prime * result + hardwareType;
		result = prime * result + ((opCode == null) ? 0 : opCode.hashCode());
		result = prime * result + protocolAddressLength;
		result = prime * result + protocolType;
		result = prime
				* result
				+ ((senderHardwareAddress == null) ? 0 : senderHardwareAddress
						.hashCode());
		result = prime
				* result
				+ ((senderProtocolAddress == null) ? 0 : senderProtocolAddress
						.hashCode());
		result = prime
				* result
				+ ((targetHardwareAddress == null) ? 0 : targetHardwareAddress
						.hashCode());
		result = prime
				* result
				+ ((targetProtocolAddress == null) ? 0 : targetProtocolAddress
						.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		ARP other = (ARP) obj;
		if (hardwareAddressLength != other.hardwareAddressLength)
			return false;
		if (hardwareType != other.hardwareType)
			return false;
		if (opCode == null) {
			if (other.opCode != null)
				return false;
		} else if (!opCode.equals(other.opCode))
			return false;
		if (protocolAddressLength != other.protocolAddressLength)
			return false;
		if (protocolType != other.protocolType)
			return false;
		if (senderHardwareAddress == null) {
			if (other.senderHardwareAddress != null)
				return false;
		} else if (!senderHardwareAddress.equals(other.senderHardwareAddress))
			return false;
		if (senderProtocolAddress == null) {
			if (other.senderProtocolAddress != null)
				return false;
		} else if (!senderProtocolAddress.equals(other.senderProtocolAddress))
			return false;
		if (targetHardwareAddress == null) {
			if (other.targetHardwareAddress != null)
				return false;
		} else if (!targetHardwareAddress.equals(other.targetHardwareAddress))
			return false;
		if (targetProtocolAddress == null) {
			if (other.targetProtocolAddress != null)
				return false;
		} else if (!targetProtocolAddress.equals(other.targetProtocolAddress))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "ARP [hardwareType=" + hardwareType + ", protocolType="
				+ protocolType + ", hardwareAddressLength="
				+ hardwareAddressLength + ", protocolAddressLength="
				+ protocolAddressLength + ", opCode=" + opCode
				+ ", senderHardwareAddress=" + senderHardwareAddress
				+ ", senderProtocolAddress=" + senderProtocolAddress
				+ ", targetHardwareAddress=" + targetHardwareAddress
				+ ", targetProtocolAddress=" + targetProtocolAddress + "]";
	}

}
