package log

import (
	"fmt"
	"io"
	"strings"
	"sync"
)

type devLogger struct {
	w    io.Writer
	lock *sync.Mutex
}

var _ logEventWriter = &devLogger{}

func newDevLogEventWriter(w io.Writer) *devLogger {
	return &devLogger{
		w:    w,
		lock: &sync.Mutex{},
	}
}

// log writes the given logEvent to its io.Writer, using the following format:
//
// time level message [ field1=value1 field2=value2 ]
//   error 1 message [ field1=value1 field2=value2 ]
//     stack trace
//     stack trace
//   error 2 message [ field1=value1 field2=value2 ]
//     stack trace
//     stack trace
func (d *devLogger) log(logEvent *LogEvent) {
	if logEvent == nil {
		return
	}

	payload := getLogEventPayload(logEvent)

	d.lock.Lock()
	defer d.lock.Unlock()

	p := newPrinter(d.w)

	d.writeFirstLine(p, payload)
	p.addNewline()
	d.writeErrorPayloads(p, payload.ErrorChain)
	p.addNewline()
}

func (d *devLogger) writeFirstLine(p *printer, event *logEventPayload) {
	if event == nil {
		return
	}

	if !event.Time.IsZero() {
		localTime := event.Time.Local()
		p.append(localTime.Format("2006-01-02 15:04:05-07"))
	}

	p.addSeparator()
	p.append(event.Level)
	p.addSeparator()
	p.append(event.Message)
	d.writeFields(p, event.Fields)
}

func (d *devLogger) writeFields(p *printer, fields map[string]interface{}) {
	if len(fields) == 0 {
		return
	}

	p.addSeparator()
	p.append("[")

	for key, val := range fields {
		valString := fmt.Sprintf("%v", val)
		keyVal := fmt.Sprintf("%s=%q", key, valString)

		p.addSeparator()
		p.append(keyVal)
	}

	p.addSeparator()
	p.append("]")
}

func (d *devLogger) writeErrorPayloads(p *printer, errorPayloads []*errorPayload) {
	if len(errorPayloads) == 0 {
		return
	}

	for _, errorPayload := range errorPayloads {
		d.writeErrorPayloadCompactFormat(p, errorPayload)
	}
}

func (d *devLogger) writeErrorPayloadCompactFormat(p *printer, errorPayload *errorPayload) {
	if errorPayload == nil {
		return
	}

	p.addNewline()
	p.indent(1)
	p.append(errorPayload.Message)
	p.addSeparator()
	d.writeFields(p, errorPayload.Fields)

	for _, stackFrame := range errorPayload.Stack {
		p.addNewline()
		p.indent(2)
		p.append(stackFrame.Method)
		p.addNewline()
		p.indent(3)
		p.append(fmt.Sprintf("%s:%d", stackFrame.File, stackFrame.Line))
	}
}

// printer simplifies printing the log by keeping track of the whitespace that is written.
type printer struct {
	w              io.Writer
	needsSeparator bool
	needsNewline   bool
	needsIndent    bool
}

func newPrinter(w io.Writer) *printer {
	return &printer{
		w:              w,
		needsSeparator: false,
		needsNewline:   false,
		needsIndent:    true,
	}
}

// append writes the string to the writer
func (p *printer) append(s string) {
	if s == "" {
		return
	}

	_, _ = io.WriteString(p.w, s)

	p.needsSeparator = true
	p.needsNewline = true
	p.needsIndent = false
}

// addSeparator writes a space to separate items, unless we've already written a space.
func (p *printer) addSeparator() {
	if p.needsSeparator {
		_, _ = io.WriteString(p.w, " ")
		p.needsSeparator = false
	}
}

// addNewline writes a new line, unless we're already at the beginning of a line.
func (p *printer) addNewline() {
	if p.needsNewline {
		_, _ = io.WriteString(p.w, "\n")
		p.needsNewline = false
		p.needsIndent = true
	}
}

// indent adds a few spaces if we're at the beginning of the line, otherwise does nothing.
func (p *printer) indent(level int) {
	if p.needsIndent {
		_, _ = io.WriteString(p.w, strings.Repeat("  ", level))
		p.needsSeparator = false
		p.needsIndent = false
	}
}
