// Copyright 2017 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package clog

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const (
	FILE               MODE = "file"
	SIMPLE_DATE_FORMAT      = "2006-01-02"
	LOG_PREFIX_LENGTH       = len("2017/02/06 21:20:08 ")
)

// FileRotationConfig represents rotation related configurations for file mode logger.
// All the settings can take effect at the same time, remain zero values to disable them.
type FileRotationConfig struct {
	// Do rotation for output files.
	Rotate bool
	// Rotate on daily basis.
	Daily bool
	// Maximum size in bytes of file for a rotation.
	MaxSize int64
	// Maximum number of lines for a rotation.
	MaxLines int64
	// Maximum lifetime of a output file in days.
	MaxDays int64
}

type FileConfig struct {
	// Minimum level of messages to be processed.
	Level LEVEL
	// Buffer size defines how many messages can be queued before hangs.
	BufferSize int64
	// File name to outout messages.
	Filename string
	// Rotation related configurations.
	FileRotationConfig
}

type file struct {
	// Indicates whether object is been used in standalone mode.
	standalone bool

	*log.Logger
	Adapter

	file         *os.File
	filename     string
	openDay      int
	currentSize  int64
	currentLines int64
	rotate       FileRotationConfig
}

func newFile() Logger {
	return &file{
		Adapter: Adapter{
			quitChan: make(chan struct{}),
		},
	}
}

// NewFileWriter returns an io.Writer for synchronized file logger in standalone mode.
func NewFileWriter(filename string, cfg FileRotationConfig) (io.Writer, error) {
	f := &file{
		standalone: true,
	}
	if err := f.Init(FileConfig{
		Filename:           filename,
		FileRotationConfig: cfg,
	}); err != nil {
		return nil, err
	}

	return f, nil
}

func (f *file) Level() LEVEL { return f.level }

var newLineBytes = []byte("\n")

func (f *file) initFile() (err error) {
	f.file, err = os.OpenFile(f.filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
	if err != nil {
		return fmt.Errorf("OpenFile '%s': %v", f.filename, err)
	}

	f.Logger = log.New(f.file, "", log.Ldate|log.Ltime)
	return nil
}

// isExist checks whether a file or directory exists.
// It returns false when the file or directory does not exist.
func isExist(path string) bool {
	_, err := os.Stat(path)
	return err == nil || os.IsExist(err)
}

// rotateFilename returns next available rotate filename with given date.
func (f *file) rotateFilename(date string) string {
	filename := fmt.Sprintf("%s.%s", f.filename, date)
	if !isExist(filename) {
		return filename
	}

	format := filename + ".%03d"
	for i := 1; i < 1000; i++ {
		filename := fmt.Sprintf(format, i)
		if !isExist(filename) {
			return filename
		}
	}

	panic("too many log files for yesterday")
}

func (f *file) deleteOutdatedFiles() {
	filepath.Walk(filepath.Dir(f.filename), func(path string, info os.FileInfo, err error) error {
		if !info.IsDir() &&
			info.ModTime().Before(time.Now().Add(-24*time.Hour*time.Duration(f.rotate.MaxDays))) &&
			strings.HasPrefix(filepath.Base(path), filepath.Base(f.filename)) {
			os.Remove(path)
		}
		return nil
	})
}

func (f *file) initRotate() error {
	// Gather basic file info for rotation.
	fi, err := f.file.Stat()
	if err != nil {
		return fmt.Errorf("Stat: %v", err)
	}

	f.currentSize = fi.Size()

	// If there is any content in the file, count the number of lines.
	if f.rotate.MaxLines > 0 && f.currentSize > 0 {
		data, err := ioutil.ReadFile(f.filename)
		if err != nil {
			return fmt.Errorf("ReadFile '%s': %v", f.filename, err)
		}

		f.currentLines = int64(bytes.Count(data, newLineBytes)) + 1
	}

	if f.rotate.Daily {
		now := time.Now()
		f.openDay = now.Day()

		lastWriteTime := fi.ModTime()
		if lastWriteTime.Year() != now.Year() ||
			lastWriteTime.Month() != now.Month() ||
			lastWriteTime.Day() != now.Day() {

			if err = f.file.Close(); err != nil {
				return fmt.Errorf("Close: %v", err)
			}
			if err = os.Rename(f.filename, f.rotateFilename(lastWriteTime.Format(SIMPLE_DATE_FORMAT))); err != nil {
				return fmt.Errorf("Rename: %v", err)
			}

			if err = f.initFile(); err != nil {
				return fmt.Errorf("initFile: %v", err)
			}
		}
	}

	if f.rotate.MaxDays > 0 {
		f.deleteOutdatedFiles()
	}
	return nil
}

func (f *file) Init(v interface{}) (err error) {
	cfg, ok := v.(FileConfig)
	if !ok {
		return ErrConfigObject{"FileConfig", v}
	}

	if !isValidLevel(cfg.Level) {
		return ErrInvalidLevel{}
	}
	f.level = cfg.Level

	f.filename = cfg.Filename
	os.MkdirAll(filepath.Dir(f.filename), os.ModePerm)
	if err = f.initFile(); err != nil {
		return fmt.Errorf("initFile: %v", err)
	}

	f.rotate = cfg.FileRotationConfig
	if f.rotate.Rotate {
		f.initRotate()
	}

	if !f.standalone {
		f.msgChan = make(chan *Message, cfg.BufferSize)
	}
	return nil
}

func (f *file) ExchangeChans(errorChan chan<- error) chan *Message {
	f.errorChan = errorChan
	return f.msgChan
}

func (f *file) write(msg *Message) int {
	f.Logger.Print(msg.Body)

	bytesWrote := len(msg.Body)
	if !f.standalone {
		bytesWrote += LOG_PREFIX_LENGTH
	}
	if f.rotate.Rotate {
		f.currentSize += int64(bytesWrote)
		f.currentLines++ // TODO: should I care if log message itself contains new lines?

		var (
			needsRotate = false
			rotateDate  time.Time
		)

		now := time.Now()
		if f.rotate.Daily && now.Day() != f.openDay {
			needsRotate = true
			rotateDate = now.Add(-24 * time.Hour)

		} else if (f.rotate.MaxSize > 0 && f.currentSize >= f.rotate.MaxSize) ||
			(f.rotate.MaxLines > 0 && f.currentLines >= f.rotate.MaxLines) {
			needsRotate = true
			rotateDate = now
		}

		if needsRotate {
			f.file.Close()
			if err := os.Rename(f.filename, f.rotateFilename(rotateDate.Format(SIMPLE_DATE_FORMAT))); err != nil {
				f.errorChan <- fmt.Errorf("fail to rename rotate file '%s': %v", f.filename, err)
			}

			if err := f.initFile(); err != nil {
				f.errorChan <- fmt.Errorf("fail to init log file '%s': %v", f.filename, err)
			}

			f.openDay = now.Day()
			f.currentSize = 0
			f.currentLines = 0
		}
	}
	return bytesWrote
}

var _ io.Writer = new(file)

// Write implements method of io.Writer interface.
func (f *file) Write(p []byte) (int, error) {
	return f.write(&Message{
		Body: string(p),
	}), nil
}

func (f *file) Start() {
LOOP:
	for {
		select {
		case msg := <-f.msgChan:
			f.write(msg)
		case <-f.quitChan:
			break LOOP
		}
	}

	for {
		if len(f.msgChan) == 0 {
			break
		}

		f.write(<-f.msgChan)
	}
	f.quitChan <- struct{}{} // Notify the cleanup is done.
}

func (f *file) Destroy() {
	f.quitChan <- struct{}{}
	<-f.quitChan

	close(f.msgChan)
	close(f.quitChan)

	f.file.Close()
}

func init() {
	Register(FILE, newFile)
}