/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package channelz

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"

	"google.golang.org/grpc/grpclog"
)

const (
	defaultMaxTraceEntry int32 = 30
)

var maxTraceEntry = defaultMaxTraceEntry

// SetMaxTraceEntry sets maximum number of trace entries per entity (i.e.
// channel/subchannel).  Setting it to 0 will disable channel tracing.
func SetMaxTraceEntry(i int32) {
	atomic.StoreInt32(&maxTraceEntry, i)
}

// ResetMaxTraceEntryToDefault resets the maximum number of trace entries per
// entity to default.
func ResetMaxTraceEntryToDefault() {
	atomic.StoreInt32(&maxTraceEntry, defaultMaxTraceEntry)
}

func getMaxTraceEntry() int {
	i := atomic.LoadInt32(&maxTraceEntry)
	return int(i)
}

// traceEvent is an internal representation of a single trace event
type traceEvent struct {
	// Desc is a simple description of the trace event.
	Desc string
	// Severity states the severity of this trace event.
	Severity Severity
	// Timestamp is the event time.
	Timestamp time.Time
	// RefID is the id of the entity that gets referenced in the event. RefID is 0 if no other entity is
	// involved in this event.
	// e.g. SubChannel (id: 4[]) Created. --> RefID = 4, RefName = "" (inside [])
	RefID int64
	// RefName is the reference name for the entity that gets referenced in the event.
	RefName string
	// RefType indicates the referenced entity type, i.e Channel or SubChannel.
	RefType RefChannelType
}

// TraceEvent is what the caller of AddTraceEvent should provide to describe the
// event to be added to the channel trace.
//
// The Parent field is optional. It is used for an event that will be recorded
// in the entity's parent trace.
type TraceEvent struct {
	Desc     string
	Severity Severity
	Parent   *TraceEvent
}

type ChannelTrace struct {
	cm           *channelMap
	clearCalled  bool
	CreationTime time.Time
	EventNum     int64
	mu           sync.Mutex
	Events       []*traceEvent
}

func (c *ChannelTrace) copy() *ChannelTrace {
	return &ChannelTrace{
		CreationTime: c.CreationTime,
		EventNum:     c.EventNum,
		Events:       append(([]*traceEvent)(nil), c.Events...),
	}
}

func (c *ChannelTrace) append(e *traceEvent) {
	c.mu.Lock()
	if len(c.Events) == getMaxTraceEntry() {
		del := c.Events[0]
		c.Events = c.Events[1:]
		if del.RefID != 0 {
			// start recursive cleanup in a goroutine to not block the call originated from grpc.
			go func() {
				// need to acquire c.cm.mu lock to call the unlocked attemptCleanup func.
				c.cm.mu.Lock()
				c.cm.decrTraceRefCount(del.RefID)
				c.cm.mu.Unlock()
			}()
		}
	}
	e.Timestamp = time.Now()
	c.Events = append(c.Events, e)
	c.EventNum++
	c.mu.Unlock()
}

func (c *ChannelTrace) clear() {
	if c.clearCalled {
		return
	}
	c.clearCalled = true
	c.mu.Lock()
	for _, e := range c.Events {
		if e.RefID != 0 {
			// caller should have already held the c.cm.mu lock.
			c.cm.decrTraceRefCount(e.RefID)
		}
	}
	c.mu.Unlock()
}

// Severity is the severity level of a trace event.
// The canonical enumeration of all valid values is here:
// https://github.com/grpc/grpc-proto/blob/9b13d199cc0d4703c7ea26c9c330ba695866eb23/grpc/channelz/v1/channelz.proto#L126.
type Severity int

const (
	// CtUnknown indicates unknown severity of a trace event.
	CtUnknown Severity = iota
	// CtInfo indicates info level severity of a trace event.
	CtInfo
	// CtWarning indicates warning level severity of a trace event.
	CtWarning
	// CtError indicates error level severity of a trace event.
	CtError
)

// RefChannelType is the type of the entity being referenced in a trace event.
type RefChannelType int

const (
	// RefUnknown indicates an unknown entity type, the zero value for this type.
	RefUnknown RefChannelType = iota
	// RefChannel indicates the referenced entity is a Channel.
	RefChannel
	// RefSubChannel indicates the referenced entity is a SubChannel.
	RefSubChannel
	// RefServer indicates the referenced entity is a Server.
	RefServer
	// RefListenSocket indicates the referenced entity is a ListenSocket.
	RefListenSocket
	// RefNormalSocket indicates the referenced entity is a NormalSocket.
	RefNormalSocket
)

var refChannelTypeToString = map[RefChannelType]string{
	RefUnknown:      "Unknown",
	RefChannel:      "Channel",
	RefSubChannel:   "SubChannel",
	RefServer:       "Server",
	RefListenSocket: "ListenSocket",
	RefNormalSocket: "NormalSocket",
}

func (r RefChannelType) String() string {
	return refChannelTypeToString[r]
}

// AddTraceEvent adds trace related to the entity with specified id, using the
// provided TraceEventDesc.
//
// If channelz is not turned ON, this will simply log the event descriptions.
func AddTraceEvent(l grpclog.DepthLoggerV2, e Entity, depth int, desc *TraceEvent) {
	// Log only the trace description associated with the bottom most entity.
	d := fmt.Sprintf("[%s]%s", e, desc.Desc)
	switch desc.Severity {
	case CtUnknown, CtInfo:
		l.InfoDepth(depth+1, d)
	case CtWarning:
		l.WarningDepth(depth+1, d)
	case CtError:
		l.ErrorDepth(depth+1, d)
	}

	if getMaxTraceEntry() == 0 {
		return
	}
	if IsOn() {
		db.traceEvent(e.id(), desc)
	}
}