/*
File Name:  Packet Lite.go
Copyright:  2021 Peernet s.r.o.
Author:     Peter Kleissner

The lite packet header is used for encoding data transfer packets. The regular header is too expensive in terms of CPU consumption due to public key signing.
Instead, a simple session ID will identify lite packets. The ID is randomized and only valid during the session.
Unsolicited lite packets are therefore impossible; the receiver must have the ID already whitelisted for the packet to be recognized.

Offset  Size   Info
0       16     ID
16      2      Size of data to follow

*/

package protocol

import (
	"encoding/binary"
	"errors"
	"sync"
	"time"

	"github.com/google/uuid"
)

// PacketLiteRaw is a decrypted P2P lite packet
type PacketLiteRaw struct {
	ID      uuid.UUID // ID
	Payload []byte    // Payload
	Session *LiteID   // Session info
}

// Minimum packet size of lite packets.
const PacketLiteSizeMin = 16 + 2

// IsPacketLite identifies a lite packet based on its ID. If the ID is not recognized, it fails.
func (router *LiteRouter) IsPacketLite(raw []byte) (isLite bool, err error) {
	if len(raw) < PacketLiteSizeMin {
		return false, errors.New("invalid packet size")
	}

	// Parse the ID and then look it up.
	var id uuid.UUID
	copy(id[:], raw[0:16])

	return router.LookupLiteID(id) != nil, nil
}

// PacketLiteDecode a lite packet. It will identify the lite packet based on its ID. If the ID is not recognized (which is the case for regular Peernet packets), the function fails.
// It does not perform any decryption.
func (router *LiteRouter) PacketLiteDecode(raw []byte) (packet *PacketLiteRaw, err error) {
	if len(raw) < PacketLiteSizeMin {
		return nil, errors.New("invalid packet size")
	}

	// Parse the ID and look it up. It will contain information about the decryption algorithm to use.
	var id uuid.UUID
	copy(id[:], raw[0:16])

	session := router.LookupLiteID(id)
	if session == nil {
		return nil, errors.New("packet ID not found")
	}

	// TODO: Decrypt the data if indicated by the session.

	sizePayload := binary.LittleEndian.Uint16(raw[16 : 16+2])
	if int(sizePayload) > len(raw)-PacketLiteSizeMin { // invalid size field?
		return nil, errors.New("invalid packet size field")
	}

	// Valid packet received, extend expiration.
	session.expires = time.Now().Add(session.timeout)

	return &PacketLiteRaw{Payload: raw[PacketLiteSizeMin:], ID: id, Session: session}, nil
}

// Encodes a lite packet.
func PacketLiteEncode(id uuid.UUID, data []byte) (raw []byte, err error) {
	raw = make([]byte, PacketLiteSizeMin+len(data))

	copy(raw[0:16], id[:])
	binary.LittleEndian.PutUint16(raw[16:16+2], uint16(len(data)))
	copy(raw[PacketLiteSizeMin:], data)

	return raw, nil
}

// ---- Lite packet ID management. This is similar to packet sequences. ----

// LiteRouter keeps track of accepted (expected) packet IDs.
type LiteRouter struct {
	// list of recognized IDs
	ids map[uuid.UUID]*LiteID

	sync.Mutex // synchronized access to the IDs
}

// LiteID contains session information for a bidirectional transfer of data
type LiteID struct {
	ID             uuid.UUID     // ID
	created        time.Time     // When the ID was created.
	expires        time.Time     // When the ID expires. This can be extended on the fly!
	Data           interface{}   // Optional high-level data associated with the ID
	timeout        time.Duration // Timeout for receiving the next message
	invalidateFunc func()        // Called on expiration.
}

// Creates a new manager to keep track of accepted IDs.
func NewLiteRouter() (router *LiteRouter) {
	router = &LiteRouter{
		ids: make(map[uuid.UUID]*LiteID),
	}

	go router.autoDeleteExpired()

	return
}

// autoDeleteExpired deletes all IDs that are expired.
func (router *LiteRouter) autoDeleteExpired() {
	for {
		time.Sleep(4 * time.Second)
		now := time.Now()

		router.Lock()
		for id, info := range router.ids {
			if info.expires.Before(now) {
				delete(router.ids, id)

				if info.invalidateFunc != nil {
					go info.invalidateFunc()
				}
			}
		}
		router.Unlock()
	}
}

func (router *LiteRouter) LookupLiteID(id uuid.UUID) (info *LiteID) {
	router.Lock()
	info = router.ids[id]
	router.Unlock()

	return info
}

// Returns a new lite ID to be used.
func (router *LiteRouter) NewLiteID(data interface{}, timeout time.Duration, invalidateFunc func()) (info *LiteID) {
	info = &LiteID{
		created:        time.Now(),
		expires:        time.Now().Add(timeout),
		timeout:        timeout,
		invalidateFunc: invalidateFunc,
		Data:           data,
		ID:             uuid.New(),
	}

	router.Lock()
	router.ids[info.ID] = info
	router.Unlock()

	return
}

func (router *LiteRouter) RegisterLiteID(id uuid.UUID, data interface{}, timeout time.Duration, invalidateFunc func()) (info *LiteID) {
	info = &LiteID{
		ID:             id,
		created:        time.Now(),
		expires:        time.Now().Add(timeout),
		timeout:        timeout,
		invalidateFunc: invalidateFunc,
		Data:           data,
	}

	router.Lock()
	existingInfo := router.ids[info.ID]
	router.ids[info.ID] = info
	router.Unlock()

	// Call the invalidate function if there is a collision. This should never happen.
	if existingInfo != nil && existingInfo.invalidateFunc != nil {
		go existingInfo.invalidateFunc()
	}

	return
}

// Returns all lite sessions
func (router *LiteRouter) All() (sessions []*LiteID) {
	router.Lock()
	for _, info := range router.ids {
		sessions = append(sessions, info)
	}
	router.Unlock()

	return sessions
}
