// 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 outputs provides types for implementing instance type output functions as well as prebuilt output functions.
package outputs

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"reflect"
	"strconv"
	"strings"
	"text/tabwriter"

	"github.com/aws/amazon-ec2-instance-selector/v3/pkg/instancetypes"
)

const columnTag = "column"

// wideColumnsData stores the data that should be displayed on each column
// of a wide output row.
type wideColumnsData struct {
	instanceName       string `column:"Instance Type"`
	vcpu               int32  `column:"VCPUs"`
	memory             string `column:"Mem (GiB)"`
	hypervisor         string `column:"Hypervisor"`
	currentGen         bool   `column:"Current Gen"`
	hibernationSupport bool   `column:"Hibernation Support"`
	cpuArch            string `column:"CPU Arch"`
	networkPerformance string `column:"Network Performance"`
	eni                int32  `column:"ENIs"`
	gpu                int32  `column:"GPUs"`
	gpuMemory          string `column:"GPU Mem (GiB)"`
	gpuInfo            string `column:"GPU Info"`
	odPrice            string `column:"On-Demand Price/Hr"`
	spotPrice          string `column:"Spot Price/Hr"`
}

// SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names.
func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
	instanceTypeStrings := []string{}
	for _, instanceTypeInfo := range instanceTypeInfoSlice {
		instanceTypeStrings = append(instanceTypeStrings, string(instanceTypeInfo.InstanceType))
	}
	return instanceTypeStrings
}

// VerboseInstanceTypeOutput is an OutputFn which outputs a slice of instance type names.
func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
	output, err := json.MarshalIndent(instanceTypeInfoSlice, "", "    ")
	if err != nil {
		log.Println("Unable to convert instance type info to JSON")
		return []string{}
	}
	if string(output) == "[]" || string(output) == "null" {
		return []string{}
	}
	return []string{string(output)}
}

// TableOutputShort is an OutputFn which returns a CLI table for easy reading.
func TableOutputShort(instanceTypeInfoSlice []*instancetypes.Details) []string {
	if len(instanceTypeInfoSlice) == 0 {
		return nil
	}
	w := new(tabwriter.Writer)
	buf := new(bytes.Buffer)
	w.Init(buf, 8, 8, 8, ' ', 0)
	defer w.Flush()

	headers := []interface{}{
		"Instance Type",
		"VCPUs",
		"Mem (GiB)",
	}
	separators := []interface{}{}

	headerFormat := ""
	for _, header := range headers {
		headerFormat = headerFormat + "%s\t"
		separators = append(separators, strings.Repeat("-", len(header.(string))))
	}
	fmt.Fprintf(w, headerFormat, headers...)
	fmt.Fprintf(w, "\n"+headerFormat, separators...)

	for _, instanceTypeInfo := range instanceTypeInfoSlice {
		fmt.Fprintf(w, "\n%s\t%d\t%s\t",
			instanceTypeInfo.InstanceType,
			*instanceTypeInfo.VCpuInfo.DefaultVCpus,
			formatFloat(float64(*instanceTypeInfo.MemoryInfo.SizeInMiB)/1024.0),
		)
	}
	w.Flush()
	return []string{buf.String()}
}

// TableOutputWide is an OutputFn which returns a detailed CLI table for easy reading.
func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string {
	if len(instanceTypeInfoSlice) == 0 {
		return nil
	}
	w := new(tabwriter.Writer)
	buf := new(bytes.Buffer)
	w.Init(buf, 8, 8, 2, ' ', 0)
	defer w.Flush()

	columnDataStruct := wideColumnsData{}
	headers := []interface{}{}
	structType := reflect.TypeOf(columnDataStruct)
	for i := 0; i < structType.NumField(); i++ {
		columnHeader := structType.Field(i).Tag.Get(columnTag)
		headers = append(headers, columnHeader)
	}
	separators := make([]interface{}, 0)

	headerFormat := ""
	for _, header := range headers {
		headerFormat = headerFormat + "%s\t"
		separators = append(separators, strings.Repeat("-", len(header.(string))))
	}
	fmt.Fprintf(w, headerFormat, headers...)
	fmt.Fprintf(w, "\n"+headerFormat, separators...)

	columnsData := getWideColumnsData(instanceTypeInfoSlice)

	for _, data := range columnsData {
		fmt.Fprintf(w, "\n%s\t%d\t%s\t%s\t%t\t%t\t%s\t%s\t%d\t%d\t%s\t%s\t%s\t%s\t",
			data.instanceName,
			data.vcpu,
			data.memory,
			data.hypervisor,
			data.currentGen,
			data.hibernationSupport,
			data.cpuArch,
			data.networkPerformance,
			data.eni,
			data.gpu,
			data.gpuMemory,
			data.gpuInfo,
			data.odPrice,
			data.spotPrice,
		)
	}
	w.Flush()
	return []string{buf.String()}
}

// OneLineOutput is an output function which prints the instance type names on a single line separated by commas.
func OneLineOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
	instanceTypeNames := []string{}
	for _, instanceType := range instanceTypeInfoSlice {
		instanceTypeNames = append(instanceTypeNames, string(instanceType.InstanceType))
	}
	if len(instanceTypeNames) == 0 {
		return []string{}
	}
	return []string{strings.Join(instanceTypeNames, ",")}
}

func formatFloat(f float64) string {
	s := strconv.FormatFloat(f, 'f', 5, 64)
	parts := strings.Split(s, ".")
	if len(parts) == 1 {
		return s
	}
	reversed := reverse(parts[0])
	withCommas := ""
	for i, p := range reversed {
		if i%3 == 0 && i != 0 {
			withCommas += ","
		}
		withCommas += string(p)
	}
	s = strings.Join([]string{reverse(withCommas), parts[1]}, ".")
	return strings.TrimRight(strings.TrimRight(s, "0"), ".")
}

func reverse(s string) string {
	runes := []rune(s)
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return string(runes)
}

// getWideColumnsData returns the column data necessary for a wide output for each of
// the given instance types.
func getWideColumnsData(instanceTypes []*instancetypes.Details) []*wideColumnsData {
	columnsData := []*wideColumnsData{}

	for _, instanceType := range instanceTypes {
		none := "none"

		cpuArchitectures := []string{}
		for _, cpuArch := range instanceType.ProcessorInfo.SupportedArchitectures {
			cpuArchitectures = append(cpuArchitectures, string(cpuArch))
		}

		gpus := int32(0)
		gpuMemory := int32(0)
		gpuType := []string{}
		if instanceType.GpuInfo != nil {
			gpuMemory = *instanceType.GpuInfo.TotalGpuMemoryInMiB
			for _, gpuInfo := range instanceType.GpuInfo.Gpus {
				gpus = gpus + *gpuInfo.Count
				gpuType = append(gpuType, *gpuInfo.Manufacturer+" "+*gpuInfo.Name)
			}
		} else {
			gpuType = append(gpuType, none)
		}

		onDemandPricePerHourStr := "-Not Fetched-"
		spotPricePerHourStr := "-Not Fetched-"
		if instanceType.OndemandPricePerHour != nil {
			onDemandPricePerHourStr = "$" + formatFloat(*instanceType.OndemandPricePerHour)
		}
		if instanceType.SpotPrice != nil {
			spotPricePerHourStr = "$" + formatFloat(*instanceType.SpotPrice)
		}

		newColumn := wideColumnsData{
			instanceName:       string(instanceType.InstanceType),
			vcpu:               *instanceType.VCpuInfo.DefaultVCpus,
			memory:             formatFloat(float64(*instanceType.MemoryInfo.SizeInMiB) / 1024.0),
			hypervisor:         string(instanceType.Hypervisor),
			currentGen:         *instanceType.CurrentGeneration,
			hibernationSupport: *instanceType.HibernationSupported,
			cpuArch:            strings.Join(cpuArchitectures, ", "),
			networkPerformance: *instanceType.NetworkInfo.NetworkPerformance,
			eni:                *instanceType.NetworkInfo.MaximumNetworkInterfaces,
			gpu:                gpus,
			gpuMemory:          formatFloat(float64(gpuMemory) / 1024.0),
			gpuInfo:            strings.Join(gpuType, ", "),
			odPrice:            onDemandPricePerHourStr,
			spotPrice:          spotPricePerHourStr,
		}

		columnsData = append(columnsData, &newColumn)
	}

	return columnsData
}

// getUnderlyingValue returns the underlying value of the given
// reflect.Value type.
func getUnderlyingValue(value reflect.Value) interface{} {
	var val interface{}

	switch value.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		val = value.Int()
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		val = value.Uint()
	case reflect.Float32, reflect.Float64:
		val = value.Float()
	case reflect.String:
		val = value.String()
	case reflect.Pointer:
		val = value.Pointer()
	case reflect.Bool:
		val = value.Bool()
	case reflect.Complex128, reflect.Complex64:
		val = value.Complex()
	case reflect.Interface:
		val = value.Interface()
	case reflect.UnsafePointer:
		val = value.UnsafePointer()
	default:
		val = nil
	}

	return val
}
