package main

import (
	"bufio"
	"bytes"
	"context"
	"flag"
	"fmt"
	"io"
	"log"
	"math"
	"os"
	"strings"
	"sync"
	"time"
)

func LogErr(format string, v ...interface{}) {
	s := fmt.Sprintf(format, v...)
	outer.Output(2, "[ ERR ] "+s)
}

func LogWarn(format string, v ...interface{}) {
	s := fmt.Sprintf(format, v...)
	log.Output(2, "[ Warn] "+s)
}

func LogInfo(format string, v ...interface{}) {
	s := fmt.Sprintf(format, v...)
	log.Output(2, "[ INFO] "+s)
}

func LogFatal(format string, v ...interface{}) {
	s := fmt.Sprintf(format, v...)
	outer.Output(2, "[Fatal] "+s)
	os.Exit(1)
}

var (
	eol    = flag.String("eol", "", "end of line.default is '\n'.")
	eow    = flag.String("eow", "", "end of word.default is ','.")
	word   = flag.String("w", "", "word wrap.default is empty.")
	inputs = flag.String("i", "", "input csv files for merge. multi file contract by ','.")
	out    = flag.String("o", "", "output file name.")
	pk     = flag.String("pk", "", "primary key name. one column and at first column default.")
	tickT  = flag.Int("t", 10, "report every $t second.")
	//
	outer *log.Logger = log.New(os.Stdout, "", log.LstdFlags)
	//
	cm CSVMerge
	wg sync.WaitGroup
)

// csvmerge -o out.csv -i "a.csv,b.csv,c.csv" --eol="\r" --eow="|"

func checkFile(strs []string, cm *CSVMerge) {
	for i := 0; i < len(strs); i++ {
		strs[i] = strings.TrimSpace(strs[i])
	}
	cm.fps = make([]*CMFile, len(strs))
	cm.fps = make([]*CMFile, len(strs))
	var buf = make([]byte, 4*1024*1024)
	var lineCnt int
	for idx, str := range strs {
		fp, err := os.Open(str)
		if err != nil {
			cm.Clean()
			LogFatal("open file(%s) failed:%s", str, err.Error())
		}
		// check lines;
		lineCnt = 0
		for {
			n, err := fp.Read(buf)
			if err != nil {
				if err != io.EOF {
					LogFatal("read file(%s) failed:%s", str, err.Error())
				} else {
					if n == 0 {
						break
					}
				}
			}
			buf = buf[:n]
			lineCnt += bytes.Count(buf, cm.arg.eol)
		}
		fp.Seek(0, io.SeekStart)
		// init fps;
		cm.fps[idx] = &CMFile{
			name:    str,
			fp:      fp,
			rd:      bufio.NewReader(fp),
			lineCnt: 0,
			arg:     &cm.arg,
		}
		LogInfo("file '%s' has %d line", str, lineCnt)
		if cm.totolLine > 0 {
			if math.Abs(float64(cm.totolLine-int64(lineCnt))) > 3 {
				LogFatal("diff too many lines")
			}
		} else {
			cm.totolLine = int64(lineCnt) + 1
		}
	}
}

func parseArgs() {
	flag.Parse()
	//
	str := strings.TrimSpace(*out)
	if len(str) == 0 {
		cm.Clean()
		LogFatal("no output files.please check.")
	}
	fp, err := os.Create(str)
	if err != nil {
		cm.Clean()
		LogFatal("open write file(%s) failed:%s", str, err.Error())
	}
	cm.output = fp
	//
	str = strings.Trim(*eol, " ")
	if len(str) == 0 {
		LogInfo("line split was empty.use default Separator'\\n'")
		str = "\\n"
	}
	cm.arg.eol = convertCRLF(str)
	//
	str = strings.Trim(*eow, " ")
	if len(str) == 0 {
		LogInfo("word split was empty.use Separator ','")
		str = ","
	}
	cm.arg.eow = convertCRLF(str)
	if bytes.Contains(cm.arg.eow, cm.arg.eol) {
		LogFatal("eol(%s,%x) is sub of eow(%s,%x)", toShow(cm.arg.eol), cm.arg.eol, toShow(cm.arg.eow), cm.arg.eow)
	}
	//
	str = strings.TrimSpace(*word)
	// if len(str) == 0 {
	// 	LogWarn("word wrap '%s' was not support.use default \"'\" ", str)
	// 	str = "'" // 单引号.
	// }
	cm.arg.wrap = []byte(str)
	//
	str = strings.TrimSpace(*pk)
	cm.arg.pk = str
	//
	str = strings.TrimSpace(*inputs)
	if len(str) == 0 {
		LogFatal("no input files.please check.")
	}
	strs := strings.Split(str, ",")
	checkFile(strs, &cm)
	//
	LogInfo("merge '%s' --> '%s', eof='%s'(%x) eow='%s'(%x) word-wrap='%s', report-tick=%d'sec.",
		*inputs, *out, toShow(cm.arg.eol), cm.arg.eol, toShow(cm.arg.eow), cm.arg.eow, cm.arg.wrap, *tickT)
}

func toShow(str []byte) string {
	var bs strings.Builder
	for i := 0; i < len(str); i++ {
		switch str[i] {
		case '\r':
			bs.WriteByte('\\')
			bs.WriteByte('r')
		case '\n':
			bs.WriteByte('\\')
			bs.WriteByte('n')
		case '\t':
			bs.WriteByte('\\')
			bs.WriteByte('t')
		case '\v':
			bs.WriteByte('\\')
			bs.WriteByte('v')
		default:
			bs.WriteByte(str[i])
		}
	}
	return bs.String()
}

func convertCRLF(str string) []byte {
	var bs bytes.Buffer
	idx := 0
	for ; idx < len(str); idx++ {
		if str[idx] != '\\' {
			bs.WriteByte(str[idx])
			continue
		}
		idx++
		switch str[idx] {
		case 'r':
			bs.WriteByte('\r')
		case 'n':
			bs.WriteByte('\n')
		case 't':
			bs.WriteByte('\t')
		case 'v':
			bs.WriteByte('\v')
		default:
			log.Fatalf("unknow convert char'%c'", str[idx])
		}
	}
	return bs.Bytes()
}

func stat(ctx context.Context) {
	t := *tickT
	if t < 0 {
		t = 10
	}
	// tick := time.Tick(time.Duration(t) * time.Second)
	d := time.Duration(t) * time.Second
	tick := time.NewTicker(d)
	start := time.Now()
	running := true
	for running {
		select {
		case <-ctx.Done():
			running = false
			break
		case <-tick.C:
			iv := cm.lineCnt * 100 * 100 / cm.totolLine
			tmp := iv % 100
			iv /= 100
			LogInfo(" now has processed %d(%d.%d %%) lines,total is %d lines.", cm.lineCnt, iv, tmp, cm.totolLine)
			tick.Reset(d)
		}
	}
	LogInfo(" now has processed %d(100.0 %%) lines,total is %d lines.", cm.totolLine, cm.totolLine)
	LogInfo("stat go to exiting. cost %v", time.Now().Sub(start).Seconds())
	tick.Stop()
	wg.Done()
}

func main() {

	parseArgs()

	ctx, cancel := context.WithCancel(context.TODO())
	wg.Add(1)
	go stat(ctx)

	cm.Merge()
	cm.Clean()
	//
	cancel()
	LogInfo("csvMerge finished.")
	wg.Wait()
}
