// Package cmdlog implements color leveled logging for command line tools.
package cmdlog

import (
	"bytes"
	"io"
	"log"
	"os"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/d2lang/util-go/xos"
	"github.com/d2lang/util-go/xterm"
)

var timeNow = time.Now

const defaultTSFormat = "15:04:05"

func init() {
	l := New(xos.NewEnv(os.Environ()), os.Stderr)
	l.SetTS(true)
	l = l.WithPrefix(xterm.Blue, "stdlog")

	log.SetOutput(l.NoLevel.Writer())
	log.SetPrefix(l.NoLevel.Prefix())
	log.SetFlags(l.NoLevel.Flags())
}

type Logger struct {
	env *xos.Env
	w   io.Writer
	tsw *tsWriter
	dw  *debugWriter

	NoLevel *log.Logger
	Debug   *log.Logger
	Success *log.Logger
	Info    *log.Logger
	Warn    *log.Logger
	Error   *log.Logger
}

func (l *Logger) GetTS() bool {
	l.tsw.mu.Lock()
	defer l.tsw.mu.Unlock()
	return l.tsw.enabled
}

func (l *Logger) GetTSFormat() string {
	l.tsw.mu.Lock()
	defer l.tsw.mu.Unlock()
	return l.tsw.tsfmt
}

func (l *Logger) GetDebug() bool {
	return l.dw.debug()
}

func (l *Logger) SetTS(enabled bool) {
	l.tsw.mu.Lock()
	l.tsw.enabled = enabled
	l.tsw.mu.Unlock()
}

func (l *Logger) SetTSFormat(tsfmt string) {
	l.tsw.mu.Lock()
	l.tsw.tsfmt = tsfmt
	l.tsw.mu.Unlock()
}

func (l *Logger) SetDebug(enabled bool) {
	vi := int64(0)
	if enabled {
		vi = 1
	}
	atomic.StoreInt64(&l.dw.flag, vi)
}

func New(env *xos.Env, w io.Writer) *Logger {
	tsw := &tsWriter{w: w, tsfmt: defaultTSFormat}
	dw := &debugWriter{w: tsw, env: env}
	l := &Logger{
		env: env,
		w:   w,
		dw:  dw,
		tsw: tsw,
	}
	l.init("")
	return l
}

func (l *Logger) init(prefix string) {
	l.NoLevel = log.New(prefixWriter{l.tsw, prefix}, "", 0)

	if prefix != "" {
		prefix += " "
	}
	l.Debug = log.New(prefixWriter{l.dw, prefix + xterm.Prefix(l.env, l.w, "", "debug")}, "", 0)
	l.Success = log.New(prefixWriter{l.tsw, prefix + xterm.Prefix(l.env, l.w, xterm.Green, "success")}, "", 0)
	l.Info = log.New(prefixWriter{l.tsw, prefix + xterm.Prefix(l.env, l.w, xterm.Blue, "info")}, "", 0)
	l.Warn = log.New(prefixWriter{l.tsw, prefix + xterm.Prefix(l.env, l.w, xterm.Yellow, "warn")}, "", 0)
	l.Error = log.New(prefixWriter{l.tsw, prefix + xterm.Prefix(l.env, l.w, xterm.Red, "err")}, "", 0)
}

type prefixWriter struct {
	w      io.Writer
	prefix string
}

func (pw prefixWriter) Write(p []byte) (int, error) {
	lines := bytes.Split(p, []byte("\n"))
	p2 := make([]byte, 0, (len(pw.prefix)+1)*len(lines)+len(p))

	for _, l := range lines[:len(lines)-1] {
		prefix := pw.prefix
		if len(l) > 0 {
			prefix += " "
		}
		p2 = append(p2, prefix...)
		p2 = append(p2, l...)
		p2 = append(p2, '\n')
	}

	n, err := pw.w.Write(p2)
	if n > len(p) {
		n = len(p)
	}
	return n, err
}

type debugWriter struct {
	w    io.Writer
	flag int64
	env  *xos.Env
}

func (dw *debugWriter) debug() bool {
	if atomic.LoadInt64(&dw.flag) == 0 {
		return dw.env.Debug()
	}
	return true
}

func (dw *debugWriter) Write(p []byte) (int, error) {
	if !dw.debug() {
		return len(p), nil
	}
	return dw.w.Write(p)
}

type tsWriter struct {
	w io.Writer

	mu      sync.Mutex
	tsfmt   string
	enabled bool
}

func (tsw *tsWriter) Write(p []byte) (int, error) {
	tsw.mu.Lock()
	enabled := tsw.enabled
	tsfmt := tsw.tsfmt
	tsw.mu.Unlock()

	if !enabled {
		return tsw.w.Write(p)
	}

	ts := timeNow().Format(tsfmt)
	prefix := []byte("[" + ts + "]")

	lines := bytes.Split(p, []byte("\n"))
	p2 := make([]byte, 0, (len(prefix)+1)*len(lines)+len(p))

	for _, l := range lines[:len(lines)-1] {
		prefix := prefix
		if len(l) > 0 {
			prefix = append(prefix, ' ')
		}
		p2 = append(p2, prefix...)
		p2 = append(p2, l...)
		p2 = append(p2, '\n')
	}

	n, err := tsw.w.Write(p2)
	if n > len(p) {
		n = len(p)
	}
	return n, err
}

func NewTB(env *xos.Env, tb testing.TB) *Logger {
	return New(env, tbWriter{tb})
}

type tbWriter struct {
	tb testing.TB
}

func (tbw tbWriter) Write(p []byte) (int, error) {
	tbw.tb.Logf("%s", p)
	return len(p), nil
}

// Allows detection as a terminal.
func (tbWriter) Fd() uintptr {
	return os.Stderr.Fd()
}

func (l *Logger) WithCCPrefix(s string) *Logger {
	return l.withPrefix(xterm.CCPrefix(l.env, l.w, s))
}

func (l *Logger) WithPrefix(caps, s string) *Logger {
	return l.withPrefix(xterm.Prefix(l.env, l.w, caps, s))
}

func (l *Logger) withPrefix(s string) *Logger {
	l2 := new(Logger)
	*l2 = *l

	prefix := l.NoLevel.Writer().(prefixWriter).prefix
	if len(s) > 0 {
		if len(prefix) > 0 {
			prefix += " "
		}
		prefix += s
	}
	l2.init(prefix)
	return l2
}
