package display

import (
	"fmt"
	"log"
	"os"
	"sort"
	"strings"

	// regular tabwriter do not work with color, this is a forked versions that ignores color special characters
	"github.com/Ladicle/tabwriter"
	"github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/types"
	"github.com/percona/percona-toolkit/src/go/pt-galera-log-explainer/utils"
)

// TimelineCLI print a timeline to the terminal using tabulated format
// It will print header and footers, and dequeue the timeline chronologically
func TimelineCLI(timeline types.Timeline, verbosity types.Verbosity) {

	timeline = removeEmptyColumns(timeline, verbosity)

	// to hold the current context for each node
	// "keys" is needed, because iterating over a map must give a different order each time
	// a slice keeps its order
	keys, currentContext := initKeysContext(timeline)           // currentcontext to follow when important thing changed
	latestContext := timeline.GetLatestContextsByNodes()        // so that we have fully updated context when we print
	lastContext := make(map[string]types.LogCtx, len(timeline)) // just to follow when important thing changed

	w := tabwriter.NewWriter(os.Stdout, 8, 8, 3, ' ', tabwriter.DiscardEmptyColumns)
	defer w.Flush()

	// header
	fmt.Fprintln(w, headerNodes(keys))
	fmt.Fprintln(w, headerFilePath(keys, currentContext))
	fmt.Fprintln(w, headerIP(keys, latestContext))
	fmt.Fprintln(w, headerName(keys, latestContext))
	fmt.Fprintln(w, headerVersion(keys, latestContext))
	fmt.Fprintln(w, separator(keys))

	var (
		args      []string // stuff to print
		linecount int
	)

	// as long as there is a next event to print
	for nextNodes := timeline.IterateNode(); len(nextNodes) != 0; nextNodes = timeline.IterateNode() {

		// Date column
		date := timeline[nextNodes[0]][0].Date
		args = []string{""}
		if date != nil {
			args = []string{date.DisplayTime}
		}

		displayedValue := 0

		// node values
		for _, node := range keys {

			if !utils.SliceContains(nextNodes, node) {
				// if there are no events, having a | is needed for tabwriter
				// A few color can also help highlighting how the node is doing
				logCtx := currentContext[node]
				args = append(args, utils.PaintForState("| ", logCtx.State()))
				continue
			}
			loginfo := timeline[node][0]
			lastContext[node] = currentContext[node]
			currentContext[node] = loginfo.LogCtx

			timeline.Dequeue(node)

			msg := loginfo.Msg(latestContext[node])
			if verbosity >= loginfo.Verbosity && msg != "" {
				args = append(args, msg)
				displayedValue++
			} else {
				args = append(args, utils.PaintForState("| ", loginfo.LogCtx.State()))
			}
		}

		if sep := transitionSeparator(keys, lastContext, currentContext); sep != "" {
			// reset current context, so that we avoid duplicating transitions
			// lastContext/currentContext is only useful for that anyway
			lastContext = map[string]types.LogCtx{}
			for k, v := range currentContext {
				lastContext[k] = v
			}
			// print transition
			fmt.Fprintln(w, sep)
		}

		// If line is not filled with default placeholder values
		if displayedValue == 0 {
			continue

		}

		// Print tabwriter line
		_, err := fmt.Fprintln(w, strings.Join(args, "\t")+"\t")
		if err != nil {
			log.Println("Failed to write a line", err)
		}
		linecount++
	}

	// footer
	// only having a header is not fast enough to read when there are too many lines
	if linecount >= 50 {
		fmt.Fprintln(w, separator(keys))
		fmt.Fprintln(w, headerNodes(keys))
		fmt.Fprintln(w, headerFilePath(keys, currentContext))
		fmt.Fprintln(w, headerIP(keys, currentContext))
		fmt.Fprintln(w, headerName(keys, currentContext))
		fmt.Fprintln(w, headerVersion(keys, currentContext))
	}

	// TODO: where to print conflicts details ?
}

func initKeysContext(timeline types.Timeline) ([]string, map[string]types.LogCtx) {
	currentContext := map[string]types.LogCtx{}

	// keys will be used to access the timeline map with an ordered manner
	// without this, we would not print on the correct column as the order of a map is guaranteed to be random each time
	keys := make([]string, 0, len(timeline))
	for node := range timeline {
		keys = append(keys, node)
		if len(timeline[node]) > 0 {
			currentContext[node] = timeline[node][0].LogCtx
		} else {
			// Avoid crashing, but not ideal: we could have a better default Ctx with filepath at least
			currentContext[node] = types.NewLogCtx()
		}
	}
	sort.Strings(keys)
	return keys, currentContext
}

func separator(keys []string) string {
	return " \t" + strings.Repeat(" \t", len(keys))
}

func headerNodes(keys []string) string {
	return "identifier\t" + strings.Join(keys, "\t") + "\t"
}

func headerFilePath(keys []string, logCtxs map[string]types.LogCtx) string {
	header := "current path\t"
	for _, node := range keys {
		if logCtx, ok := logCtxs[node]; ok {
			if len(logCtx.FilePath) < 50 {
				header += logCtx.FilePath + "\t"
			} else {
				header += "..." + logCtx.FilePath[len(logCtx.FilePath)-50:] + "\t"
			}
		} else {
			header += " \t"
		}
	}
	return header
}

func headerIP(keys []string, logCtxs map[string]types.LogCtx) string {
	header := "last known ip\t"
	for _, node := range keys {
		if logCtx, ok := logCtxs[node]; ok && len(logCtx.OwnIPs) > 0 {
			header += logCtx.OwnIPs[len(logCtx.OwnIPs)-1] + "\t"
		} else {
			header += " \t"
		}
	}
	return header
}

func headerVersion(keys []string, logCtxs map[string]types.LogCtx) string {
	header := "mysql version\t"
	for _, node := range keys {
		if logCtx, ok := logCtxs[node]; ok {
			header += logCtx.Version + "\t"
		}
	}
	return header
}

func headerName(keys []string, logCtxs map[string]types.LogCtx) string {
	header := "last known name\t"
	for _, node := range keys {
		if logCtx, ok := logCtxs[node]; ok && len(logCtx.OwnNames) > 0 {
			header += logCtx.OwnNames[len(logCtx.OwnNames)-1] + "\t"
		} else {
			header += " \t"
		}
	}
	return header
}

func removeEmptyColumns(timeline types.Timeline, verbosity types.Verbosity) types.Timeline {

	for key := range timeline {
		if !timeline[key][len(timeline[key])-1].LogCtx.HasVisibleEvents(verbosity) {
			delete(timeline, key)
		}
	}
	return timeline
}

// transition is to builds the check+display of an important context transition
// like files, IP, name, anything
// summary will hold the whole multi-line report
type transition struct {
	s1, s2, changeType string
	ok                 bool
	summary            transitionSummary
}

// transitions will hold any number of transition to test
// transitionToPrint will hold whatever transition happened, but will also store empty transitions
// to ensure that every columns will have the same amount of rows to write: this is needed to maintain
// the columnar output
type transitions struct {
	tests             []*transition
	transitionToPrint []*transition
	numberFound       int
}

// 4 here means there are 4 rows to store
// 0: base info, 1: type of info that changed, 2: just an arrow placeholder, 3: new info
const RowPerTransitions = 4

type transitionSummary [RowPerTransitions]string

// because only those transitions are implemented: file path, ip, node name, version
const NumberOfPossibleTransition = 4

// transactionSeparator is useful to highlight a change of context
// example, changing file
//   mysqld.log.2
//    (file path)
//           V
//   mysqld.log.1
// or a change of ip, node name, ...
// This feels complicated: it is
// It was made difficult because of how "tabwriter" works
// it needs an element on each columns so that we don't break columns
// The rows can't have a variable count of elements: it has to be strictly identical each time
// so the whole next functions are here to ensure it takes minimal spaces, while giving context and preserving columns
func transitionSeparator(keys []string, oldlogCtxs, logCtxs map[string]types.LogCtx) string {

	ts := map[string]*transitions{}

	// For each columns to print, we build tests
	for _, node := range keys {
		logCtx, ok1 := logCtxs[node]
		oldlogCtx, ok2 := oldlogCtxs[node]

		ts[node] = &transitions{tests: []*transition{}}
		if ok1 && ok2 {
			ts[node].tests = append(ts[node].tests, &transition{s1: oldlogCtx.FilePath, s2: logCtx.FilePath, changeType: "file path"})

			if len(oldlogCtx.OwnNames) > 0 && len(logCtx.OwnNames) > 0 {
				ts[node].tests = append(ts[node].tests, &transition{s1: oldlogCtx.OwnNames[len(oldlogCtx.OwnNames)-1], s2: logCtx.OwnNames[len(logCtx.OwnNames)-1], changeType: "node name"})
			}
			if len(oldlogCtx.OwnIPs) > 0 && len(logCtx.OwnIPs) > 0 {
				ts[node].tests = append(ts[node].tests, &transition{s1: oldlogCtx.OwnIPs[len(oldlogCtx.OwnIPs)-1], s2: logCtx.OwnIPs[len(logCtx.OwnIPs)-1], changeType: "node ip"})
			}
			if oldlogCtx.Version != "" && logCtx.Version != "" {
				ts[node].tests = append(ts[node].tests, &transition{s1: oldlogCtx.Version, s2: logCtx.Version, changeType: "version"})
			}

		}

		// we resolve tests
		ts[node].fillEmptyTransition()
		ts[node].iterate()
	}

	highestStackOfTransitions := 0

	// we need to know the maximum height to print
	for _, node := range keys {
		if ts[node].numberFound > highestStackOfTransitions {
			highestStackOfTransitions = ts[node].numberFound
		}
	}
	// now we have the height, we compile the stack to print (possibly empty placeholders for some columns)
	for _, node := range keys {
		ts[node].stackPrioritizeFound(highestStackOfTransitions)
	}

	out := "\t"
	for i := 0; i < highestStackOfTransitions; i++ {
		for row := 0; row < RowPerTransitions; row++ {
			for _, node := range keys {
				out += ts[node].transitionToPrint[i].summary[row]
			}
			if !(i == highestStackOfTransitions-1 && row == RowPerTransitions-1) { // unless last row
				out += "\n\t"
			}
		}
	}

	if out == "\t" {
		return ""
	}
	return out
}

func (ts *transitions) iterate() {

	for _, test := range ts.tests {

		test.summarizeIfDifferent()
		if test.ok {
			ts.numberFound++
		}
	}

}

func (ts *transitions) stackPrioritizeFound(height int) {
	for i, test := range ts.tests {
		// if at the right height
		if len(ts.tests)-i+len(ts.transitionToPrint) == height {
			ts.transitionToPrint = append(ts.transitionToPrint, ts.tests[i:]...)
		}
		if test.ok {
			ts.transitionToPrint = append(ts.transitionToPrint, test)
		}
	}
}

func (ts *transitions) fillEmptyTransition() {
	if len(ts.tests) == NumberOfPossibleTransition {
		return
	}
	for i := len(ts.tests); i < NumberOfPossibleTransition; i++ {
		ts.tests = append(ts.tests, &transition{s1: "", s2: "", changeType: ""})
	}

}

func (t *transition) summarizeIfDifferent() {
	if t.s1 != t.s2 {
		t.summary = [RowPerTransitions]string{utils.Paint(utils.BrightBlueText, t.s1), utils.Paint(utils.BlueText, "("+t.changeType+")"), utils.Paint(utils.BrightBlueText, " V "), utils.Paint(utils.BrightBlueText, t.s2)}
		t.ok = true
	}
	for i := range t.summary {
		t.summary[i] = t.summary[i] + "\t"
	}
	return
}
