package numeter

import (
	"fmt"
	"image"
	"image/color"
	"math"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/data/binding"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

type vuRenderer struct {
	currentframe *canvas.Raster
	lastPeakTime time.Time
	lastPeakVal  float64
	meter        *vuMeter
}

func (v *vuRenderer) MinSize() fyne.Size {
	if (v.meter.overriddenMinSize.Height >= 0) && (v.meter.overriddenMinSize.Height >= 0) {
		return v.meter.overriddenMinSize
	}
	var tsize fyne.Size
	if text := v.meter.TextFormatter; text != nil {
		tsize = fyne.MeasureText(text(), theme.TextSize(), fyne.TextStyle{false, false, false, 4})
	} else {
		tsize = fyne.MeasureText("100%", theme.TextSize(), fyne.TextStyle{false, false, false, 4})
	}
	if v.meter.VUMeterDirection == VUMeterVertical {
		oldsize := tsize
		tsize.Width = oldsize.Height
		tsize.Height = oldsize.Width
	}
	return fyne.NewSize(tsize.Width+theme.Padding()*4, tsize.Height+theme.Padding()*2)
}

func vuSet(i *image.RGBA, w int, h int, o VUMeterDirectionEnum, val float64, colGrn color.Color, colAmber color.Color, colRed color.Color) {
	valPixels := int(math.Round(float64(w) * val))
	long := w
	short := h
	if o == VUMeterVertical {
		valPixels = int(math.Round(float64(h) * val))
		long = h
		short = w
	}
	for l := 0; l < valPixels; l++ {
		c := colGrn
		if float64(l)/float64(long) > 0.75 {
			c = colAmber
		}
		if float64(l)/float64(long) > 0.85 {
			c = colRed
		}
		for s := 0; s < short; s++ {
			if o == VUMeterVertical {
				i.Set(s, h-l, c)
			} else {
				i.Set(l, s, c)
			}
		}
	}
}

func drawPeakBar(i *image.RGBA, w int, h int, o VUMeterDirectionEnum, val float64, colGrn color.Color, colAmber color.Color, colRed color.Color) {
	valPixel := int(math.Round(float64(w) * val))
	c := colGrn
	if o == VUMeterVertical {
		valPixel = int(math.Round(float64(h) * val))
		if float64(valPixel)/float64(h) > 0.75 {
			c = colAmber
		}
		if float64(valPixel)/float64(h) > 0.85 {
			c = colRed
		}
		for x := 0; x < w; x++ {
			i.Set(x, valPixel-h, c)
		}
	} else {
		valPixel = int(math.Round(float64(w) * val))
		if float64(valPixel)/float64(w) > 0.75 {
			c = colAmber
		}
		if float64(valPixel)/float64(w) > 0.85 {
			c = colRed
		}
		for y := 0; y < h; y++ {
			i.Set(valPixel, y, c)
		}
	}
}

func (v *vuRenderer) Render(w int, h int) image.Image {
	i := image.NewRGBA(image.Rect(0, 0, w, h))
	g := color.RGBA{0, 0x1f, 0, 0xff}
	a := color.RGBA{0x1f, 0x1f, 0, 0xff}
	r := color.RGBA{0x1f, 0, 0, 0xff}
	vuSet(i, w, h, v.meter.VUMeterDirection, 1, g, a, r)
	g = color.RGBA{0, 0xff, 0, 0xff}
	a = color.RGBA{0xff, 0xff, 0, 0xff}
	r = color.RGBA{0xff, 0, 0, 0xff}
	val := (v.meter.Value - v.meter.Min) / (v.meter.Max - v.meter.Min) * 100
	vuSet(i, w, h, v.meter.VUMeterDirection, val, g, a, r)
	if v.meter.Value > v.lastPeakVal {
		fmt.Printf("New peak: %f, expires at %s\n", v.meter.Value, time.Now().Add(v.meter.Peakhold))
		v.lastPeakTime = time.Now()
		v.lastPeakVal = v.meter.Value
	}
	if v.lastPeakTime.Add(v.meter.Peakhold).Before(time.Now()) {
		fmt.Printf("Previous peak good, bar at %f\n", v.lastPeakVal)
		drawPeakBar(i, w, h, v.meter.VUMeterDirection, v.lastPeakVal, g, a, r)
	}
	if v.lastPeakTime.Add(v.meter.Peakhold).After(time.Now()) {
		fmt.Printf("Previous peak expired, holding at %f\n", v.meter.Value)
		v.lastPeakVal = v.meter.Value
		v.lastPeakTime = time.Now()
	}
	return i
}

// Layout the components of the widget
func (v *vuRenderer) Layout(size fyne.Size) {
	v.currentframe.Resize(size)
	v.currentframe.Refresh()
}

// ApplyTheme is called when the vuMeter may need to update it's look
func (v *vuRenderer) ApplyTheme() {
	v.Refresh()
}

func (v *vuRenderer) BackgroundColor() color.Color {
	return theme.ButtonColor()
}

func (v *vuRenderer) Refresh() {
	v.Layout(v.meter.Size())
	canvas.Refresh(v.meter)
}

func (v *vuRenderer) Objects() []fyne.CanvasObject {
	return []fyne.CanvasObject{v.currentframe}
}

func (v *vuRenderer) Destroy() {
}

type VUMeterDirectionEnum uint32

const (
	VUMeterVertical = iota
	VUMeterHorizontal
)

// vuMeter widget is a kind of custom progressbar but has "zones" of different color for peaking.
type vuMeter struct {
	widget.BaseWidget
	TextFormatter func() string
	Value, Min, Max,
	OptimumValueMin, OptimumValueMax float64
	Peakhold         time.Duration
	VUMeterDirection VUMeterDirectionEnum

	binder            basicBinder
	overriddenMinSize fyne.Size
}

func NewVUMeterRenderer(m *vuMeter) *vuRenderer {
	c := canvas.NewRaster(func(w int, h int) image.Image { return image.NewNRGBA(image.Rect(0, 0, 200, 200)) })
	renderer := vuRenderer{c, time.Now(), 0, m}
	c.Generator = renderer.Render
	renderer.currentframe = c
	renderer.lastPeakTime = time.Now()
	renderer.lastPeakVal = 0

	return &renderer
}

func (m *vuMeter) Resize(s fyne.Size) {
	m.BaseWidget.Resize(s)
}

func (m *vuMeter) CreateRenderer() fyne.WidgetRenderer {
	return NewVUMeterRenderer(m)
}

// SetValue changes the current value of this progress bar (from p.Min to p.Max).
// The widget will be refreshed to indicate the change.
func (m *vuMeter) SetValue(v float64) {
	m.Value = v
	m.Refresh()
}

func (m *vuMeter) updateFromData(data binding.DataItem) {
	if data == nil {
		return
	}
	floatSource, ok := data.(binding.Float)
	if !ok {
		return
	}

	val, err := floatSource.Get()
	if err != nil {
		fyne.LogError("Error getting current data value", err)
		return
	}
	m.SetValue(val)
}

func (m *vuMeter) MinSize() fyne.Size {
	m.ExtendBaseWidget(m)
	return m.BaseWidget.MinSize()
}

func (m *vuMeter) Bind(data binding.Float) {
	m.binder.SetCallback(m.updateFromData)
	m.binder.Bind(data)
}

func (m *vuMeter) Unbind() {
	m.binder.Unbind()
}

func (m *vuMeter) SetMinSize(s fyne.Size) {
	m.overriddenMinSize = s
	m.Refresh()
}

// NewVUMeter creates a new meter widget with the specified value
func NewVUMeter(value float64) *vuMeter {
	meter := &vuMeter{Value: value}
	meter.OptimumValueMin = 75
	meter.OptimumValueMax = 85
	meter.Min = 0
	meter.Max = 100
	meter.ExtendBaseWidget(meter)
	meter.overriddenMinSize = fyne.NewSize(-1, -1)
	meter.VUMeterDirection = VUMeterHorizontal
	meter.Peakhold = 0
	return meter
}

func NewVUMeterWithData(data binding.Float) *vuMeter {
	f, err := data.Get()
	if err != nil {
		f = 25.0
	}
	m := NewVUMeter(f)
	m.Bind(data)
	return m
}
