package asciigraph

import (
	"bytes"
	"fmt"
	"math"
	"strings"
)

// Plot returns ascii graph for a series.
func Plot(series []float64, options ...Option) string {
	return PlotMany([][]float64{series}, options...)
}

// PlotMany returns ascii graph for multiple series.
func PlotMany(data [][]float64, options ...Option) string {
	var logMaximum float64
	config := configure(config{
		Offset:    3,
		Precision: 2,
	}, options)

	// Create a deep copy of the input data
	dataCopy := make([][]float64, len(data))
	for i, series := range data {
		dataCopy[i] = make([]float64, len(series))
		copy(dataCopy[i], series)
	}
	data = dataCopy

	lenMax := 0
	for i := range data {
		if l := len(data[i]); l > lenMax {
			lenMax = l
		}
	}

	if config.Width > 0 {
		for i := range data {
			for j := len(data[i]); j < lenMax; j++ {
				data[i] = append(data[i], math.NaN())
			}
			data[i] = interpolateArray(data[i], config.Width)
		}

		lenMax = config.Width
	}

	minimum, maximum := math.Inf(1), math.Inf(-1)
	for i := range data {
		minVal, maxVal := minMaxFloat64Slice(data[i])
		if minVal < minimum {
			minimum = minVal
		}
		if maxVal > maximum {
			maximum = maxVal
		}
	}
	if config.LowerBound != nil && *config.LowerBound < minimum {
		minimum = *config.LowerBound
	}
	if config.UpperBound != nil && *config.UpperBound > maximum {
		maximum = *config.UpperBound
	}
	interval := math.Abs(maximum - minimum)

	if config.Height <= 0 {
		config.Height = calculateHeight(interval)
	}

	if config.Offset <= 0 {
		config.Offset = 3
	}

	var ratio float64
	if interval != 0 {
		ratio = float64(config.Height) / interval
	} else {
		ratio = 1
	}
	min2 := round(minimum * ratio)
	max2 := round(maximum * ratio)

	intmin2 := int(min2)
	intmax2 := int(max2)

	rows := int(math.Abs(float64(intmax2 - intmin2)))
	width := lenMax + config.Offset

	type cell struct {
		Text  string
		Color AnsiColor
	}
	plot := make([][]cell, rows+1)

	// initialise empty 2D grid
	for i := 0; i < rows+1; i++ {
		line := make([]cell, width)
		for j := 0; j < width; j++ {
			line[j].Text = " "
			line[j].Color = Default
		}
		plot[i] = line
	}

	precision := config.Precision
	logMaximum = math.Log10(math.Max(math.Abs(maximum), math.Abs(minimum))) //to find number of zeros after decimal
	if minimum == float64(0) && maximum == float64(0) {
		logMaximum = float64(-1)
	}

	if logMaximum < 0 {
		// negative log
		if math.Mod(logMaximum, 1) != 0 {
			// non-zero digits after decimal
			precision += uint(math.Abs(logMaximum))
		} else {
			precision += uint(math.Abs(logMaximum) - 1.0)
		}
	} else if logMaximum > 2 {
		precision = 0
	}

	maxNumLength := len(fmt.Sprintf("%0.*f", precision, maximum))
	minNumLength := len(fmt.Sprintf("%0.*f", precision, minimum))
	maxWidth := int(math.Max(float64(maxNumLength), float64(minNumLength)))

	// axis and labels
	for y := intmin2; y < intmax2+1; y++ {
		var magnitude float64
		if rows > 0 {
			magnitude = maximum - (float64(y-intmin2) * interval / float64(rows))
		} else {
			magnitude = float64(y)
		}

		label := fmt.Sprintf("%*.*f", maxWidth+1, precision, magnitude)
		w := y - intmin2
		h := int(math.Max(float64(config.Offset)-float64(len(label)), 0))

		plot[w][h].Text = label
		plot[w][h].Color = config.LabelColor
		plot[w][config.Offset-1].Text = "┤"
		plot[w][config.Offset-1].Color = config.AxisColor
	}

	for i := range data {
		series := data[i]

		color := Default
		if i < len(config.SeriesColors) {
			color = config.SeriesColors[i]
		}

		var y0, y1 int

		if !math.IsNaN(series[0]) {
			y0 = int(round(series[0]*ratio) - min2)
			plot[rows-y0][config.Offset-1].Text = "┼" // first value
			plot[rows-y0][config.Offset-1].Color = config.AxisColor
		}

		for x := 0; x < len(series)-1; x++ { // plot the line
			d0 := series[x]
			d1 := series[x+1]

			if math.IsNaN(d0) && math.IsNaN(d1) {
				continue
			}

			if math.IsNaN(d1) && !math.IsNaN(d0) {
				y0 = int(round(d0*ratio) - float64(intmin2))
				plot[rows-y0][x+config.Offset].Text = "╴"
				plot[rows-y0][x+config.Offset].Color = color
				continue
			}

			if math.IsNaN(d0) && !math.IsNaN(d1) {
				y1 = int(round(d1*ratio) - float64(intmin2))
				plot[rows-y1][x+config.Offset].Text = "╶"
				plot[rows-y1][x+config.Offset].Color = color
				continue
			}

			y0 = int(round(d0*ratio) - float64(intmin2))
			y1 = int(round(d1*ratio) - float64(intmin2))

			if y0 == y1 {
				plot[rows-y0][x+config.Offset].Text = "─"
			} else {
				if y0 > y1 {
					plot[rows-y1][x+config.Offset].Text = "╰"
					plot[rows-y0][x+config.Offset].Text = "╮"
				} else {
					plot[rows-y1][x+config.Offset].Text = "╭"
					plot[rows-y0][x+config.Offset].Text = "╯"
				}

				start := int(math.Min(float64(y0), float64(y1))) + 1
				end := int(math.Max(float64(y0), float64(y1)))
				for y := start; y < end; y++ {
					plot[rows-y][x+config.Offset].Text = "│"
				}
			}

			start := int(math.Min(float64(y0), float64(y1)))
			end := int(math.Max(float64(y0), float64(y1)))
			for y := start; y <= end; y++ {
				plot[rows-y][x+config.Offset].Color = color
			}
		}
	}

	// join columns
	var lines bytes.Buffer
	for h, horizontal := range plot {
		if h != 0 {
			lines.WriteRune('\n')
		}

		// remove trailing spaces
		lastCharIndex := 0
		for i := width - 1; i >= 0; i-- {
			if horizontal[i].Text != " " {
				lastCharIndex = i
				break
			}
		}

		c := Default
		for _, v := range horizontal[:lastCharIndex+1] {
			if v.Color != c {
				c = v.Color
				lines.WriteString(c.String())
			}

			lines.WriteString(v.Text)
		}
		if c != Default {
			lines.WriteString(Default.String())
		}
	}

	// add caption if not empty
	if config.Caption != "" {
		lines.WriteRune('\n')
		lines.WriteString(strings.Repeat(" ", config.Offset+maxWidth))
		if len(config.Caption) < lenMax {
			lines.WriteString(strings.Repeat(" ", (lenMax-len(config.Caption))/2))
		}
		if config.CaptionColor != Default {
			lines.WriteString(config.CaptionColor.String())
		}
		lines.WriteString(config.Caption)
		if config.CaptionColor != Default {
			lines.WriteString(Default.String())
		}
	}

	if len(config.SeriesLegends) > 0 {
		addLegends(&lines, config, lenMax, config.Offset+maxWidth)
	}

	return lines.String()
}
