// Copyright 2019 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 logfmt

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strconv"
	"time"
	"unicode/utf8"

	"golang.org/x/exp/event"
)

// TODO: some actual research into what this arbritray optimization number should be
const bufCap = 50

const TimeFormat = "2006/01/02 15:04:05"

type Printer struct {
	QuoteValues       bool
	SuppressNamespace bool
	buf               [bufCap]byte
	needSep           bool
	w                 bytes.Buffer
}

type Handler struct {
	to io.Writer
	Printer
}

// NewHandler returns a handler that prints the events to the supplied writer.
// Each event is printed in logfmt format on a single line.
func NewHandler(to io.Writer) *Handler {
	return &Handler{to: to}
}

func (h *Handler) Event(ctx context.Context, ev *event.Event) context.Context {
	h.Printer.Event(h.to, ev)
	return ctx
}

func (p *Printer) Event(w io.Writer, ev *event.Event) {
	p.needSep = false
	if !ev.At.IsZero() {
		p.separator(w)
		io.WriteString(w, `time="`)
		p.time(w, ev.At)
		io.WriteString(w, `"`)
	}

	if !p.SuppressNamespace && ev.Source.Space != "" {
		p.Label(w, event.String("in", ev.Source.Space))
	}
	if ev.Source.Owner != "" {
		p.Label(w, event.String("owner", ev.Source.Owner))
	}
	if ev.Source.Name != "" {
		p.Label(w, event.String("name", ev.Source.Name))
	}

	if ev.Parent != 0 {
		p.separator(w)
		io.WriteString(w, `parent=`)
		w.Write(strconv.AppendUint(p.buf[:0], ev.Parent, 10))
	}
	for _, l := range ev.Labels {
		if l.Name == "" {
			continue
		}
		p.Label(w, l)
	}

	if ev.ID != 0 && ev.Kind == event.StartKind {
		p.separator(w)
		io.WriteString(w, `trace=`)
		w.Write(strconv.AppendUint(p.buf[:0], ev.ID, 10))
	}

	if ev.Kind == event.EndKind {
		p.separator(w)
		io.WriteString(w, `end`)
	}

	io.WriteString(w, "\n")
}

func (p *Printer) separator(w io.Writer) {
	if p.needSep {
		io.WriteString(w, " ")
	}
	p.needSep = true
}

func (p *Printer) Label(w io.Writer, l event.Label) {
	if l.Name == "" {
		return
	}
	p.separator(w)
	p.Ident(w, l.Name)
	if l.HasValue() {
		io.WriteString(w, "=")
		switch {
		case l.IsString():
			p.string(w, l.String())
		case l.IsBytes():
			p.bytes(w, l.Bytes())
		case l.IsInt64():
			w.Write(strconv.AppendInt(p.buf[:0], l.Int64(), 10))
		case l.IsUint64():
			w.Write(strconv.AppendUint(p.buf[:0], l.Uint64(), 10))
		case l.IsFloat64():
			w.Write(strconv.AppendFloat(p.buf[:0], l.Float64(), 'g', -1, 64))
		case l.IsBool():
			if l.Bool() {
				io.WriteString(w, "true")
			} else {
				io.WriteString(w, "false")
			}
		default:
			v := l.Interface()
			switch v := v.(type) {
			case string:
				p.string(w, v)
			case fmt.Stringer:
				p.string(w, v.String())
			default:
				if p.w.Cap() == 0 {
					// we rely on the inliner to cause this to not allocate
					p.w = *bytes.NewBuffer(p.buf[:0])
				}
				fmt.Fprint(&p.w, v)
				b := p.w.Bytes()
				p.w.Reset()
				p.bytes(w, b)
			}
		}
	}
}

func (p *Printer) Ident(w io.Writer, s string) {
	if !stringNeedQuote(s) {
		io.WriteString(w, s)
		return
	}
	p.quoteString(w, s)
}

func (p *Printer) string(w io.Writer, s string) {
	if p.QuoteValues || stringNeedQuote(s) {
		p.quoteString(w, s)
	} else {
		io.WriteString(w, s)
	}
}

func (p *Printer) quoteString(w io.Writer, s string) {
	io.WriteString(w, `"`)
	written := 0
	for offset, r := range s {
		q := quoteRune(r)
		if len(q) == 0 {
			continue
		}
		// write out any prefix
		io.WriteString(w, s[written:offset])
		written = offset + utf8.RuneLen(r)
		// and write out the quoted rune
		io.WriteString(w, q)
	}
	io.WriteString(w, s[written:])
	io.WriteString(w, `"`)
}

func (p *Printer) bytes(w io.Writer, buf []byte) {
	if p.QuoteValues || stringNeedQuote(string(buf)) {
		p.quoteBytes(w, buf)
	} else {
		w.Write(buf)
	}
}

// Bytes writes a byte array in string form to the printer.
func (p *Printer) quoteBytes(w io.Writer, buf []byte) {
	io.WriteString(w, `"`)
	written := 0
	for offset := 0; offset < len(buf); {
		r, size := utf8.DecodeRune(buf[offset:])
		offset += size
		q := quoteRune(r)
		if len(q) == 0 {
			continue
		}
		// write out any prefix
		w.Write(buf[written : offset-size])
		written = offset
		// and write out the quoted rune
		io.WriteString(w, q)
	}
	w.Write(buf[written:])
	io.WriteString(w, `"`)
}

// time writes a timstamp in the same format as
func (p *Printer) time(w io.Writer, t time.Time) {
	year, month, day := t.Date()
	hour, minute, second := t.Clock()
	p.padInt(w, int64(year), 4)
	io.WriteString(w, `/`)
	p.padInt(w, int64(month), 2)
	io.WriteString(w, `/`)
	p.padInt(w, int64(day), 2)
	io.WriteString(w, ` `)
	p.padInt(w, int64(hour), 2)
	io.WriteString(w, `:`)
	p.padInt(w, int64(minute), 2)
	io.WriteString(w, `:`)
	p.padInt(w, int64(second), 2)
}

func (p *Printer) padInt(w io.Writer, v int64, width int) {
	b := strconv.AppendInt(p.buf[:0], int64(v), 10)
	if len(b) < width {
		io.WriteString(w, "0000"[:width-len(b)])
	}
	w.Write(b)
}

func stringNeedQuote(s string) bool {
	if len(s) == 0 {
		return true
	}
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c >= utf8.RuneSelf || c == ' ' || c == '"' || c == '\n' || c == '\\' {
			return true
		}
	}
	return false
}

func quoteRune(r rune) string {
	switch r {
	case '"':
		return `\"`
	case '\n':
		return `\n`
	case '\\':
		return `\\`
	default:
		return ``
	}
}
