//
// Copyright (c) 2017 Intel Corporation
//
// 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 tfortools

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"text/tabwriter"
	"text/template"
	"unicode"
	"unicode/utf8"
)

type tableHeading struct {
	name  string
	index int
}

var sortAscMap = map[reflect.Kind]func(interface{}, interface{}) bool{
	reflect.Int: func(v1, v2 interface{}) bool {
		return v1.(int) < v2.(int)
	},
	reflect.Int8: func(v1, v2 interface{}) bool {
		return v1.(int8) < v2.(int8)
	},
	reflect.Int16: func(v1, v2 interface{}) bool {
		return v1.(int16) < v2.(int16)
	},
	reflect.Int32: func(v1, v2 interface{}) bool {
		return v1.(int32) < v2.(int32)
	},
	reflect.Int64: func(v1, v2 interface{}) bool {
		return v1.(int64) < v2.(int64)
	},
	reflect.Uint: func(v1, v2 interface{}) bool {
		return v1.(uint) < v2.(uint)
	},
	reflect.Uint8: func(v1, v2 interface{}) bool {
		return v1.(uint8) < v2.(uint8)
	},
	reflect.Uint16: func(v1, v2 interface{}) bool {
		return v1.(uint16) < v2.(uint16)
	},
	reflect.Uint32: func(v1, v2 interface{}) bool {
		return v1.(uint32) < v2.(uint32)
	},
	reflect.Uint64: func(v1, v2 interface{}) bool {
		return v1.(uint64) < v2.(uint64)
	},
	reflect.Float64: func(v1, v2 interface{}) bool {
		return v1.(float64) < v2.(float64)
	},
	reflect.Float32: func(v1, v2 interface{}) bool {
		return v1.(float32) < v2.(float32)
	},
	reflect.String: func(v1, v2 interface{}) bool {
		return v1.(string) < v2.(string)
	},
}

var sortDscMap = map[reflect.Kind]func(interface{}, interface{}) bool{
	reflect.Int: func(v1, v2 interface{}) bool {
		return v2.(int) < v1.(int)
	},
	reflect.Int8: func(v1, v2 interface{}) bool {
		return v2.(int8) < v1.(int8)
	},
	reflect.Int16: func(v1, v2 interface{}) bool {
		return v2.(int16) < v1.(int16)
	},
	reflect.Int32: func(v1, v2 interface{}) bool {
		return v2.(int32) < v1.(int32)
	},
	reflect.Int64: func(v1, v2 interface{}) bool {
		return v2.(int64) < v1.(int64)
	},
	reflect.Uint: func(v1, v2 interface{}) bool {
		return v2.(uint) < v1.(uint)
	},
	reflect.Uint8: func(v1, v2 interface{}) bool {
		return v2.(uint8) < v1.(uint8)
	},
	reflect.Uint16: func(v1, v2 interface{}) bool {
		return v2.(uint16) < v1.(uint16)
	},
	reflect.Uint32: func(v1, v2 interface{}) bool {
		return v2.(uint32) < v1.(uint32)
	},
	reflect.Uint64: func(v1, v2 interface{}) bool {
		return v2.(uint64) < v1.(uint64)
	},
	reflect.Float64: func(v1, v2 interface{}) bool {
		return v2.(float64) < v1.(float64)
	},
	reflect.Float32: func(v1, v2 interface{}) bool {
		return v2.(float32) < v1.(float32)
	},
	reflect.String: func(v1, v2 interface{}) bool {
		return v2.(string) < v1.(string)
	},
}

type valueSorter struct {
	val   reflect.Value
	field int
	less  func(v1, v2 interface{}) bool
}

func (v *valueSorter) Len() int {
	return v.val.Len()
}

func (v *valueSorter) index(i int) reflect.Value {
	val := v.val.Index(i)
	if val.Kind() == reflect.Ptr {
		val = reflect.Indirect(val)
	}
	return val
}

func (v *valueSorter) Less(i, j int) bool {
	iVal := v.index(i)
	jVal := v.index(j)
	return v.less(iVal.Field(v.field).Interface(), jVal.Field(v.field).Interface())
}

func (v *valueSorter) Swap(i, j int) {
	iVal := v.index(i).Interface()
	jVal := v.index(j).Interface()
	v.index(i).Set(reflect.ValueOf(jVal))
	v.index(j).Set(reflect.ValueOf(iVal))
}

func getValue(obj interface{}) reflect.Value {
	val := reflect.ValueOf(obj)
	if val.Kind() == reflect.Ptr {
		val = reflect.Indirect(val)
	}
	return val
}

func fatalf(name, format string, args ...interface{}) {
	panic(template.ExecError{
		Name: name,
		Err:  fmt.Errorf(format, args...),
	})
}

func newValueSorter(obj interface{}, field string, ascending bool) *valueSorter {
	val := reflect.ValueOf(obj)
	typ := reflect.TypeOf(obj)
	sTyp := typ.Elem()
	if sTyp.Kind() == reflect.Ptr {
		sTyp = sTyp.Elem()
	}

	var index int
	var fTyp reflect.StructField
	for index = 0; index < sTyp.NumField(); index++ {
		fTyp = sTyp.Field(index)
		if fTyp.Name == field {
			break
		}
	}
	if index == sTyp.NumField() {
		fatalf("sort", "%s is not a valid field name", field)
	}
	fKind := fTyp.Type.Kind()

	var lessFn func(interface{}, interface{}) bool
	if ascending {
		lessFn = sortAscMap[fKind]
	} else {
		lessFn = sortDscMap[fKind]
	}
	if lessFn == nil {
		var stringer *fmt.Stringer
		if !fTyp.Type.Implements(reflect.TypeOf(stringer).Elem()) {
			fatalf("sort", "cannot sort fields of type %s", fKind)
		}
		lessFn = func(v1, v2 interface{}) bool {
			return v1.(fmt.Stringer).String() < v2.(fmt.Stringer).String()
		}
	}
	return &valueSorter{
		val:   val,
		field: index,
		less:  lessFn,
	}
}

func findField(fieldPath []string, v reflect.Value) reflect.Value {
	f := v
	for _, seg := range fieldPath {
		f = f.FieldByName(seg)
		if f.Kind() == reflect.Ptr {
			f = reflect.Indirect(f)
		}
	}
	return f
}

func findFieldType(fnName string, fieldPath []string, t reflect.Type) reflect.Type {
	f := t
	for _, seg := range fieldPath {
		sf, found := f.FieldByName(seg)
		if !found {
			fatalf(fnName, "Field %s not found", seg)
		}
		f = sf.Type
		if f.Kind() == reflect.Ptr {
			f = f.Elem()
		}
	}
	return f
}

func filterField(fnName string, obj interface{}, field, val string, cmp func(string, string) bool) interface{} {
	defer func() {
		err := recover()
		if err != nil {
			fatalf(fnName, "Invalid use of filter: %v", err)
		}
	}()

	list := getValue(obj)
	filtered := reflect.MakeSlice(list.Type(), 0, list.Len())

	fieldPath := strings.Split(field, ".")

	for i := 0; i < list.Len(); i++ {
		v := list.Index(i)
		if v.Kind() == reflect.Ptr {
			v = reflect.Indirect(v)
		}

		f := findField(fieldPath, v)

		strVal := fmt.Sprintf("%v", f.Interface())
		if cmp(strVal, val) {
			filtered = reflect.Append(filtered, list.Index(i))
		}
	}

	return filtered.Interface()
}

func filterByField(obj interface{}, field, val string) interface{} {
	return filterField("filter", obj, field, val, func(a, b string) bool {
		return a == b
	})
}

func filterByContains(obj interface{}, field, val string) interface{} {
	return filterField("filterContains", obj, field, val, strings.Contains)
}

func filterByFolded(obj interface{}, field, val string) interface{} {
	return filterField("filterFolded", obj, field, val, strings.EqualFold)
}

func filterByHasPrefix(obj interface{}, field, val string) interface{} {
	return filterField("filterHasPrefix", obj, field, val, strings.HasPrefix)
}

func filterByHasSuffix(obj interface{}, field, val string) interface{} {
	return filterField("filterHasSuffix", obj, field, val, strings.HasSuffix)
}

func filterByRegexp(obj interface{}, field, val string) interface{} {
	return filterField("filterRegexp", obj, field, val, func(a, b string) bool {
		matched, err := regexp.MatchString(b, a)
		if err != nil {
			fatalf("filter", "Invalid regexp: %v", err)
		}
		return matched
	})
}

func selectFieldBase(obj interface{}, field, format string) string {
	defer func() {
		err := recover()
		if err != nil {
			fatalf("select", "Invalid use of select: %v", err)
		}
	}()

	var b bytes.Buffer
	list := getValue(obj)

	fieldPath := strings.Split(field, ".")

	for i := 0; i < list.Len(); i++ {
		v := list.Index(i)
		if v.Kind() == reflect.Ptr {
			v = reflect.Indirect(v)
		}

		f := findField(fieldPath, v)

		fmt.Fprintf(&b, format+"\n", f.Interface())
	}

	return string(b.Bytes())
}

func selectField(obj interface{}, field string) string {
	return selectFieldBase(obj, field, "%v")
}

func selectFieldAlt(obj interface{}, field string) string {
	return selectFieldBase(obj, field, "%#v")
}

func toJSON(obj interface{}) string {
	b, err := json.MarshalIndent(obj, "", "\t")
	if err != nil {
		return ""
	}
	return string(b)
}

func toCSV(obj interface{}, skipHeader ...bool) string {
	var data [][]string
	var buf bytes.Buffer

	data, ok := obj.([][]string)
	if ok {
		_ = csv.NewWriter(&buf).WriteAll(data)
		return buf.String()
	}

	assertCollectionOfStructs("toCSV", reflect.ValueOf(obj))
	v := reflect.ValueOf(obj)
	data = make([][]string, 0, v.Len()+1)
	if len(skipHeader) == 0 || !skipHeader[0] {
		styp := v.Type().Elem()
		if styp.Kind() == reflect.Ptr {
			styp = styp.Elem()
		}
		var row []string
		for i := 0; i < styp.NumField(); i++ {
			field := styp.Field(i)
			if field.PkgPath != "" {
				continue
			}
			row = append(row, field.Name)
		}
		data = append(data, row)
	}
	for i := 0; i < v.Len(); i++ {
		var row []string
		s := v.Index(i)
		if s.Kind() == reflect.Ptr {
			s = s.Elem()
		}
		for j := 0; j < s.NumField(); j++ {
			field := s.Field(j)
			if s.Type().Field(j).PkgPath != "" {
				continue
			}
			row = append(row, fmt.Sprintf("%v", field.Interface()))
		}
		data = append(data, row)
	}

	_ = csv.NewWriter(&buf).WriteAll(data)
	return buf.String()
}

func assertCollectionOfStructs(fnName string, v reflect.Value) {
	typ := v.Type()
	kind := typ.Kind()
	if kind != reflect.Slice && kind != reflect.Array {
		fatalf(fnName, "slice or an array of structs or pointers to structs expected")
	}
	styp := typ.Elem()
	if !(styp.Kind() == reflect.Struct ||
		(styp.Kind() == reflect.Ptr && styp.Elem().Kind() == reflect.Struct)) {
		fatalf(fnName, "slice or an array of structs or pointers to structs expected")
	}
}

func getTableHeadings(fnName string, v reflect.Value) []tableHeading {
	assertCollectionOfStructs(fnName, v)

	typ := v.Type()
	styp := typ.Elem()
	if styp.Kind() == reflect.Ptr {
		styp = styp.Elem()
	}

	var headings []tableHeading
	for i := 0; i < styp.NumField(); i++ {
		field := styp.Field(i)
		if field.PkgPath != "" || ignoreKind(field.Type.Kind()) {
			continue
		}
		headings = append(headings, tableHeading{name: field.Name, index: i})
	}

	if len(headings) == 0 {
		fatalf(fnName, "structures must contain at least one exported non-channel field")
	}
	return headings
}

func createTable(v reflect.Value, minWidth, tabWidth, padding int,
	format string, headings []tableHeading) string {
	var b bytes.Buffer
	w := tabwriter.NewWriter(&b, minWidth, tabWidth, padding, ' ', 0)
	for _, h := range headings {
		fmt.Fprintf(w, "%s\t", h.name)
	}
	fmt.Fprintln(w)

	for i := 0; i < v.Len(); i++ {
		el := v.Index(i)
		if el.Kind() == reflect.Ptr {
			el = el.Elem()
		}
		for _, h := range headings {
			fmt.Fprintf(w, format+"\t", el.Field(h.index).Interface())
		}
		fmt.Fprintln(w)
	}
	_ = w.Flush()

	return b.String()
}

func createHTable(v reflect.Value, minWidth, tabWidth, padding int,
	format string, headings []tableHeading) string {
	var b bytes.Buffer
	w := tabwriter.NewWriter(&b, minWidth, tabWidth, padding, ' ', 0)
	for i := 0; i < v.Len(); i++ {
		if i > 0 {
			fmt.Fprintln(w)
		}
		for _, h := range headings {
			fmt.Fprintf(w, "%s:\t", h.name)
			el := v.Index(i)
			if el.Kind() == reflect.Ptr {
				el = el.Elem()
			}
			fmt.Fprintf(w, format+"\t", el.Field(h.index).Interface())
			fmt.Fprintln(w)
		}
	}
	_ = w.Flush()

	return b.String()
}

func table(obj interface{}) string {
	val := getValue(obj)
	return createTable(val, 8, 8, 1, "%v", getTableHeadings("table", val))
}

func tableAlt(obj interface{}) string {
	val := getValue(obj)
	return createTable(val, 8, 8, 1, "%#v", getTableHeadings("table", val))
}

func xHeadings(fnName string, val reflect.Value, userHeadings []string) []tableHeading {
	headings := getTableHeadings(fnName, val)
	if len(headings) < len(userHeadings) {
		fatalf(fnName, "Too many headings specified.  Max permitted %d got %d",
			len(headings), len(userHeadings))
	}
	for i := range userHeadings {
		headings[i].name = userHeadings[i]
	}
	return headings
}

func tablex(obj interface{}, minWidth, tabWidth, padding int, userHeadings ...string) string {
	val := getValue(obj)
	headings := xHeadings("tablex", val, userHeadings)
	return createTable(val, minWidth, tabWidth, padding, "%v", headings)
}

func tablexAlt(obj interface{}, minWidth, tabWidth, padding int, userHeadings ...string) string {
	val := getValue(obj)
	headings := xHeadings("tablexalt", val, userHeadings)
	return createTable(val, minWidth, tabWidth, padding, "%#v", headings)
}

func htable(obj interface{}) string {
	val := getValue(obj)
	return createHTable(val, 8, 8, 1, "%v", getTableHeadings("htable", val))
}

func htableAlt(obj interface{}) string {
	val := getValue(obj)
	return createHTable(val, 8, 8, 1, "%#v", getTableHeadings("htablealt", val))
}

func htablex(obj interface{}, minWidth, tabWidth, padding int, userHeadings ...string) string {
	val := getValue(obj)
	headings := xHeadings("htablex", val, userHeadings)
	return createHTable(val, minWidth, tabWidth, padding, "%v", headings)
}

func htablexAlt(obj interface{}, minWidth, tabWidth, padding int, userHeadings ...string) string {
	val := getValue(obj)
	headings := xHeadings("htablexalt", val, userHeadings)
	return createHTable(val, minWidth, tabWidth, padding, "%#v", headings)
}

func cols(obj interface{}, fields ...string) interface{} {
	val := getValue(obj)
	assertCollectionOfStructs("cols", val)
	if len(fields) == 0 {
		fatalf("cols", "at least one column name must be specified")
	}

	var newFields []reflect.StructField
	var indicies []int
	styp := val.Type().Elem()
	if styp.Kind() == reflect.Ptr {
		styp = styp.Elem()
	}
	for i := 0; i < styp.NumField(); i++ {
		field := styp.Field(i)
		if field.PkgPath != "" || ignoreKind(field.Type.Kind()) {
			continue
		}

		var j int
		for j = 0; j < len(fields); j++ {
			if fields[j] == field.Name {
				break
			}
		}
		if j == len(fields) {
			continue
		}

		indicies = append(indicies, i)
		newFields = append(newFields, field)
	}

	if len(indicies) != len(fields) {
		fatalf("cols", "not all column names are valid")
	}

	newStyp := reflect.StructOf(newFields)
	newVal := reflect.MakeSlice(reflect.SliceOf(newStyp), val.Len(), val.Len())
	for i := 0; i < val.Len(); i++ {
		sval := val.Index(i)
		if sval.Kind() == reflect.Ptr {
			sval = sval.Elem()
		}
		newSval := reflect.New(newStyp).Elem()
		for j, origIndex := range indicies {
			newSval.Field(j).Set(sval.Field(origIndex))
		}
		newVal.Index(i).Set(newSval)
	}

	return newVal.Interface()
}

func sortSlice(obj interface{}, field string, direction ...string) interface{} {
	ascending := true
	if len(direction) > 1 {
		fatalf("sort", "Too many parameters passed to sort")
	} else if len(direction) == 1 {
		if direction[0] == "dsc" {
			ascending = false
		} else if direction[0] != "asc" {
			fatalf("sort", "direction parameter must be \"asc\" or \"dsc\"")
		}
	}

	val := getValue(obj)
	assertCollectionOfStructs("sort", val)

	copy := reflect.MakeSlice(reflect.SliceOf(val.Type().Elem()), 0, val.Len())
	for i := 0; i < val.Len(); i++ {
		copy = reflect.Append(copy, val.Index(i))
	}

	newobj := copy.Interface()
	vs := newValueSorter(newobj, field, ascending)
	sort.Sort(vs)
	return newobj
}

func rows(obj interface{}, rows ...int) interface{} {
	val := getValue(obj)
	typ := val.Type()
	kind := typ.Kind()
	if kind != reflect.Slice && kind != reflect.Array {
		fatalf("rows", "slice or an array of expected")
	}

	if len(rows) == 0 {
		fatalf("rows", "at least one row index must be specified")
	}

	copy := reflect.MakeSlice(reflect.SliceOf(val.Type().Elem()), 0, len(rows))
	for _, row := range rows {
		if row < val.Len() {
			copy = reflect.Append(copy, val.Index(row))
		}
	}

	return copy.Interface()
}

func assertSliceAndRetrieveCount(fnName string, obj interface{}, count ...int) (reflect.Value, int) {
	val := getValue(obj)
	typ := val.Type()
	kind := typ.Kind()
	if kind != reflect.Slice && kind != reflect.Array {
		fatalf(fnName, "slice or an array of expected")
	}

	rows := 1
	if len(count) == 1 {
		rows = count[0]
	} else if len(count) > 1 {
		fatalf(fnName, "accepts a maximum of two arguments expected")
	}

	return val, rows
}

func head(obj interface{}, count ...int) interface{} {
	val, rows := assertSliceAndRetrieveCount("head", obj, count...)
	copy := reflect.MakeSlice(reflect.SliceOf(val.Type().Elem()), 0, rows)
	for i := 0; i < rows && i < val.Len(); i++ {
		copy = reflect.Append(copy, val.Index(i))
	}

	return copy.Interface()
}

func tail(obj interface{}, count ...int) interface{} {
	val, rows := assertSliceAndRetrieveCount("tail", obj, count...)
	copy := reflect.MakeSlice(reflect.SliceOf(val.Type().Elem()), 0, rows)
	start := val.Len() - rows
	if start < 0 {
		start = 0
	}
	for i := start; i < val.Len(); i++ {
		copy = reflect.Append(copy, val.Index(i))
	}

	return copy.Interface()
}

func promote(obj interface{}, field string) interface{} {
	defer func() {
		err := recover()
		if err != nil {
			fatalf("promote", "Invalid use of promote: %v", err)
		}
	}()

	if len(strings.TrimSpace(field)) == 0 {
		fatalf("promote", "Empty field specifier given")
	}

	fieldPath := strings.Split(field, ".")

	val := getValue(obj)
	assertCollectionOfStructs("promote", val)

	ftype := val.Type().Elem()
	if ftype.Kind() == reflect.Ptr {
		ftype = ftype.Elem()
	}
	ftype = findFieldType("promote", fieldPath, ftype)

	rows := val.Len()
	copy := reflect.MakeSlice(reflect.SliceOf(ftype), 0, rows)
	for i := 0; i < rows; i++ {
		el := val.Index(i)
		if el.Kind() == reflect.Ptr {
			el = reflect.Indirect(el)
		}
		el = findField(fieldPath, el)
		copy = reflect.Append(copy, el)
	}

	return copy.Interface()
}

func sliceof(obj interface{}) interface{} {
	val := reflect.ValueOf(obj)
	sl := reflect.MakeSlice(reflect.SliceOf(val.Type()), 0, 1)
	sl = reflect.Append(sl, val)
	return sl.Interface()
}

func describe(obj interface{}) string {
	var buf bytes.Buffer
	generateIndentedUsage(&buf, obj)
	return buf.String()
}

func sanitizeName(name string) string {
	var buf bytes.Buffer

	name = strings.TrimSpace(name)
	if name == "" {
		return name
	}

	rune, len := utf8.DecodeRuneInString(name)
	if !unicode.IsLetter(rune) {
		_, _ = buf.WriteRune('X')
	} else {
		_, _ = buf.WriteRune(unicode.ToTitle(rune))
		name = name[len:]
	}

	for _, rune := range name {
		if !unicode.IsLetter(rune) && !unicode.IsDigit(rune) {
			rune = '_'
		}
		_, _ = buf.WriteRune(rune)
	}

	return buf.String()
}

func guessType(v string) reflect.Type {
	if p, err := strconv.Atoi(v); err == nil {
		return reflect.TypeOf(p)
	}
	if p, err := strconv.ParseFloat(v, 64); err == nil {
		return reflect.TypeOf(p)
	}
	return reflect.TypeOf("")
}

func toTable(data [][]string) interface{} {
	defer func() {
		err := recover()
		if err != nil {
			fatalf("promote", "Invalid use of toTable: %v", err)
		}
	}()

	if len(data) < 2 {
		panic("Expected at least two rows")
	}

	var fields []reflect.StructField
	for _, f := range data[0] {
		fields = append(fields, reflect.StructField{
			Name: sanitizeName(f),
		})
	}

	for i, v := range data[1] {
		fields[i].Type = guessType(v)
	}

	sTyp := reflect.StructOf(fields)
	newVal := reflect.MakeSlice(reflect.SliceOf(sTyp), len(data)-1, len(data)-1)
	for i, row := range data[1:] {
		sVal := reflect.New(sTyp)
		for j, v := range row {
			f := sVal.Elem().Field(j)
			switch f.Kind() {
			case reflect.Int:
				num, err := strconv.Atoi(v)
				if err != nil {
					panic(fmt.Sprintf("integer expected found %s at (%d, %d)", v, j, i))
				}
				f.Set(reflect.ValueOf(num))
			case reflect.Float64:
				num, err := strconv.ParseFloat(v, 64)
				if err != nil {
					panic(fmt.Sprintf("float expected found %s at (%d, %d)", v, j, i))
				}
				f.Set(reflect.ValueOf(num))
			default:
				f.Set(reflect.ValueOf(v))
			}
		}
		newVal.Index(i).Set(sVal.Elem())
	}

	return newVal.Interface()
}
