package etw

import (
	"bytes"
	"encoding/binary"
)

// inType indicates the type of data contained in the ETW event.
type inType byte

// Various inType definitions for TraceLogging. These must match the definitions
// found in TraceLoggingProvider.h in the Windows SDK.
const (
	inTypeNull inType = iota
	inTypeUnicodeString
	inTypeANSIString
	inTypeInt8
	inTypeUint8
	inTypeInt16
	inTypeUint16
	inTypeInt32
	inTypeUint32
	inTypeInt64
	inTypeUint64
	inTypeFloat
	inTypeDouble
	inTypeBool32
	inTypeBinary
	inTypeGUID
	inTypePointerUnsupported
	inTypeFileTime
	inTypeSystemTime
	inTypeSID
	inTypeHexInt32
	inTypeHexInt64
	inTypeCountedString
	inTypeCountedANSIString
	inTypeStruct
	inTypeCountedBinary
	inTypeCountedArray inType = 32
	inTypeArray        inType = 64
)

// outType specifies a hint to the event decoder for how the value should be
// formatted.
type outType byte

// Various outType definitions for TraceLogging. These must match the
// definitions found in TraceLoggingProvider.h in the Windows SDK.
const (
	// outTypeDefault indicates that the default formatting for the inType will
	// be used by the event decoder.
	outTypeDefault outType = iota
	outTypeNoPrint
	outTypeString
	outTypeBoolean
	outTypeHex
	outTypePID
	outTypeTID
	outTypePort
	outTypeIPv4
	outTypeIPv6
	outTypeSocketAddress
	outTypeXML
	outTypeJSON
	outTypeWin32Error
	outTypeNTStatus
	outTypeHResult
	outTypeFileTime
	outTypeSigned
	outTypeUnsigned
	outTypeUTF8              outType = 35
	outTypePKCS7WithTypeInfo outType = 36
	outTypeCodePointer       outType = 37
	outTypeDateTimeUTC       outType = 38
)

// eventMetadata maintains a buffer which builds up the metadata for an ETW
// event. It needs to be paired with EventData which describes the event.
type eventMetadata struct {
	buffer bytes.Buffer
}

// bytes returns the raw binary data containing the event metadata. Before being
// returned, the current size of the buffer is written to the start of the
// buffer. The returned value is not copied from the internal buffer, so it can
// be mutated by the eventMetadata object after it is returned.
func (em *eventMetadata) bytes() []byte {
	// Finalize the event metadata buffer by filling in the buffer length at the
	// beginning.
	binary.LittleEndian.PutUint16(em.buffer.Bytes(), uint16(em.buffer.Len()))
	return em.buffer.Bytes()
}

// writeEventHeader writes the metadata for the start of an event to the buffer.
// This specifies the event name and tags.
func (em *eventMetadata) writeEventHeader(name string, tags uint32) {
	binary.Write(&em.buffer, binary.LittleEndian, uint16(0)) // Length placeholder
	em.writeTags(tags)
	em.buffer.WriteString(name)
	em.buffer.WriteByte(0) // Null terminator for name
}

func (em *eventMetadata) writeFieldInner(name string, inType inType, outType outType, tags uint32, arrSize uint16) {
	em.buffer.WriteString(name)
	em.buffer.WriteByte(0) // Null terminator for name

	if outType == outTypeDefault && tags == 0 {
		em.buffer.WriteByte(byte(inType))
	} else {
		em.buffer.WriteByte(byte(inType | 128))
		if tags == 0 {
			em.buffer.WriteByte(byte(outType))
		} else {
			em.buffer.WriteByte(byte(outType | 128))
			em.writeTags(tags)
		}
	}

	if arrSize != 0 {
		binary.Write(&em.buffer, binary.LittleEndian, arrSize)
	}
}

// writeTags writes out the tags value to the event metadata. Tags is a 28-bit
// value, interpreted as bit flags, which are only relevant to the event
// consumer. The event consumer may choose to attribute special meaning to tags
// (e.g. 0x4 could mean the field contains PII). Tags are written as a series of
// bytes, each containing 7 bits of tag value, with the high bit set if there is
// more tag data in the following byte. This allows for a more compact
// representation when not all of the tag bits are needed.
func (em *eventMetadata) writeTags(tags uint32) {
	// Only use the top 28 bits of the tags value.
	tags &= 0xfffffff

	for {
		// Tags are written with the most significant bits (e.g. 21-27) first.
		val := tags >> 21

		if tags&0x1fffff == 0 {
			// If there is no more data to write after this, write this value
			// without the high bit set, and return.
			em.buffer.WriteByte(byte(val & 0x7f))
			return
		}

		em.buffer.WriteByte(byte(val | 0x80))

		tags <<= 7
	}
}

// writeField writes the metadata for a simple field to the buffer.
func (em *eventMetadata) writeField(name string, inType inType, outType outType, tags uint32) {
	em.writeFieldInner(name, inType, outType, tags, 0)
}

// writeArray writes the metadata for an array field to the buffer. The number
// of elements in the array must be written as a uint16 in the event data,
// immediately preceeding the event data.
func (em *eventMetadata) writeArray(name string, inType inType, outType outType, tags uint32) {
	em.writeFieldInner(name, inType|inTypeArray, outType, tags, 0)
}

// writeCountedArray writes the metadata for an array field to the buffer. The
// size of a counted array is fixed, and the size is written into the metadata
// directly.
func (em *eventMetadata) writeCountedArray(name string, count uint16, inType inType, outType outType, tags uint32) {
	em.writeFieldInner(name, inType|inTypeCountedArray, outType, tags, count)
}

// writeStruct writes the metadata for a nested struct to the buffer. The struct
// contains the next N fields in the metadata, where N is specified by the
// fieldCount argument.
func (em *eventMetadata) writeStruct(name string, fieldCount uint8, tags uint32) {
	em.writeFieldInner(name, inTypeStruct, outType(fieldCount), tags, 0)
}
