// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package trace

import (
	"fmt"
	"internal/trace/tracev2"
	"io"
	"runtime"
	"sync"
	"sync/atomic"
	_ "unsafe"
)

var tracing traceMultiplexer

type traceMultiplexer struct {
	sync.Mutex
	enabled     atomic.Bool
	subscribers int

	subscribersMu    sync.Mutex
	traceStartWriter io.Writer
	flightRecorder   *recorder
}

func (t *traceMultiplexer) subscribeFlightRecorder(r *recorder) error {
	t.Lock()
	defer t.Unlock()

	t.subscribersMu.Lock()
	if t.flightRecorder != nil {
		t.subscribersMu.Unlock()
		return fmt.Errorf("flight recorder already enabled")
	}
	t.flightRecorder = r
	t.subscribersMu.Unlock()

	if err := t.addedSubscriber(); err != nil {
		t.subscribersMu.Lock()
		t.flightRecorder = nil
		t.subscribersMu.Unlock()
		return err
	}
	return nil
}

func (t *traceMultiplexer) unsubscribeFlightRecorder() error {
	t.Lock()
	defer t.Unlock()

	t.removingSubscriber()

	t.subscribersMu.Lock()
	if t.flightRecorder == nil {
		t.subscribersMu.Unlock()
		return fmt.Errorf("attempt to unsubscribe missing flight recorder")
	}
	t.flightRecorder = nil
	t.subscribersMu.Unlock()

	t.removedSubscriber()
	return nil
}

func (t *traceMultiplexer) subscribeTraceStartWriter(w io.Writer) error {
	t.Lock()
	defer t.Unlock()

	t.subscribersMu.Lock()
	if t.traceStartWriter != nil {
		t.subscribersMu.Unlock()
		return fmt.Errorf("execution tracer already enabled")
	}
	t.traceStartWriter = w
	t.subscribersMu.Unlock()

	if err := t.addedSubscriber(); err != nil {
		t.subscribersMu.Lock()
		t.traceStartWriter = nil
		t.subscribersMu.Unlock()
		return err
	}
	return nil
}

func (t *traceMultiplexer) unsubscribeTraceStartWriter() {
	t.Lock()
	defer t.Unlock()

	t.removingSubscriber()

	t.subscribersMu.Lock()
	if t.traceStartWriter == nil {
		t.subscribersMu.Unlock()
		return
	}
	t.traceStartWriter = nil
	t.subscribersMu.Unlock()

	t.removedSubscriber()
	return
}

func (t *traceMultiplexer) addedSubscriber() error {
	if t.enabled.Load() {
		// This is necessary for the trace reader goroutine to pick up on the new subscriber.
		runtime_traceAdvance(false)
	} else {
		if err := t.startLocked(); err != nil {
			return err
		}
	}
	t.subscribers++
	return nil
}

func (t *traceMultiplexer) removingSubscriber() {
	if t.subscribers == 0 {
		return
	}
	t.subscribers--
	if t.subscribers == 0 {
		runtime.StopTrace()
		t.enabled.Store(false)
	} else {
		// This is necessary to avoid missing trace data when the system is under high load.
		runtime_traceAdvance(false)
	}
}

func (t *traceMultiplexer) removedSubscriber() {
	if t.subscribers > 0 {
		// This is necessary for the trace reader goroutine to pick up on the new subscriber.
		runtime_traceAdvance(false)
	}
}

func (t *traceMultiplexer) startLocked() error {
	if err := runtime.StartTrace(); err != nil {
		return err
	}

	// Grab the trace reader goroutine's subscribers.
	//
	// We only update our subscribers if we see an end-of-generation
	// signal from the runtime after this, so any new subscriptions
	// or unsubscriptions must call traceAdvance to ensure the reader
	// goroutine sees an end-of-generation signal.
	t.subscribersMu.Lock()
	flightRecorder := t.flightRecorder
	traceStartWriter := t.traceStartWriter
	t.subscribersMu.Unlock()

	go func() {
		header := runtime.ReadTrace()
		if traceStartWriter != nil {
			traceStartWriter.Write(header)
		}
		if flightRecorder != nil {
			flightRecorder.Write(header)
		}

		for {
			data := runtime.ReadTrace()
			if data == nil {
				break
			}
			if traceStartWriter != nil {
				traceStartWriter.Write(data)
			}
			if flightRecorder != nil {
				flightRecorder.Write(data)
			}
			if len(data) == 1 && tracev2.EventType(data[0]) == tracev2.EvEndOfGeneration {
				if flightRecorder != nil {
					flightRecorder.endGeneration()
				}

				// Pick up any changes.
				t.subscribersMu.Lock()
				frIsNew := flightRecorder != t.flightRecorder && t.flightRecorder != nil
				trIsNew := traceStartWriter != t.traceStartWriter && t.traceStartWriter != nil
				flightRecorder = t.flightRecorder
				traceStartWriter = t.traceStartWriter
				t.subscribersMu.Unlock()

				if trIsNew {
					traceStartWriter.Write(header)
				}
				if frIsNew {
					flightRecorder.Write(header)
				}
			}
		}
	}()
	t.enabled.Store(true)
	return nil
}

//go:linkname runtime_readTrace
func runtime_readTrace() (buf []byte)
