package qlog

import (
	"github.com/quic-go/quic-go"
	"github.com/quic-go/quic-go/qlogwriter/jsontext"
)

// Frame represents an HTTP/3 frame.
type Frame struct {
	Frame any
}

func (f Frame) encode(enc *jsontext.Encoder) error {
	switch frame := f.Frame.(type) {
	case DataFrame:
		return frame.encode(enc)
	case HeadersFrame:
		return frame.encode(enc)
	case GoAwayFrame:
		return frame.encode(enc)
	case SettingsFrame:
		return frame.encode(enc)
	case PushPromiseFrame:
		return frame.encode(enc)
	case CancelPushFrame:
		return frame.encode(enc)
	case MaxPushIDFrame:
		return frame.encode(enc)
	case ReservedFrame:
		return frame.encode(enc)
	case UnknownFrame:
		return frame.encode(enc)
	}
	// This shouldn't happen if the code is correctly logging frames.
	// Write a null token to produce valid JSON.
	return enc.WriteToken(jsontext.Null)
}

// A DataFrame is a DATA frame
type DataFrame struct{}

func (f *DataFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("data"))
	h.WriteToken(jsontext.EndObject)
	return h.err
}

type HeaderField struct {
	Name  string
	Value string
}

// A HeadersFrame is a HEADERS frame
type HeadersFrame struct {
	HeaderFields []HeaderField
}

func (f *HeadersFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("headers"))
	if len(f.HeaderFields) > 0 {
		h.WriteToken(jsontext.String("header_fields"))
		h.WriteToken(jsontext.BeginArray)
		for _, f := range f.HeaderFields {
			h.WriteToken(jsontext.BeginObject)
			h.WriteToken(jsontext.String("name"))
			h.WriteToken(jsontext.String(f.Name))
			h.WriteToken(jsontext.String("value"))
			h.WriteToken(jsontext.String(f.Value))
			h.WriteToken(jsontext.EndObject)
		}
		h.WriteToken(jsontext.EndArray)
	}
	h.WriteToken(jsontext.EndObject)
	return h.err
}

// A GoAwayFrame is a GOAWAY frame
type GoAwayFrame struct {
	StreamID quic.StreamID
}

func (f *GoAwayFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("goaway"))
	h.WriteToken(jsontext.String("id"))
	h.WriteToken(jsontext.Uint(uint64(f.StreamID)))
	h.WriteToken(jsontext.EndObject)
	return h.err
}

type SettingsFrame struct {
	MaxFieldSectionSize int64
	Datagram            *bool
	ExtendedConnect     *bool
	Other               map[uint64]uint64
}

func (f *SettingsFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("settings"))
	h.WriteToken(jsontext.String("settings"))
	h.WriteToken(jsontext.BeginArray)
	if f.MaxFieldSectionSize >= 0 {
		h.WriteToken(jsontext.BeginObject)
		h.WriteToken(jsontext.String("name"))
		h.WriteToken(jsontext.String("settings_max_field_section_size"))
		h.WriteToken(jsontext.String("value"))
		h.WriteToken(jsontext.Uint(uint64(f.MaxFieldSectionSize)))
		h.WriteToken(jsontext.EndObject)
	}
	if f.Datagram != nil {
		h.WriteToken(jsontext.BeginObject)
		h.WriteToken(jsontext.String("name"))
		h.WriteToken(jsontext.String("settings_h3_datagram"))
		h.WriteToken(jsontext.String("value"))
		h.WriteToken(jsontext.Bool(*f.Datagram))
		h.WriteToken(jsontext.EndObject)
	}
	if f.ExtendedConnect != nil {
		h.WriteToken(jsontext.BeginObject)
		h.WriteToken(jsontext.String("name"))
		h.WriteToken(jsontext.String("settings_enable_connect_protocol"))
		h.WriteToken(jsontext.String("value"))
		h.WriteToken(jsontext.Bool(*f.ExtendedConnect))
		h.WriteToken(jsontext.EndObject)
	}
	if len(f.Other) > 0 {
		for k, v := range f.Other {
			h.WriteToken(jsontext.BeginObject)
			h.WriteToken(jsontext.String("name"))
			h.WriteToken(jsontext.String("unknown"))
			h.WriteToken(jsontext.String("name_bytes"))
			h.WriteToken(jsontext.Uint(k))
			h.WriteToken(jsontext.String("value"))
			h.WriteToken(jsontext.Uint(v))
			h.WriteToken(jsontext.EndObject)
		}
	}
	h.WriteToken(jsontext.EndArray)
	h.WriteToken(jsontext.EndObject)
	return h.err
}

// A PushPromiseFrame is a PUSH_PROMISE frame
type PushPromiseFrame struct{}

func (f *PushPromiseFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("push_promise"))
	h.WriteToken(jsontext.EndObject)
	return h.err
}

// A CancelPushFrame is a CANCEL_PUSH frame
type CancelPushFrame struct{}

func (f *CancelPushFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("cancel_push"))
	h.WriteToken(jsontext.EndObject)
	return h.err
}

// A MaxPushIDFrame is a MAX_PUSH_ID frame
type MaxPushIDFrame struct{}

func (f *MaxPushIDFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("max_push_id"))
	h.WriteToken(jsontext.EndObject)
	return h.err
}

// A ReservedFrame is one of the reserved frame types
type ReservedFrame struct {
	Type uint64
}

func (f *ReservedFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("reserved"))
	h.WriteToken(jsontext.String("frame_type_bytes"))
	h.WriteToken(jsontext.Uint(f.Type))
	h.WriteToken(jsontext.EndObject)
	return h.err
}

// An UnknownFrame is an unknown frame type
type UnknownFrame struct {
	Type uint64
}

func (f *UnknownFrame) encode(enc *jsontext.Encoder) error {
	h := encoderHelper{enc: enc}
	h.WriteToken(jsontext.BeginObject)
	h.WriteToken(jsontext.String("frame_type"))
	h.WriteToken(jsontext.String("unknown"))
	h.WriteToken(jsontext.String("frame_type_bytes"))
	h.WriteToken(jsontext.Uint(f.Type))
	h.WriteToken(jsontext.EndObject)
	return h.err
}
