
// charts provides functions to create charts
// These are used to show statistics about users and genetic prediction model accuracies
// See userStatistics.go to see how input statistics are created

package createCharts

import "seekia/internal/statisticsDatum"

import goChart "github.com/wcharczuk/go-chart/v2"
import "github.com/wcharczuk/go-chart/v2/drawing"

import "image"
import "errors"
import "slices"

// Inputs:
//	-string: Chart title
//	-[]statisticsDatum.DiscreteStatisticsDatum: Statistics datums list
//	-func(float64)(string, error): formatYAxisValuesFunction
//		-This will take values such as 1000000 and turn them to "1 million"
//	-bool: Y-axis units exist
//	-string: Y-axis units (example: " Users")
// Outputs:
//	-image.Image
//	-error
func CreateBarChart(chartTitle string, chartStatisticsDatumsList []statisticsDatum.DiscreteStatisticsDatum, formatYAxisValuesFunction func(float64)(string, error), yAxisUnitsProvided bool, yAxisUnits string)(image.Image, error){

	if (len(chartStatisticsDatumsList) == 0) {
		return nil, errors.New("CreateBarChart called with empty chartStatisticsDatumsList")
	}

	chartDatumsList := make([]goChart.Value, 0, len(chartStatisticsDatumsList))

	for _, statisticsDatum := range chartStatisticsDatumsList{

		datumLabel := statisticsDatum.LabelFormatted

		datumValue := statisticsDatum.Value

		// We make sure this function does not error
		_, err := formatYAxisValuesFunction(datumValue)
		if (err != nil){
			return nil, errors.New("Invalid chartStatisticsDatumsList: Datum value is invalid. Reason: " + err.Error())
		}

		newChartValue := goChart.Value{
				Label: datumLabel,
				Value: datumValue,
			}

		chartDatumsList = append(chartDatumsList, newChartValue)
	}

	if (len(chartDatumsList) == 1){

		// This package cannot create bar charts with only 1 datum
		// Thus, we must add an empty datum

		newChartValue := goChart.Value{
				Style: goChart.Style{
					StrokeColor: drawing.ColorWhite,
					FillColor: drawing.ColorWhite,
				},
				Label: "",
				Value: .001,
			}

		chartDatumsList = append(chartDatumsList, newChartValue)
	}

	chartStyleObject := goChart.Style{
						Padding: goChart.Box{
							Top: 40,
							Bottom: 0,
							Left: 0,
							Right: 0,
							},
						}
	titleStyleObject := goChart.Style{
								Padding: goChart.Box{
									Top: 10,
									Bottom: 30,
								},
								FontSize: 15,
								FontColor: drawing.ColorBlack,
							}

	yAxisChartValueFormatter := func(v interface{}) string {

		valueFloat64 := v.(float64)

		valueFormatted, err := formatYAxisValuesFunction(valueFloat64)
		if (err != nil){
			return "ERROR"
		}

		if (yAxisUnitsProvided == false){
			return valueFormatted
		}

		result := valueFormatted + yAxisUnits
		return result
	}

	yAxisObject := goChart.YAxis{
			ValueFormatter: yAxisChartValueFormatter,
		}

	barChartObject := goChart.BarChart{
		Title: chartTitle,
		TitleStyle: titleStyleObject,
		Background: chartStyleObject,
		Height:   500,
		BarWidth: 60,
		Bars: chartDatumsList,
		YAxis: yAxisObject,
		}

	collector := &goChart.ImageWriter{}
	barChartObject.Render(goChart.PNG, collector)

	goImage, err := collector.Image()
	if (err != nil) { return nil, err }
	
	return goImage, nil
}

func CreateDonutChart(chartTitle string, chartStatisticsDatumsList []statisticsDatum.DiscreteStatisticsDatum)(image.Image, error){

	if (len(chartStatisticsDatumsList) == 0) {

		return nil, errors.New("CreateDonutChart called with empty chartStatisticsDatumsList")
	}

	chartDatumsList := make([]goChart.Value, 0, len(chartStatisticsDatumsList))

	for _, statisticsDatum := range chartStatisticsDatumsList{

		datumLabel := statisticsDatum.LabelFormatted

		// Value is always a number representing the percentage of the donut
		datumValue := statisticsDatum.Value

		newChartValue := goChart.Value{
				Label: datumLabel,
				Value: datumValue,
			}

		chartDatumsList = append(chartDatumsList, newChartValue)
	}

	chartStyleObject := goChart.Style{
						Padding: goChart.Box{
							Top: 20,
							Bottom: 0,
							Left: 0,
							Right: 0,
							},
						}

	titleStyleObject := goChart.Style{
								FontSize: 15,
								FontColor: drawing.ColorBlack,
							}

	donutChartObject := goChart.DonutChart{
		Title: chartTitle,
		TitleStyle: titleStyleObject,
		Background: chartStyleObject,
		Height: 500,
		Values: chartDatumsList,
	}

	if (len(chartDatumsList) == 1){

		// Default is transparent, we need to add color
		sliceStyleObject := goChart.Style{
							FillColor: drawing.ColorRed,
						}
	
		donutChartObject.SliceStyle = sliceStyleObject
	}

	collector := &goChart.ImageWriter{}
	donutChartObject.Render(goChart.PNG, collector)

	goImage, err := collector.Image()
	if (err != nil) { return nil, err }
	
	return goImage, nil
}

type ChartLineData struct{

	// Name of the line
	// Example: "34-66% Of Loci Known"
	Name string

	// A list of the graph points for this line
	// Each is an X/Y Coordinate pair
	LineDatumsList []statisticsDatum.NumericStatisticsDatum
}

// Inputs:
//	-string: Chart title
//	-[]ChartLineData: ChartLineDatas list
//	-string: X axis name
//	-func(float64)(string, error): Format X-Axis Values Function
//		-This will take values such as 1000000 and turn them to "1 million"
//	-string: Y axis name
//	-func(float64)(string, error): formatYAxisValuesFunction
//		-This will take values such as 1000000 and turn them to "1 million"
//	-bool: Y-axis units exist
//	-string: Y-axis units (example: " Users")
// Outputs:
//	-image.Image
//	-error
func CreateLineChart(chartTitle string, chartLineDatasList []ChartLineData, xAxisName string, formatXAxisValuesFunction func(float64)(string, error), yAxisName string, formatYAxisValuesFunction func(float64)(string, error), yAxisUnitsProvided bool, yAxisUnits string)(image.Image, error){

	if (len(chartLineDatasList) == 0) {
		return nil, errors.New("CreateLineChart called with empty chartLineDatasList")
	}

	// This list stores the character counts for each chart line title
	// We use this to find the longest character count, so we can offset the legend the correct amount
	chartLineTitleCharacterCountsList := make([]int, 0)

	chartLineObjectsList := make([]goChart.Series, 0)

	for _, chartLineData := range chartLineDatasList{

		chartLineName := chartLineData.Name
		lineDatumsList := chartLineData.LineDatumsList

		chartLineNameCharacterCount := len([]rune(chartLineName))

		chartLineTitleCharacterCountsList = append(chartLineTitleCharacterCountsList, chartLineNameCharacterCount)

		// We sort the chart line datums list

		sortFunction := func(element1 statisticsDatum.NumericStatisticsDatum, element2 statisticsDatum.NumericStatisticsDatum)int{

			xValue1 := element1.XAxisValue
			xValue2 := element2.XAxisValue

			if (xValue1 < xValue2){
				return -1	
			}
			if (xValue1 == xValue2){
				return 0
			}

			return 1
		}

		slices.SortFunc(lineDatumsList, sortFunction)

		xValuesList := make([]float64, 0)
		yValuesList := make([]float64, 0)

		for _, statisticsDatum := range lineDatumsList{

			xValue := statisticsDatum.XAxisValue
			yValue := statisticsDatum.YAxisValue

			// We make sure this function does not error
			_, err := formatXAxisValuesFunction(xValue)
			if (err != nil){
				return nil, errors.New("Invalid chartLineDatasList: X-axis datum value is invalid. Reason: " + err.Error())
			}

			// We make sure this function does not error
			_, err = formatYAxisValuesFunction(yValue)
			if (err != nil){
				return nil, errors.New("Invalid chartLineDatasList: Y-axis datum value is invalid. Reason: " + err.Error())
			}

			xValuesList = append(xValuesList, xValue)
			yValuesList = append(yValuesList, yValue)
		}

		newLineObject := goChart.ContinuousSeries{
			Name: chartLineName,
			XValues: xValuesList,
			YValues: yValuesList,
		}

		chartLineObjectsList = append(chartLineObjectsList, newLineObject)
	}

	longestChartLineNameCharacterCount := slices.Max(chartLineTitleCharacterCountsList)

	leftPadding := longestChartLineNameCharacterCount * 7

	chartBackgroundStyleObject := goChart.Style{
						Padding: goChart.Box{
							Top: 30,
							Bottom: 0,
							Left: leftPadding,
							Right: 0,
							},
						}

	titleStyleObject := goChart.Style{
								Padding: goChart.Box{
									Top: 10,
								},
								FontSize: 15,
								FontColor: drawing.ColorBlack,
							}

	xAxisChartValueFormatter := func(v interface{}) string {

		valueFloat64 := v.(float64)

		valueFormatted, err := formatXAxisValuesFunction(valueFloat64)
		if (err != nil){
			return "ERROR"
		}

		return valueFormatted
	}

	yAxisChartValueFormatter := func(v interface{}) string {

		valueFloat64 := v.(float64)

		valueFormatted, err := formatYAxisValuesFunction(valueFloat64)
		if (err != nil){
			return "ERROR"
		}

		if (yAxisUnitsProvided == false){
			return valueFormatted
		}

		result := valueFormatted + yAxisUnits
		return result
	}

	newLineChartObject := goChart.Chart{
		Title: chartTitle,
		TitleStyle: titleStyleObject,
		Background: chartBackgroundStyleObject,
		Height:   500,
		XAxis: goChart.XAxis{
			Name: xAxisName,
			ValueFormatter: xAxisChartValueFormatter,
		},

		YAxis: goChart.YAxis{
			Name: yAxisName,
			ValueFormatter: yAxisChartValueFormatter,
		},

		Series: chartLineObjectsList,
	}

	if (longestChartLineNameCharacterCount > 0){

		// There exists at least 1 named line.
		// We add a legend to explain what each line color represents

		newLegend := goChart.LegendLeft(&newLineChartObject)

		newLineChartObject.Elements = []goChart.Renderable{newLegend}
	}

	collector := &goChart.ImageWriter{}
	newLineChartObject.Render(goChart.PNG, collector)

	goImage, err := collector.Image()
	if (err != nil) { return nil, err }
	
	return goImage, nil
}

