package galog

import (
	"encoding/json"
	"fmt"
	"os"
	"sync"
	"time"
	"unsafe"
)

const (
	AdapterFile = "file"
)

type LogMsg struct {
	Msg  string
	When time.Time
}

// Logger defines the behavior of a log provider.
type LogProvider interface {
	Init(config string) error
	WriteMsg(lm *LogMsg) error
	Destroy()
	Flush()
}

type newLogProviderFunc func() LogProvider

var (
	adapters = make(map[string]newLogProviderFunc)
)

// Register register a new log provider.
func Register(name string, log newLogProviderFunc) {
	if log == nil {
		panic("logs: Register provide is nil")
	}
	if _, dup := adapters[name]; dup {
		panic("logs: Register called twice for provider " + name)
	}
	adapters[name] = log
}

type Logger struct {
	lock         sync.Mutex
	asynchronous bool
	wg           sync.WaitGroup
	msgChanLen   int64
	msgChan      chan *LogMsg
	closeChan    chan struct{}
	flushChan    chan struct{}
	outputs      []*nameLogger
}

type nameLogger struct {
	LogProvider
	name string
}

var logMsgPool *sync.Pool

// newLogger return a new Logger.
func newLogger(channelLens ...int64) *Logger {
	gl := new(Logger)
	gl.msgChanLen = append(channelLens, 0)[0]
	if gl.msgChanLen <= 0 {
		gl.msgChanLen = defaultAsyncMsgLen
	}
	gl.flushChan = make(chan struct{}, 1)
	gl.closeChan = make(chan struct{}, 1)
	return gl
}

// Async sets the log to asynchronous and start the goroutine
func (gl *Logger) Async(msgLen ...int64) *Logger {
	gl.lock.Lock()
	defer gl.lock.Unlock()
	if gl.asynchronous {
		return gl
	}
	gl.asynchronous = true
	if len(msgLen) > 0 && msgLen[0] > 0 {
		gl.msgChanLen = msgLen[0]
	}
	gl.msgChan = make(chan *LogMsg, gl.msgChanLen)
	logMsgPool = &sync.Pool{
		New: func() interface{} {
			return &LogMsg{}
		},
	}
	gl.wg.Add(1)
	go gl.startLogger()
	return gl
}

// SetLogger provides a given logger adapter into Logger with config string.
// config must in in JSON format like {"interval":360}}
func (gl *Logger) setLogger(adapterName string, configs ...string) error {
	config := append(configs, "{}")[0]

	logAdapter, ok := adapters[adapterName]
	if !ok {
		return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName)
	}

	lg := logAdapter()

	err := lg.Init(config)
	if err != nil {
		return err
	}

	gl.outputs = append(gl.outputs, &nameLogger{name: adapterName, LogProvider: lg})
	return nil
}

func (gl *Logger) writeToLoggers(lm *LogMsg) {
	for _, l := range gl.outputs {
		err := l.WriteMsg(lm)
		if err != nil {
			fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err)
		}
	}
}

// Write implements io.Writer.
func (gl *Logger) Write(p []byte) (n int, err error) {
	if len(p) == 0 {
		return 0, nil
	}
	// writeMsg will always add a '\n' character
	if p[len(p)-1] == '\n' {
		p = p[0 : len(p)-1]
	}
	lm := &LogMsg{
		Msg:  string(p),
		When: time.Now(),
	}

	// set levelLoggerImpl to ensure all log message will be write out
	err = gl.writeMsg(lm)
	if err == nil {
		return len(p), nil
	}
	return 0, err
}

func (gl *Logger) writeMsg(lm *LogMsg) error {
	if gl.asynchronous {
		logM := logMsgPool.Get().(*LogMsg)
		logM.Msg = lm.Msg
		logM.When = lm.When

		if gl.outputs != nil {
			gl.msgChan <- lm
		} else {
			logMsgPool.Put(lm)
		}
	} else {
		gl.writeToLoggers(lm)
	}
	return nil
}

func (gl *Logger) startLogger() {
	gameOver := false
	for {
		select {
		case bm, ok := <-gl.msgChan:
			if ok {
				gl.writeToLoggers(bm)
				logMsgPool.Put(bm)
			}
		case <-gl.closeChan:
			gl.flush()
			for _, l := range gl.outputs {
				l.Destroy()
			}
			gl.outputs = nil
			gameOver = true
			gl.wg.Done()
		case <-gl.flushChan:
			gl.flush()
			gl.wg.Done()
		}
		if gameOver {
			break
		}
	}
}

// Info Log INFO level message.
func (gl *Logger) Info(format string) {
	lm := &LogMsg{
		Msg:  format,
		When: time.Now(),
	}

	gl.writeMsg(lm)
}

// BizErr Log a json-interface
func (gl *Logger) BizErr(v interface{}) error {
	buf, err := json.Marshal(v)
	if err != nil {
		return err
	}
	str := *(*string)(unsafe.Pointer(&buf))
	gl.Info(str)
	return nil
}

// Flush flush all chan data.
func (gl *Logger) Flush() {
	if gl.asynchronous {
		gl.flushChan <- struct{}{}
		gl.wg.Wait()
		gl.wg.Add(1)
		return
	}
	gl.flush()
}

// Close close logger, flush all chan data and destroy all adapters in Logger.
func (gl *Logger) Close() {
	if gl.asynchronous {
		gl.closeChan <- struct{}{}
		gl.wg.Wait()
		close(gl.msgChan)
	} else {
		gl.flush()
		for _, l := range gl.outputs {
			l.Destroy()
		}
		gl.outputs = nil
	}
	close(gl.flushChan)
	close(gl.closeChan)
}

func (gl *Logger) flush() {
	if gl.asynchronous {
		for {
			if len(gl.msgChan) > 0 {
				bm, ok := <-gl.msgChan
				if !ok {
					continue
				}
				gl.writeToLoggers(bm)
				logMsgPool.Put(bm)
				continue
			}
			break
		}
	}
	for _, l := range gl.outputs {
		l.Flush()
	}
}
