// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 flows

import (
	"bytes"
	"encoding/base64"
	"encoding/binary"
	"net"
)

type FlowID struct {
	rawFlowID
	flow Flow // remember associated flow for faster lookup
}

type rawFlowID struct {
	flowID []byte
	flowIDMeta
	dir flowDirection
}

type flowIDMeta struct {
	flags FlowIDFlag

	// offsets into flowID
	offEth        uint8
	offOutterVlan uint8
	offVlan       uint8
	offOutterIPv4 uint8
	offIPv4       uint8
	offOutterIPv6 uint8
	offIPv6       uint8
	offICMPv4     uint8
	offICMPv6     uint8
	offUDP        uint8
	offTCP        uint8
	offID         uint8

	cntEth  uint8
	cntVlan uint8
	cntIP   uint8
}

type FlowIDFlag uint16

const (
	EthFlow FlowIDFlag = (1 << iota)
	OutterVlanFlow
	VLanFlow
	OutterIPv4Flow
	IPv4Flow
	OutterIPv6Flow
	IPv6Flow
	ICMPv4Flow
	ICMPv6Flow
	UDPFlow
	TCPFlow
	ConnectionID
)

const (
	SizeEthAddr    = 6
	SizeVlan       = 2
	SizeIPv4Addr   = 4
	SizeIPv6Addr   = 16
	SizeICMPID     = 2
	SizePortNumber = 2

	SizeEthFlowID    = 2 * SizeEthAddr    // source + dest mac address
	SizeVlanFlowID   = SizeVlan           // raw vlan id
	SizeIPv4FlowID   = 2 * SizeIPv4Addr   // source + dest ip
	SizeIPv6FlowID   = 2 * SizeIPv6Addr   // source + dest ip
	SizeICMPFlowID   = SizeICMPID         // icmp identifier (if present)
	SizeTCPFlowID    = 2 * SizePortNumber // source + dest port
	SizeUDPFlowID    = 2 * SizePortNumber // source + dest port
	SizeConnectionID = 8                  // 64bit internal connection id

	SizeFlowIDMax int = SizeEthFlowID +
		2*(SizeVlanFlowID+SizeIPv4FlowID+SizeIPv6FlowID) +
		SizeICMPFlowID +
		SizeTCPFlowID +
		SizeUDPFlowID +
		SizeConnectionID
)

const offUnset uint8 = 0xff

var flowIDEmptyMeta = flowIDMeta{
	flags: 0,

	offEth:        offUnset,
	offOutterVlan: offUnset,
	offVlan:       offUnset,
	offOutterIPv4: offUnset,
	offIPv4:       offUnset,
	offOutterIPv6: offUnset,
	offIPv6:       offUnset,
	offICMPv4:     offUnset,
	offICMPv6:     offUnset,
	offUDP:        offUnset,
	offTCP:        offUnset,
	offID:         offUnset,

	cntEth:  0,
	cntVlan: 0,
	cntIP:   0,
}

type flowDirection int8

const (
	flowDirUnset flowDirection = iota - 1
	flowDirForward
	flowDirReversed
)

func init() {
	if SizeFlowIDMax > 255 {
		panic("SizeFlowIDMax exceeds size limit")
	}
}

func newFlowID() *FlowID {
	f := &FlowID{}
	f.init()
	return f
}

func (f *FlowID) init() {
	f.Reset(nil)
}

func (f *FlowID) Reset(buf []byte) {
	f.flowID = buf
	f.flowIDMeta = flowIDEmptyMeta
	f.dir = flowDirUnset
	f.flow.stats = nil
}

func (f *FlowID) AddEth(src, dst net.HardwareAddr) {
	debugf("flowid: add eth")
	f.addID(&f.offEth, EthFlow, src, dst, flowDirUnset)
	f.cntEth++
}

func (f *FlowID) AddIPv4(src, dst net.IP) {
	debugf("flowid: add ipv4")
	f.addMultLayerID(
		&f.offIPv4, &f.offOutterIPv4,
		IPv4Flow, OutterIPv4Flow,
		src, dst, flowDirUnset)
	f.cntIP++
}

func (f *FlowID) AddIPv6(src, dst net.IP) {
	debugf("flowid: add ipv6")
	f.addMultLayerID(
		&f.offIPv6, &f.offOutterIPv6,
		IPv6Flow, OutterIPv6Flow,
		src, dst, flowDirUnset)
	f.cntIP++
}

func (f *FlowID) AddVLan(id uint16) {
	debugf("flowid: add vlan")
	var tmp [2]byte
	binary.LittleEndian.PutUint16(tmp[:], id)
	f.addMultLayerID(
		&f.offVlan, &f.offOutterVlan,
		VLanFlow, OutterVlanFlow,
		tmp[:], nil, flowDirUnset)
	f.cntVlan++
}

func (f *FlowID) AddICMPv4Request(id uint16) {
	debugf("flowid: add icmp4 request")

	var tmp [2]byte
	binary.LittleEndian.PutUint16(tmp[:], id)
	f.addID(&f.offICMPv4, ICMPv4Flow, tmp[:], nil, flowDirForward)
}

func (f *FlowID) AddICMPv4Response(id uint16) {
	debugf("flowid: add icmp4 response")

	var tmp [2]byte
	binary.LittleEndian.PutUint16(tmp[:], id)
	f.addID(&f.offICMPv4, ICMPv4Flow, tmp[:], nil, flowDirReversed)
}

func (f *FlowID) AddICMPv6Request(id uint16) {
	debugf("flowid: add icmp6 request")

	var tmp [2]byte
	binary.LittleEndian.PutUint16(tmp[:], id)
	f.addID(&f.offICMPv6, ICMPv6Flow, tmp[:], nil, flowDirForward)
}

func (f *FlowID) AddICMPv6Response(id uint16) {
	debugf("flowid: add icmp6 response")

	var tmp [2]byte
	binary.LittleEndian.PutUint16(tmp[:], id)
	f.addID(&f.offICMPv6, ICMPv6Flow, tmp[:], nil, flowDirReversed)
}

func (f *FlowID) AddUDP(src, dst uint16) {
	debugf("flowid: add udp")

	f.addWithPorts(&f.offUDP, UDPFlow, src, dst)
}

func (f *FlowID) AddTCP(src, dst uint16) {
	debugf("flowid: add tcp")

	f.addWithPorts(&f.offTCP, TCPFlow, src, dst)
}

func (f *FlowID) AddConnectionID(id uint64) {
	debugf("flowid: add tcp connection id")

	var tmp [8]byte
	binary.LittleEndian.PutUint64(tmp[:], id)
	f.addID(&f.offID, ConnectionID, tmp[:], nil, flowDirUnset)
}

func (f *FlowID) addMultLayerID(
	off, outerOff *uint8,
	flag, outerFlag FlowIDFlag,
	a, b []byte,
	hint flowDirection,
) {
	a, b = f.sortAddrWrite(a, b, hint)

	flags := f.flags & (flag | outerFlag)
	switch flags {
	case flag | outerFlag:
		*outerOff, *off = *off, *outerOff
		la := copy(f.flowID[(*off):], a)
		copy(f.flowID[la+int(*off):], b)

	case flag:
		*outerOff = *off
		*off = uint8(len(f.flowID))
		f.flowID = append(append(f.flowID, a...), b...)
		f.flags |= outerFlag

	default:
		*off = uint8(len(f.flowID))
		f.flowID = append(append(f.flowID, a...), b...)
		f.flags |= flag

	}
}

func (f *FlowID) addID(
	off *uint8,
	flag FlowIDFlag,
	a, b []byte,
	hint flowDirection,
) {
	a, b = f.sortAddrWrite(a, b, hint)

	if *off == offUnset {
		*off = uint8(len(f.flowID))
		f.flowID = append(append(f.flowID, a...), b...)
		f.flags |= flag
	} else {
		la := copy(f.flowID[(*off):], a)
		copy(f.flowID[la+int(*off):], b)
	}
}

func (f *FlowID) addWithPorts(
	off *uint8,
	flag FlowIDFlag,
	src, dst uint16,
) {
	var a, b [2]byte
	binary.LittleEndian.PutUint16(a[:], src)
	binary.LittleEndian.PutUint16(b[:], dst)
	f.addID(off, flag, a[:], b[:], flowDirUnset)
}

func (f *FlowID) sortAddrWrite(a, b []byte, hint flowDirection) ([]byte, []byte) {
	if b == nil {
		if f.dir == flowDirUnset {
			f.dir = hint
		}
		return a, b
	}

	switch f.dir {
	case flowDirForward:
		return a, b
	case flowDirReversed:
		return b, a
	}

	switch bytes.Compare(a, b) {
	case -1:
		f.dir = flowDirForward
	case 1:
		f.dir = flowDirReversed
		a, b = b, a
	case 0:
		f.dir = hint
	}
	return a, b
}

func (f rawFlowID) clone() rawFlowID {
	n := f
	n.flowID = make([]byte, len(f.flowID))
	copy(n.flowID, f.flowID)
	return n
}

func FlowIDsEqual(f1, f2 *FlowID) bool {
	return f1.flags == f2.flags && bytes.Equal(f1.flowID, f2.flowID)
}

func (f *rawFlowID) Flags() FlowIDFlag {
	return f.flags
}

func (f *rawFlowID) Get(i FlowIDFlag) []byte {
	switch i {
	case EthFlow:
		return f.Eth()
	case OutterVlanFlow:
		return f.OutterVLan()
	case VLanFlow:
		return f.VLan()
	case OutterIPv4Flow:
		return f.OutterIPv4()
	case OutterIPv6Flow:
		return f.OutterIPv6()
	case IPv4Flow:
		return f.IPv4()
	case IPv6Flow:
		return f.IPv6()
	case ICMPv4Flow:
		return f.ICMPv4()
	case ICMPv6Flow:
		return f.ICMPv6()
	case UDPFlow:
		return f.UDP()
	case TCPFlow:
		return f.TCP()
	default:
		return nil
	}
}

func (f *rawFlowID) Serialize() []byte {
	buf := bytes.NewBuffer(nil)
	enc := base64.NewEncoder(base64.RawStdEncoding, buf)

	enc.Write([]byte{
		byte(f.flags & 0xff),
		byte(f.flags >> 8),
		f.offEth,
		f.offOutterVlan,
		f.offVlan,
		f.offOutterIPv4,
		f.offIPv4,
		f.offOutterIPv6,
		f.offIPv6,
		f.offICMPv4,
		f.offICMPv6,
		f.offUDP,
		f.offTCP,
		f.offID,
		f.cntEth,
		f.cntVlan,
		f.cntIP,
	})
	enc.Write(f.flowID)
	enc.Close()

	return buf.Bytes()
}

func (f *rawFlowID) Eth() []byte {
	return f.extractID(f.offEth, SizeEthFlowID)
}

func (f rawFlowID) EthAddr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offEth, SizeEthAddr)
}

func (f *rawFlowID) OutterVLan() []byte {
	return f.extractID(f.offOutterVlan, SizeVlanFlowID)
}

func (f *rawFlowID) VLan() []byte {
	return f.extractID(f.offVlan, SizeVlanFlowID)
}

func (f *rawFlowID) OutterIPv4() []byte {
	return f.extractID(f.offOutterIPv4, SizeIPv4FlowID)
}

func (f *rawFlowID) OutterIPv4Addr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offOutterIPv4, SizeIPv4Addr)
}

func (f *rawFlowID) IPv4() []byte {
	return f.extractID(f.offIPv4, SizeIPv4FlowID)
}

func (f *rawFlowID) IPv4Addr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offIPv4, SizeIPv4Addr)
}

func (f *rawFlowID) OutterIPv6() []byte {
	return f.extractID(f.offOutterIPv6, SizeIPv6FlowID)
}

func (f *rawFlowID) OutterIPv6Addr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offOutterIPv6, SizeIPv6Addr)
}

func (f *rawFlowID) IPv6() []byte {
	return f.extractID(f.offIPv6, SizeIPv6FlowID)
}

func (f *rawFlowID) IPv6Addr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offIPv6, SizeIPv6Addr)
}

func (f *rawFlowID) ICMPv4() []byte {
	return f.extractID(f.offICMPv4, SizeICMPFlowID)
}

func (f *rawFlowID) ICMPv6() []byte {
	return f.extractID(f.offICMPv6, SizeICMPFlowID)
}

func (f *rawFlowID) UDP() []byte {
	return f.extractID(f.offUDP, SizeUDPFlowID)
}

func (f *rawFlowID) UDPAddr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offUDP, SizePortNumber)
}

func (f *rawFlowID) TCP() []byte {
	return f.extractID(f.offTCP, SizeTCPFlowID)
}

func (f *rawFlowID) TCPAddr() ([]byte, []byte, bool) {
	return f.sortAddrRead(f.offTCP, SizePortNumber)
}

func (f *rawFlowID) ConnectionID() []byte {
	return f.extractID(f.offID, SizeConnectionID)
}

func (f *rawFlowID) extractID(off, sz uint8) []byte {
	if off == offUnset {
		return nil
	}

	{
		off := int(off)
		sz := int(sz)
		return f.flowID[off : off+sz]
	}
}

func (f *rawFlowID) sortAddrRead(off, sz uint8) ([]byte, []byte, bool) {
	if off == offUnset {
		return nil, nil, false
	}

	tmp := f.flowID[int(off) : int(off)+2*int(sz)]
	if f.dir == flowDirReversed {
		return tmp[int(sz):], tmp[:int(sz)], true
	}
	return tmp[:int(sz)], tmp[int(sz):], true
}
