// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package print

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"github.com/olekukonko/tablewriter"
	"gopkg.in/yaml.v3"
)

// table provides infrastructure to easily print (sorted) lists in different formats
type table struct {
	headers    []string
	values     [][]string
	sortDesc   bool // used internally by sortable interface
	sortColumn uint // ↑
}

// printable can be implemented for structs to put fields dynamically into a table
type printable interface {
	FormatField(field string, machineReadable bool) string
}

// high level api to print a table of items with dynamic fields
func tableFromItems(fields []string, values []printable, machineReadable bool) table {
	t := table{headers: fields}
	for _, v := range values {
		row := make([]string, len(fields))
		for i, f := range fields {
			row[i] = v.FormatField(f, machineReadable)
		}
		t.addRowSlice(row)
	}
	return t
}

func tableWithHeader(header ...string) table {
	return table{headers: header}
}

// it's the callers responsibility to ensure row length is equal to header length!
func (t *table) addRow(row ...string) {
	t.addRowSlice(row)
}

// it's the callers responsibility to ensure row length is equal to header length!
func (t *table) addRowSlice(row []string) {
	t.values = append(t.values, row)
}

func (t *table) sort(column uint, desc bool) {
	t.sortColumn = column
	t.sortDesc = desc
	sort.Stable(t) // stable to allow multiple calls to sort
}

// sortable interface
func (t table) Len() int      { return len(t.values) }
func (t table) Swap(i, j int) { t.values[i], t.values[j] = t.values[j], t.values[i] }
func (t table) Less(i, j int) bool {
	if t.sortDesc {
		i, j = j, i
	}
	return t.values[i][t.sortColumn] < t.values[j][t.sortColumn]
}

func (t *table) print(output string) error {
	return t.fprint(os.Stdout, output)
}

func (t *table) fprint(f io.Writer, output string) error {
	switch output {
	case "", "table":
		return outputTable(f, t.headers, t.values)
	case "csv":
		return outputDsv(f, t.headers, t.values, ',')
	case "simple":
		return outputSimple(f, t.headers, t.values)
	case "tsv":
		return outputDsv(f, t.headers, t.values, '\t')
	case "yml", "yaml":
		return outputYaml(f, t.headers, t.values)
	case "json":
		return outputJSON(f, t.headers, t.values)
	default:
		return fmt.Errorf("unknown output type %q, available types are: csv, simple, table, tsv, yaml, json", output)
	}
}

// outputTable prints structured data as table
func outputTable(f io.Writer, headers []string, values [][]string) error {
	table := tablewriter.NewWriter(f)
	if len(headers) > 0 {
		table.Header(headers)
	}
	for _, value := range values {
		if err := table.Append(value); err != nil {
			return err
		}
	}
	return table.Render()
}

// outputSimple prints structured data as space delimited value
func outputSimple(f io.Writer, headers []string, values [][]string) error {
	for _, value := range values {
		if _, err := fmt.Fprintln(f, strings.Join(value, " ")); err != nil {
			return err
		}
	}
	return nil
}

// outputDsv prints structured data as delimiter separated value format.
func outputDsv(f io.Writer, headers []string, values [][]string, delimiter rune) error {
	writer := csv.NewWriter(f)
	writer.Comma = delimiter
	if err := writer.Write(headers); err != nil {
		return err
	}
	for _, value := range values {
		if err := writer.Write(value); err != nil {
			return err
		}
	}
	writer.Flush()
	return writer.Error()
}

// outputYaml prints structured data as yaml
func outputYaml(f io.Writer, headers []string, values [][]string) error {
	root := &yaml.Node{Kind: yaml.SequenceNode}
	for _, value := range values {
		row := &yaml.Node{Kind: yaml.MappingNode}
		for j, val := range value {
			row.Content = append(row.Content, &yaml.Node{
				Kind:  yaml.ScalarNode,
				Value: headers[j],
			})

			valueNode := &yaml.Node{Kind: yaml.ScalarNode, Value: val}
			if _, err := strconv.ParseInt(val, 10, 64); err == nil {
				valueNode.Tag = "!!int"
			} else {
				valueNode.Tag = "!!str"
			}
			row.Content = append(row.Content, valueNode)
		}
		root.Content = append(root.Content, row)
	}
	encoder := yaml.NewEncoder(f)
	if err := encoder.Encode(root); err != nil {
		_ = encoder.Close()
		return err
	}
	return encoder.Close()
}

var (
	matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
	matchAllCap   = regexp.MustCompile("([a-z0-9])([A-Z])")
)

func toSnakeCase(str string) string {
	snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
	snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
	return strings.ToLower(snake)
}

// orderedRow preserves header insertion order when marshaled to JSON.
type orderedRow struct {
	keys   []string
	values map[string]string
}

func (o orderedRow) MarshalJSON() ([]byte, error) {
	var buf bytes.Buffer
	buf.WriteByte('{')
	for i, k := range o.keys {
		if i > 0 {
			buf.WriteByte(',')
		}
		key, err := json.Marshal(k)
		if err != nil {
			return nil, err
		}
		val, err := json.Marshal(o.values[k])
		if err != nil {
			return nil, err
		}
		buf.Write(key)
		buf.WriteByte(':')
		buf.Write(val)
	}
	buf.WriteByte('}')
	return buf.Bytes(), nil
}

// outputJSON prints structured data as json, preserving header field order.
func outputJSON(f io.Writer, headers []string, values [][]string) error {
	snakeHeaders := make([]string, len(headers))
	for i, h := range headers {
		snakeHeaders[i] = toSnakeCase(h)
	}
	rows := make([]orderedRow, 0, len(values))
	for _, value := range values {
		row := orderedRow{keys: snakeHeaders, values: make(map[string]string, len(headers))}
		for j, val := range value {
			row.values[snakeHeaders[j]] = val
		}
		rows = append(rows, row)
	}
	encoder := json.NewEncoder(f)
	encoder.SetIndent("", "  ")
	return encoder.Encode(rows)
}

func isMachineReadable(outputFormat string) bool {
	switch outputFormat {
	case "yml", "yaml", "csv", "tsv", "json":
		return true
	}
	return false
}
