package binding

import (
	"context"

	"github.com/cloudevents/sdk-go/v2/event"
)

type eventEncodingKey int

const (
	skipDirectStructuredEncoding eventEncodingKey = iota
	skipDirectBinaryEncoding
	preferredEventEncoding
)

// DirectWrite invokes the encoders. structuredWriter and binaryWriter could be nil if the protocol doesn't support it.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
// This function MUST be invoked only if message.ReadEncoding() == EncodingBinary or message.ReadEncoding() == EncodingStructured
//
// Returns:
// * EncodingStructured, nil if message is correctly encoded in structured encoding
// * EncodingBinary, nil if message is correctly encoded in binary encoding
// * EncodingStructured, err if message was structured but error happened during the encoding
// * EncodingBinary, err if message was binary but error happened during the encoding
// * EncodingUnknown, ErrUnknownEncoding if message is not a structured or a binary Message
func DirectWrite(
	ctx context.Context,
	message MessageReader,
	structuredWriter StructuredWriter,
	binaryWriter BinaryWriter,
	transformers ...Transformer,
) (Encoding, error) {
	if structuredWriter != nil && len(transformers) == 0 && !GetOrDefaultFromCtx(ctx, skipDirectStructuredEncoding, false).(bool) {
		if err := message.ReadStructured(ctx, structuredWriter); err == nil {
			return EncodingStructured, nil
		} else if err != ErrNotStructured {
			return EncodingStructured, err
		}
	}

	if binaryWriter != nil && !GetOrDefaultFromCtx(ctx, skipDirectBinaryEncoding, false).(bool) && message.ReadEncoding() == EncodingBinary {
		return EncodingBinary, writeBinaryWithTransformer(ctx, message, binaryWriter, transformers)
	}

	return EncodingUnknown, ErrUnknownEncoding
}

// Write executes the full algorithm to encode a Message using transformers:
// 1. It first tries direct encoding using DirectWrite
// 2. If no direct encoding is possible, it uses ToEvent to generate an Event representation
// 3. From the Event, the message is encoded back to the provided structured or binary encoders
// You can tweak the encoding process using the context decorators WithForceStructured, WithForceStructured, etc.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
// Returns:
// * EncodingStructured, nil if message is correctly encoded in structured encoding
// * EncodingBinary, nil if message is correctly encoded in binary encoding
// * EncodingUnknown, ErrUnknownEncoding if message.ReadEncoding() == EncodingUnknown
// * _, err if error happened during the encoding
func Write(
	ctx context.Context,
	message MessageReader,
	structuredWriter StructuredWriter,
	binaryWriter BinaryWriter,
	transformers ...Transformer,
) (Encoding, error) {
	enc := message.ReadEncoding()
	var err error
	// Skip direct encoding if the event is an event message
	if enc != EncodingEvent {
		enc, err = DirectWrite(ctx, message, structuredWriter, binaryWriter, transformers...)
		if enc != EncodingUnknown {
			// Message directly encoded, nothing else to do here
			return enc, err
		}
	}

	var e *event.Event
	e, err = ToEvent(ctx, message, transformers...)
	if err != nil {
		return enc, err
	}

	message = (*EventMessage)(e)

	if GetOrDefaultFromCtx(ctx, preferredEventEncoding, EncodingBinary).(Encoding) == EncodingStructured {
		if structuredWriter != nil {
			return EncodingStructured, message.ReadStructured(ctx, structuredWriter)
		}
		if binaryWriter != nil {
			return EncodingBinary, writeBinary(ctx, message, binaryWriter)
		}
	} else {
		if binaryWriter != nil {
			return EncodingBinary, writeBinary(ctx, message, binaryWriter)
		}
		if structuredWriter != nil {
			return EncodingStructured, message.ReadStructured(ctx, structuredWriter)
		}
	}

	return EncodingUnknown, ErrUnknownEncoding
}

// WithSkipDirectStructuredEncoding skips direct structured to structured encoding during the encoding process
func WithSkipDirectStructuredEncoding(ctx context.Context, skip bool) context.Context {
	return context.WithValue(ctx, skipDirectStructuredEncoding, skip)
}

// WithSkipDirectBinaryEncoding skips direct binary to binary encoding during the encoding process
func WithSkipDirectBinaryEncoding(ctx context.Context, skip bool) context.Context {
	return context.WithValue(ctx, skipDirectBinaryEncoding, skip)
}

// WithPreferredEventEncoding defines the preferred encoding from event to message during the encoding process
func WithPreferredEventEncoding(ctx context.Context, enc Encoding) context.Context {
	return context.WithValue(ctx, preferredEventEncoding, enc)
}

// WithForceStructured forces structured encoding during the encoding process
func WithForceStructured(ctx context.Context) context.Context {
	return context.WithValue(context.WithValue(ctx, preferredEventEncoding, EncodingStructured), skipDirectBinaryEncoding, true)
}

// WithForceBinary forces binary encoding during the encoding process
func WithForceBinary(ctx context.Context) context.Context {
	return context.WithValue(context.WithValue(ctx, preferredEventEncoding, EncodingBinary), skipDirectStructuredEncoding, true)
}

// GetOrDefaultFromCtx gets a configuration value from the provided context
func GetOrDefaultFromCtx(ctx context.Context, key interface{}, def interface{}) interface{} {
	if val := ctx.Value(key); val != nil {
		return val
	} else {
		return def
	}
}

func writeBinaryWithTransformer(
	ctx context.Context,
	message MessageReader,
	binaryWriter BinaryWriter,
	transformers Transformers,
) error {
	err := binaryWriter.Start(ctx)
	if err != nil {
		return err
	}
	err = message.ReadBinary(ctx, binaryWriter)
	if err != nil {
		return err
	}
	err = transformers.Transform(message.(MessageMetadataReader), binaryWriter)
	if err != nil {
		return err
	}
	return binaryWriter.End(ctx)
}

func writeBinary(
	ctx context.Context,
	message MessageReader,
	binaryWriter BinaryWriter,
) error {
	err := binaryWriter.Start(ctx)
	if err != nil {
		return err
	}
	err = message.ReadBinary(ctx, binaryWriter)
	if err != nil {
		return err
	}
	return binaryWriter.End(ctx)
}
