// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.

package termui

import (
	"fmt"
	"math"
)

// only 16 possible combinations, why bother
var braillePatterns = map[[2]int]rune{
	[2]int{0, 0}: '⣀',
	[2]int{0, 1}: '⡠',
	[2]int{0, 2}: '⡐',
	[2]int{0, 3}: '⡈',

	[2]int{1, 0}: '⢄',
	[2]int{1, 1}: '⠤',
	[2]int{1, 2}: '⠔',
	[2]int{1, 3}: '⠌',

	[2]int{2, 0}: '⢂',
	[2]int{2, 1}: '⠢',
	[2]int{2, 2}: '⠒',
	[2]int{2, 3}: '⠊',

	[2]int{3, 0}: '⢁',
	[2]int{3, 1}: '⠡',
	[2]int{3, 2}: '⠑',
	[2]int{3, 3}: '⠉',
}

var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}

// LineChart has two modes: braille(default) and dot. Using braille gives 2x capacity as dot mode,
// because one braille char can represent two data points.
/*
  lc := termui.NewLineChart()
  lc.BorderLabel = "braille-mode Line Chart"
  lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
  lc.Width = 50
  lc.Height = 12
  lc.AxesColor = termui.ColorWhite
  lc.LineColor = termui.ColorGreen | termui.AttrBold
  // termui.Render(lc)...
*/
type LineChart struct {
	Block
	Data          []float64
	DataLabels    []string // if unset, the data indices will be used
	Mode          string   // braille | dot
	DotStyle      rune
	LineColor     Attribute
	scale         float64 // data span per cell on y-axis
	AxesColor     Attribute
	drawingX      int
	drawingY      int
	axisYHeight   int
	axisXWidth    int
	axisYLabelGap int
	axisXLabelGap int
	topValue      float64
	bottomValue   float64
	labelX        [][]rune
	labelY        [][]rune
	labelYSpace   int
	maxY          float64
	minY          float64
	autoLabels    bool
}

// NewLineChart returns a new LineChart with current theme.
func NewLineChart() *LineChart {
	lc := &LineChart{Block: *NewBlock()}
	lc.AxesColor = ThemeAttr("linechart.axes.fg")
	lc.LineColor = ThemeAttr("linechart.line.fg")
	lc.Mode = "braille"
	lc.DotStyle = '•'
	lc.axisXLabelGap = 2
	lc.axisYLabelGap = 0
	lc.bottomValue = math.Inf(1)
	lc.topValue = math.Inf(-1)
	return lc
}

// one cell contains two data points
// so the capacity is 2x as dot-mode
func (lc *LineChart) renderBraille() Buffer {
	buf := NewBuffer()

	// return: b -> which cell should the point be in
	//         m -> in the cell, divided into 4 equal height levels, which subcell?
	getPos := func(d float64) (b, m int) {
		cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
		b = cnt4 / 4
		m = cnt4 % 4
		return
	}
	// plot points
	for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
		b0, m0 := getPos(lc.Data[2*i])
		b1, m1 := getPos(lc.Data[2*i+1])

		if b0 == b1 {
			c := Cell{
				Ch: braillePatterns[[2]int{m0, m1}],
				Bg: lc.Bg,
				Fg: lc.LineColor,
			}
			y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
			x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
			buf.Set(x, y, c)
		} else {
			c0 := Cell{Ch: lSingleBraille[m0],
				Fg: lc.LineColor,
				Bg: lc.Bg}
			x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
			y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
			buf.Set(x0, y0, c0)

			c1 := Cell{Ch: rSingleBraille[m1],
				Fg: lc.LineColor,
				Bg: lc.Bg}
			x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
			y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
			buf.Set(x1, y1, c1)
		}

	}
	return buf
}

func (lc *LineChart) renderDot() Buffer {
	buf := NewBuffer()
	lasty := -1 // previous y val
	for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
		c := Cell{
			Ch: lc.DotStyle,
			Fg: lc.LineColor,
			Bg: lc.Bg,
		}
		x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
		y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)

		if lasty != -1 && lasty != y {
			u := 1 // direction
			if lasty > y {
				u = -1 // put dot below
			}
			for fy := lasty + u; fy != y; fy += u { // fy: filling point's y val
				dx := -1 // lastx := x-1 = x+dx
				if u*(fy-lasty) >= u*(y-lasty)/2 {
					dx = 0 // cancel the horizontal backspace when getting close to (x,y)
				}
				buf.Set(x+dx, fy, c)
			}
		}
		lasty = y

		buf.Set(x, y, c)
	}

	return buf
}

func (lc *LineChart) calcLabelX() {
	lc.labelX = [][]rune{}

	for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ {
		if lc.Mode == "dot" {
			if l >= len(lc.DataLabels) {
				break
			}

			s := str2runes(lc.DataLabels[l])
			w := strWidth(lc.DataLabels[l])
			if l+w <= lc.axisXWidth {
				lc.labelX = append(lc.labelX, s)
			}
			l += w + lc.axisXLabelGap
		} else { // braille
			if 2*l >= len(lc.DataLabels) {
				break
			}

			s := str2runes(lc.DataLabels[2*l])
			w := strWidth(lc.DataLabels[2*l])
			if l+w <= lc.axisXWidth {
				lc.labelX = append(lc.labelX, s)
			}
			l += w + lc.axisXLabelGap

		}
	}
}

func shortenFloatVal(x float64) string {
	if x > 1e12 {
		return fmt.Sprintf("%.4fT", x/1e12)
	}
	if x > 1e9 {
		return fmt.Sprintf("%.4fB", x/1e9)
	}
	if x > 1e6 {
		return fmt.Sprintf("%.4fM", x/1e6)
	}
	return fmt.Sprintf("%.4f", x)
}

func (lc *LineChart) calcLabelY() {
	span := lc.topValue - lc.bottomValue
	lc.scale = span / float64(lc.axisYHeight)

	n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
	lc.labelY = make([][]rune, n)
	maxLen := 0
	for i := 0; i < n; i++ {
		v := lc.bottomValue + float64(i)*span/float64(n)
		// don't show negative Y axis labels
		if v < 0 {
			continue
		}
		s := str2runes(shortenFloatVal(v))
		if len(s) > maxLen {
			maxLen = len(s)
		}
		lc.labelY[i] = s
	}

	lc.labelYSpace = maxLen
}

func (lc *LineChart) calcLayout() {
	// set datalabels if it is not provided
	if (lc.DataLabels == nil || len(lc.DataLabels) == 0) || lc.autoLabels {
		lc.autoLabels = true
		lc.DataLabels = make([]string, len(lc.Data))
		for i := range lc.Data {
			lc.DataLabels[i] = fmt.Sprint(i)
		}
	}

	// lazy increase, to avoid y shaking frequently
	// update bound Y when drawing is gonna overflow
	lc.minY = lc.Data[0]
	lc.maxY = lc.Data[0]

	lc.bottomValue = lc.minY
	lc.topValue = lc.maxY

	// valid visible range
	vrange := lc.innerArea.Dx()
	if lc.Mode == "braille" {
		vrange = 2 * lc.innerArea.Dx()
	}
	if vrange > len(lc.Data) {
		vrange = len(lc.Data)
	}

	for _, v := range lc.Data[:vrange] {
		if v > lc.maxY {
			lc.maxY = v
		}
		if v < lc.minY {
			lc.minY = v
		}
	}

	//span := lc.maxY - lc.minY

	if lc.minY < lc.bottomValue {
		lc.bottomValue = lc.minY
		//lc.bottomValue = lc.minY - 0.2*span
	}

	if lc.maxY > lc.topValue {
		lc.topValue = lc.maxY
		//lc.topValue = lc.maxY + 0.2*span
	}

	offset := 2
	lc.axisYHeight = lc.innerArea.Dy() - offset
	lc.calcLabelY()

	lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
	lc.calcLabelX()

	lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
	lc.drawingY = lc.innerArea.Min.Y
}

func (lc *LineChart) plotAxes() Buffer {
	buf := NewBuffer()

	offset := 2
	origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - offset
	origX := lc.innerArea.Min.X + lc.labelYSpace

	buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})

	for x := origX + 1; x < origX+lc.axisXWidth; x++ {
		buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
	}

	for dy := 1; dy <= lc.axisYHeight; dy++ {
		buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
	}

	// x label
	oft := 0
	for _, rs := range lc.labelX {
		if oft+len(rs) > lc.axisXWidth {
			break
		}
		for j, r := range rs {
			c := Cell{
				Ch: r,
				Fg: lc.AxesColor,
				Bg: lc.Bg,
			}
			x := origX + oft + j
			y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
			buf.Set(x, y, c)
		}
		oft += len(rs) + lc.axisXLabelGap
	}

	// y labels
	for i, rs := range lc.labelY {
		for j, r := range rs {
			buf.Set(
				lc.innerArea.Min.X+j,
				origY-i*(lc.axisYLabelGap+1),
				Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
		}
	}

	return buf
}

// Buffer implements Bufferer interface.
func (lc *LineChart) Buffer() Buffer {
	buf := lc.Block.Buffer()

	if lc.Data == nil || len(lc.Data) == 0 {
		return buf
	}
	lc.calcLayout()
	buf.Merge(lc.plotAxes())

	if lc.Mode == "dot" {
		buf.Merge(lc.renderDot())
	} else {
		buf.Merge(lc.renderBraille())
	}

	return buf
}
