// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you 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 heatmap

import (
	"errors"
	"fmt"
	"image"
	"math"
	"sort"
	"sync"

	"github.com/apache/skywalking-cli/pkg/heatmap/axes"

	"github.com/mum4k/termdash/cell"
	"github.com/mum4k/termdash/private/area"
	"github.com/mum4k/termdash/private/canvas"
	"github.com/mum4k/termdash/private/draw"
	"github.com/mum4k/termdash/terminal/terminalapi"
	"github.com/mum4k/termdash/widgetapi"
)

// columnValues represent values stored in a column.
type columnValues struct {
	// values are the values in a column.
	values []int64
	// Min is the smallest value in the column, zero if values is empty.
	Min int64
	// Max is the largest value in the column, zero if values is empty.
	Max int64
}

// newColumnValues returns a new columnValues instance.
func newColumnValues(values []int64) *columnValues {
	// Copy to avoid external modifications.
	v := make([]int64, len(values))
	copy(v, values)

	mn, mx := minMax(values)

	return &columnValues{
		values: v,
		Min:    mn,
		Max:    mx,
	}
}

// HeatMap draws heatmap charts.
// Implements widgetapi.Widget. This object is thread-safe.
type HeatMap struct {
	columns map[string]*columnValues

	// XLabels are the labels on the X axis in an increasing order.
	XLabels []string
	// YLabels are the labels on the Y axis in an increasing order.
	YLabels []string

	// MinValue and MaxValue are the Min and Max values in the columns.
	MinValue, MaxValue int64

	// opts are the provided options.
	opts *options

	// mu protects the HeatMap widget.
	mu sync.RWMutex
}

// NewHeatMap returns a new HeatMap widget.
func NewHeatMap(opts ...Option) (*HeatMap, error) {
	opt := newOptions(opts...)
	if err := opt.validate(); err != nil {
		return nil, err
	}
	return &HeatMap{
		columns: map[string]*columnValues{},
		opts:    opt,
	}, nil
}

// SetColumns sets the HeatMap's values, min and max values.
func (hp *HeatMap) SetColumns(values map[string][]int64) {
	hp.mu.Lock()
	defer hp.mu.Unlock()

	var minMaxValues []int64

	// The iteration order of map is uncertain, so the keys must be sorted explicitly.
	var names []string
	for name := range values {
		names = append(names, name)
	}
	sort.Strings(names)

	// Clear XLabels and columns.
	if len(hp.XLabels) > 0 {
		hp.XLabels = hp.XLabels[:0]
	}
	hp.columns = make(map[string]*columnValues)

	for _, name := range names {
		cv := newColumnValues(values[name])
		hp.columns[name] = cv
		hp.XLabels = append(hp.XLabels, name)

		minMaxValues = append(minMaxValues, cv.Min)
		minMaxValues = append(minMaxValues, cv.Max)
	}

	hp.MinValue, hp.MaxValue = minMax(minMaxValues)
}

// SetYLabels sets HeatMap's Y-Labels.
func (hp *HeatMap) SetYLabels(labels []string) {
	hp.mu.Lock()
	defer hp.mu.Unlock()

	// Clear YLabels.
	if len(hp.YLabels) > 0 {
		hp.YLabels = hp.YLabels[:0]
	}

	hp.YLabels = append(hp.YLabels, labels...)

	// Reverse the array.
	for i, j := 0, len(hp.YLabels)-1; i < j; i, j = i+1, j-1 {
		hp.YLabels[i], hp.YLabels[j] = hp.YLabels[j], hp.YLabels[i]
	}
}

// axesDetails determines the details about the X and Y axes.
func (hp *HeatMap) axesDetails(cvs *canvas.Canvas) (*axes.XDetails, *axes.YDetails, error) {
	yd, err := axes.NewYDetails(hp.YLabels)
	if err != nil {
		return nil, nil, err
	}

	xd, err := axes.NewXDetails(cvs.Area(), yd.End, hp.XLabels, hp.opts.cellWidth)
	if err != nil {
		return nil, nil, err
	}

	return xd, yd, nil
}

// Draw draws the values as HeatMap.
// Implements widgetapi.Widget.Draw.
func (hp *HeatMap) Draw(cvs *canvas.Canvas, _ *widgetapi.Meta) error {
	hp.mu.Lock()
	defer hp.mu.Unlock()

	// Check if the canvas has enough area to draw HeatMap.
	needAr, err := area.FromSize(hp.minSize())
	if err != nil {
		return err
	}
	if !needAr.In(cvs.Area()) {
		return draw.ResizeNeeded(cvs)
	}

	xd, yd, err := hp.axesDetails(cvs)
	if err != nil {
		return err
	}

	err = hp.drawColumns(cvs, xd, yd)
	if err != nil {
		return err
	}

	return hp.drawAxes(cvs, xd, yd)
}

// drawColumns draws the graph representing the stored series.
// Returns XDetails that might be adjusted to not start at zero value if some
// of the series didn't fit the graphs and XAxisUnscaled was provided.
// If the series has NaN values they will be ignored and not draw on the graph.
func (hp *HeatMap) drawColumns(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
	for i, xl := range hp.XLabels {
		cv := hp.columns[xl]

		for j := range cv.values {
			v := cv.values[j]

			startX := xd.Start.X + 1 + i*hp.opts.cellWidth
			startY := yd.Labels[j].Pos.Y

			endX := startX + hp.opts.cellWidth
			endY := startY + 1

			rect := image.Rect(startX, startY, endX, endY)
			color := hp.getBlockColor(v)

			if err := cvs.SetAreaCells(rect, ' ', cell.BgColor(color)); err != nil {
				return err
			}
		}
	}

	return nil
}

// drawAxes draws the X,Y axes and their labels.
func (hp *HeatMap) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
	for _, l := range yd.Labels {
		if err := draw.Text(cvs, l.Text, l.Pos,
			draw.TextMaxX(yd.Start.X),
			draw.TextOverrunMode(draw.OverrunModeThreeDot),
			draw.TextCellOpts(hp.opts.yLabelCellOpts...),
		); err != nil {
			return fmt.Errorf("failed to draw the Y labels: %v", err)
		}
	}

	for _, l := range xd.Labels {
		if err := draw.Text(cvs, l.Text, l.Pos, draw.TextCellOpts(hp.opts.xLabelCellOpts...)); err != nil {
			return fmt.Errorf("failed to draw the X horizontal labels: %v", err)
		}
	}
	return nil
}

// minSize determines the minimum required size to draw HeatMap.
func (hp *HeatMap) minSize() image.Point {
	// At the very least we need:
	// - n cells width for the Y axis and its labels.
	// - m cells width for the graph.
	reqWidth := axes.LongestString(hp.YLabels) + axes.AxisWidth + hp.opts.cellWidth*len(hp.columns)

	// For the height:
	// - 1 cells height for labels on the X axis.
	// - n cell height for the graph.
	reqHeight := 1 + len(hp.YLabels)

	return image.Point{X: reqWidth, Y: reqHeight}
}

// Keyboard input isn't supported on the SparkLine widget.
func (*HeatMap) Keyboard(_ *terminalapi.Keyboard) error {
	return errors.New("the HeatMap widget doesn't support keyboard events")
}

// Mouse input isn't supported on the SparkLine widget.
func (*HeatMap) Mouse(_ *terminalapi.Mouse) error {
	return errors.New("the HeatMap widget doesn't support mouse events")
}

// Options implements widgetapi.Widget.Options.
func (hp *HeatMap) Options() widgetapi.Options {
	hp.mu.Lock()
	defer hp.mu.Unlock()
	return widgetapi.Options{}
}

// getBlockColor returns the color of the block according to the value.
// The larger the value, the darker the color.
func (hp *HeatMap) getBlockColor(value int64) cell.Color {
	const colorNum = 23
	scale := float64(hp.MaxValue - hp.MinValue)
	fv := float64(value)

	// Refer to https://jonasjacek.github.io/colors/.
	// The color range is in Xterm color [232, 255].
	rgb := int(255 - (fv / scale * colorNum))
	return cell.ColorNumber(rgb)
}

// minMax returns the min and max values in given integer array.
func minMax(values []int64) (mn, mx int64) {
	mn = math.MaxInt64
	mx = math.MinInt64

	for _, v := range values {
		mn = int64(math.Min(float64(mn), float64(v)))
		mx = int64(math.Max(float64(mx), float64(v)))
	}
	return
}
