package flume

import (
	"encoding/hex"
	"github.com/mgutz/ansi"
	"go.uber.org/zap"
	"go.uber.org/zap/buffer"
	"go.uber.org/zap/zapcore"
)

//nolint:gochecknoinits
func init() {
	_ = zap.RegisterEncoder("term", func(cfg zapcore.EncoderConfig) (zapcore.Encoder, error) {
		return NewConsoleEncoder((*EncoderConfig)(&cfg)), nil
	})
	_ = zap.RegisterEncoder("term-color", func(cfg zapcore.EncoderConfig) (zapcore.Encoder, error) {
		return NewColorizedConsoleEncoder((*EncoderConfig)(&cfg), nil), nil
	})
}

// Colorizer returns ansi escape sequences for the colors for each log level.
// See Colors for a default implementation.
type Colorizer interface {
	Level(l Level) string
}

// Colors is an implementation of the Colorizer interface, which assigns colors
// to the default log levels.
type Colors struct {
	Debug, Info, Warn, Error string
}

// Level implements Colorizer
func (c *Colors) Level(l Level) string {
	if l < DebugLevel {
		return Dim
	}
	switch l {
	case DebugLevel:
		return c.Debug
	case InfoLevel:
		return c.Info
	case Level(zapcore.WarnLevel):
		return c.Warn
	default:
		return c.Error
	}
}

// DefaultColors is the default instance of Colors, used as the default colors if
// a nil Colorizer is passed to NewColorizedConsoleEncoder.
var DefaultColors = Colors{
	Debug: ansi.ColorCode("cyan"),
	Info:  ansi.ColorCode("green+h"),
	Warn:  ansi.ColorCode("yellow+bh"),
	Error: ansi.ColorCode("red+bh"),
}

type consoleEncoder struct {
	*ltsvEncoder
	colorizer Colorizer
}

// NewConsoleEncoder creates an encoder whose output is designed for human -
// rather than machine - consumption. It serializes the core log entry data
// (message, level, timestamp, etc.) in a plain-text format.  The context is
// encoded in LTSV.
//
// Note that although the console encoder doesn't use the keys specified in the
// encoder configuration, it will omit any element whose key is set to the empty
// string.
func NewConsoleEncoder(cfg *EncoderConfig) Encoder {
	ltsvEncoder := NewLTSVEncoder(cfg).(*ltsvEncoder)
	ltsvEncoder.allowNewLines = true
	ltsvEncoder.allowTabs = true
	ltsvEncoder.blankKey = "value"
	ltsvEncoder.binaryEncoder = hex.Dump

	return &consoleEncoder{ltsvEncoder: ltsvEncoder}
}

// NewColorizedConsoleEncoder creates a console encoder, like NewConsoleEncoder, but
// colors the text with ansi escape codes.  `colorize` configures which colors to
// use for each level.
//
// If `colorizer` is nil, it will default to DefaultColors.
//
// `github.com/mgutz/ansi` is a convenient package for getting color codes, e.g.:
//
//     ansi.ColorCode("red")
//
func NewColorizedConsoleEncoder(cfg *EncoderConfig, colorizer Colorizer) Encoder {
	e := NewConsoleEncoder(cfg).(*consoleEncoder)
	e.colorizer = colorizer
	if e.colorizer == nil {
		e.colorizer = &DefaultColors
	}
	return e
}

// Clone implements the Encoder interface
func (c *consoleEncoder) Clone() zapcore.Encoder {
	return &consoleEncoder{
		ltsvEncoder: c.ltsvEncoder.Clone().(*ltsvEncoder),
		colorizer:   c.colorizer,
	}
}

// Dim is the color used for context keys, time, and caller information
var Dim = ansi.ColorCode("240")

// Bright is the color used for the message
var Bright = ansi.ColorCode("default+b")

// EncodeEntry implements the Encoder interface
func (c *consoleEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
	final := *c.ltsvEncoder
	context := final.buf
	final.buf = bufPool.Get()

	origLen := final.buf.Len()

	if c.TimeKey != "" {
		c.colorDim(final.buf)
		final.skipNextElementSeparator = true
		c.EncodeTime(ent.Time, &final)
	}

	if c.LevelKey != "" {
		c.colorLevel(final.buf, ent.Level)
		if final.buf.Len() > origLen {
			final.buf.AppendByte(' ')
		}
		final.skipNextElementSeparator = true

		c.EncodeLevel(ent.Level, &final)

	}

	if final.buf.Len() > origLen {
		c.colorDim(final.buf)
		final.buf.AppendString(" | ")
	} else {
		final.buf.Reset()
	}

	// Add the message itself.
	if c.MessageKey != "" {
		c.colorReset(final.buf)
		// c.colorBright(&final)
		final.safeAddString(ent.Message, false)
		// ensure a minimum of 2 spaces between the message and the fields,
		// to improve readability
		final.buf.AppendString("  ")
	}

	c.colorDim(final.buf)

	// Add fields.
	for _, f := range fields {
		f.AddTo(&final)
	}

	// Add context
	if context.Len() > 0 {
		final.addFieldSeparator()
		_, _ = final.buf.Write(context.Bytes())
	}

	// Add callsite
	c.writeCallSite(&final, ent.LoggerName, ent.Caller)

	// If there's no stacktrace key, honor that; this allows users to force
	// single-line output.
	if ent.Stack != "" && c.StacktraceKey != "" {
		final.buf.AppendByte('\n')
		final.buf.AppendString(ent.Stack)
	}
	c.colorReset(final.buf)
	final.buf.AppendByte('\n')

	return final.buf, nil
}

func (c *consoleEncoder) writeCallSite(final *ltsvEncoder, name string, caller zapcore.EntryCaller) {
	shouldWriteName := name != "" && c.NameKey != ""
	shouldWriteCaller := caller.Defined && c.CallerKey != ""
	if !shouldWriteName && !shouldWriteCaller {
		return
	}
	final.addKey("@")
	if shouldWriteName {
		final.buf.AppendString(name)
		if shouldWriteCaller {
			final.buf.AppendByte('@')
		}
	}
	if shouldWriteCaller {
		final.skipNextElementSeparator = true
		final.EncodeCaller(caller, final)
	}
}

func (c *consoleEncoder) colorDim(buf *buffer.Buffer) {
	c.applyColor(buf, Dim)
}

func (c *consoleEncoder) colorLevel(buf *buffer.Buffer, level zapcore.Level) {
	if c.colorizer != nil {
		c.applyColor(buf, c.colorizer.Level(Level(level)))
	}
}

func (c *consoleEncoder) applyColor(buf *buffer.Buffer, s string) {
	if c.colorizer != nil {
		buf.AppendString(ansi.Reset)
		if s != "" {
			buf.AppendString(s)
		}
	}
}

func (c *consoleEncoder) colorReset(buf *buffer.Buffer) {
	c.applyColor(buf, "")
}
