package network

import (
	"sync"

	"github.com/gopacket/gopacket"
	"github.com/gopacket/gopacket/layers"
)

type Handshake struct {
	sync.RWMutex

	Beacon        gopacket.Packet
	Challenges    []gopacket.Packet
	Responses     []gopacket.Packet
	Confirmations []gopacket.Packet
	hasPMKID      bool
	unsaved       []gopacket.Packet
}

func NewHandshake() *Handshake {
	return &Handshake{
		Challenges:    make([]gopacket.Packet, 0),
		Responses:     make([]gopacket.Packet, 0),
		Confirmations: make([]gopacket.Packet, 0),
		unsaved:       make([]gopacket.Packet, 0),
	}
}

func (h *Handshake) SetBeacon(pkt gopacket.Packet) {
	h.Lock()
	defer h.Unlock()

	if h.Beacon == nil {
		h.Beacon = pkt
		h.unsaved = append(h.unsaved, pkt)
	}
}

func (h *Handshake) AddAndGetPMKID(pkt gopacket.Packet) []byte {
	h.AddFrame(0, pkt)

	prevWasKey := false
	for _, layer := range pkt.Layers() {
		if layer.LayerType() == layers.LayerTypeEAPOLKey {
			prevWasKey = true
			continue
		}

		if prevWasKey && layer.LayerType() == layers.LayerTypeDot11InformationElement {
			info := layer.(*layers.Dot11InformationElement)
			if info.ID == layers.Dot11InformationElementIDVendor && info.Length == 20 {
				h.Lock()
				defer h.Unlock()
				h.hasPMKID = true
				return info.Info
			}
		}

		prevWasKey = false
	}

	return nil
}

func (h *Handshake) AddFrame(n int, pkt gopacket.Packet) {
	h.Lock()
	defer h.Unlock()

	switch n {
	case 0:
		h.Challenges = append(h.Challenges, pkt)
	case 1:
		h.Responses = append(h.Responses, pkt)
	case 2:
		h.Confirmations = append(h.Confirmations, pkt)
	}

	h.unsaved = append(h.unsaved, pkt)
}

func (h *Handshake) AddExtra(pkt gopacket.Packet) {
	h.Lock()
	defer h.Unlock()
	h.unsaved = append(h.unsaved, pkt)
}

func (h *Handshake) Complete() bool {
	h.RLock()
	defer h.RUnlock()

	nChal := len(h.Challenges)
	nResp := len(h.Responses)
	nConf := len(h.Confirmations)

	return nChal > 0 && nResp > 0 && nConf > 0
}

func (h *Handshake) Half() bool {
	h.RLock()
	defer h.RUnlock()

	/*
	 * You can use every combination of the handshake to crack the net:
	 * M1/M2
	 * M2/M3
	 * M3/M4
	 * M1/M4 (if M4 snonce is not zero)
	 * We only have M1 (the challenge), M2 (the response) and M3 (the confirmation)
	 */
	nChal := len(h.Challenges)
	nResp := len(h.Responses)
	nConf := len(h.Confirmations)

	return (nChal > 0 && nResp > 0) || (nResp > 0 && nConf > 0)
}

func (h *Handshake) HasPMKID() bool {
	h.RLock()
	defer h.RUnlock()
	return h.hasPMKID
}

func (h *Handshake) Any() bool {
	return h.HasPMKID() || h.Half() || h.Complete()
}

func (h *Handshake) NumUnsaved() int {
	h.RLock()
	defer h.RUnlock()
	return len(h.unsaved)
}

func (h *Handshake) EachUnsavedPacket(cb func(gopacket.Packet)) {
	h.Lock()
	defer h.Unlock()

	for _, pkt := range h.unsaved {
		cb(pkt)
	}
	h.unsaved = make([]gopacket.Packet, 0)
}
