// Package le_go provides a Golang client library for logging to
// logentries.com over a TCP connection.
//
// it uses an access token for sending log events.
package le_go

import (
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"strings"
	"sync"
	"time"
)

// Logger represents a Logentries logger,
// it holds the open TCP connection, access token, prefix and flags.
//
// all Logger operations are thread safe and blocking,
// log operations can be invoked in a non-blocking way by calling them from
// a goroutine.
type Logger struct {
	conn   net.Conn
	flag   int
	mu     sync.Mutex
	prefix string
	token  string
	buf    []byte
}

const lineSep = "\n"

// Connect creates a new Logger instance and opens a TCP connection to
// logentries.com,
// The token can be generated at logentries.com by adding a new log,
// choosing manual configuration and token based TCP connection.
func Connect(token string) (*Logger, error) {
	logger := Logger{
		token: token,
	}

	if err := logger.openConnection(); err != nil {
		return nil, err
	}

	return &logger, nil
}

// Close closes the TCP connection to logentries.com
func (logger *Logger) Close() error {
	if logger.conn != nil {
		return logger.conn.Close()
	}

	return nil
}

// Opens a TCP connection to logentries.com
func (logger *Logger) openConnection() error {
	conn, err := tls.Dial("tcp", "data.logentries.com:443", &tls.Config{})
	if err != nil {
		return err
	}
	logger.conn = conn
	return nil
}

// It returns if the TCP connection to logentries.com is open
func (logger *Logger) isOpenConnection() bool {
	if logger.conn == nil {
		return false
	}

	buf := make([]byte, 1)

	logger.conn.SetReadDeadline(time.Now())

	_, err := logger.conn.Read(buf)

	switch err.(type) {
	case net.Error:
		if err.(net.Error).Timeout() == true {
			logger.conn.SetReadDeadline(time.Time{})

			return true
		}
	}

	return false
}

// It ensures that the TCP connection to logentries.com is open.
// If the connection is closed, a new one is opened.
func (logger *Logger) ensureOpenConnection() error {
	if !logger.isOpenConnection() {
		if err := logger.openConnection(); err != nil {
			return err
		}
	}

	return nil
}

// Fatal is same as Print() but calls to os.Exit(1)
func (logger *Logger) Fatal(v ...interface{}) {
	logger.Output(2, fmt.Sprint(v...))
	os.Exit(1)
}

// Fatalf is same as Printf() but calls to os.Exit(1)
func (logger *Logger) Fatalf(format string, v ...interface{}) {
	logger.Output(2, fmt.Sprintf(format, v...))
	os.Exit(1)
}

// Fatalln is same as Println() but calls to os.Exit(1)
func (logger *Logger) Fatalln(v ...interface{}) {
	logger.Output(2, fmt.Sprintln(v...))
	os.Exit(1)
}

// Flags returns the logger flags
func (logger *Logger) Flags() int {
	return logger.flag
}

// Output does the actual writing to the TCP connection
func (logger *Logger) Output(calldepth int, s string) error {
	var (
		err        error
		waitPeriod = time.Millisecond
	)
	for {
		_, err = logger.Write([]byte(s))
		if err != nil {
			if connectionErr := logger.openConnection(); connectionErr != nil {
				return connectionErr
			}
			waitPeriod *= 2
			time.Sleep(waitPeriod)
			continue
		}
		return err
	}
}

// Panic is same as Print() but calls to panic
func (logger *Logger) Panic(v ...interface{}) {
	s := fmt.Sprint(v...)
	logger.Output(2, s)
	panic(s)
}

// Panicf is same as Printf() but calls to panic
func (logger *Logger) Panicf(format string, v ...interface{}) {
	s := fmt.Sprintf(format, v...)
	logger.Output(2, s)
	panic(s)
}

// Panicln is same as Println() but calls to panic
func (logger *Logger) Panicln(v ...interface{}) {
	s := fmt.Sprintln(v...)
	logger.Output(2, s)
	panic(s)
}

// Prefix returns the logger prefix
func (logger *Logger) Prefix() string {
	return logger.prefix
}

// Print logs a message
func (logger *Logger) Print(v ...interface{}) error {
	return logger.Output(2, fmt.Sprint(v...))
}

// Printf logs a formatted message
func (logger *Logger) Printf(format string, v ...interface{}) error {
	return logger.Output(2, fmt.Sprintf(format, v...))
}

// Println logs a message with a linebreak
func (logger *Logger) Println(v ...interface{}) error {
	return logger.Output(2, fmt.Sprintln(v...))
}

// SetFlags sets the logger flags
func (logger *Logger) SetFlags(flag int) {
	logger.flag = flag
}

// SetPrefix sets the logger prefix
func (logger *Logger) SetPrefix(prefix string) {
	logger.prefix = prefix
}

// Write writes a bytes array to the Logentries TCP connection,
// it adds the access token and prefix and also replaces
// line breaks with the unicode \u2028 character
func (logger *Logger) Write(p []byte) (n int, err error) {
	logger.mu.Lock()
	if err := logger.ensureOpenConnection(); err != nil {
		return 0, err
	}
	defer logger.mu.Unlock()

	logger.makeBuf(p)

	return logger.conn.Write(logger.buf)
}

// makeBuf constructs the logger buffer
// it is not safe to be used from within multiple concurrent goroutines
func (logger *Logger) makeBuf(p []byte) {
	count := strings.Count(string(p), lineSep)
	p = []byte(strings.Replace(string(p), lineSep, "\u2028", count-1))

	logger.buf = logger.buf[:0]
	logger.buf = append(logger.buf, (logger.token + " ")...)
	logger.buf = append(logger.buf, (logger.prefix + " ")...)
	logger.buf = append(logger.buf, p...)

	if !strings.HasSuffix(string(logger.buf), lineSep) {
		logger.buf = append(logger.buf, (lineSep)...)
	}
}
