// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package plotter

import (
	"math"

	"gonum.org/v1/plot"
	"gonum.org/v1/plot/vg"
	"gonum.org/v1/plot/vg/draw"
)

// DefaultCapWidth is the default width of error bar caps.
var DefaultCapWidth = vg.Points(5)

// YErrorBars implements the plot.Plotter, plot.DataRanger,
// and plot.GlyphBoxer interfaces, drawing vertical error
// bars, denoting error in Y values.
type YErrorBars struct {
	XYs

	// YErrors is a copy of the Y errors for each point.
	YErrors

	// LineStyle is the style used to draw the error bars.
	draw.LineStyle

	// CapWidth is the width of the caps drawn at the top
	// of each error bar.
	CapWidth vg.Length
}

// NewYErrorBars returns a new YErrorBars plotter, or an error on failure.
// The error values from the YErrorer interface are interpreted as relative
// to the corresponding Y value. The errors for a given Y value are computed
// by taking the absolute value of the error returned by the YErrorer
// and subtracting the first and adding the second to the Y value.
func NewYErrorBars(yerrs interface {
	XYer
	YErrorer
}) (*YErrorBars, error) {

	errors := make(YErrors, yerrs.Len())
	for i := range errors {
		errors[i].Low, errors[i].High = yerrs.YError(i)
		if err := CheckFloats(errors[i].Low, errors[i].High); err != nil {
			return nil, err
		}
	}
	xys, err := CopyXYs(yerrs)
	if err != nil {
		return nil, err
	}

	return &YErrorBars{
		XYs:       xys,
		YErrors:   errors,
		LineStyle: DefaultLineStyle,
		CapWidth:  DefaultCapWidth,
	}, nil
}

// Plot implements the Plotter interface, drawing labels.
func (e *YErrorBars) Plot(c draw.Canvas, p *plot.Plot) {
	trX, trY := p.Transforms(&c)
	for i, err := range e.YErrors {
		x := trX(e.XYs[i].X)
		ylow := trY(e.XYs[i].Y - math.Abs(err.Low))
		yhigh := trY(e.XYs[i].Y + math.Abs(err.High))

		bar := c.ClipLinesY([]vg.Point{{x, ylow}, {x, yhigh}})
		c.StrokeLines(e.LineStyle, bar...)
		e.drawCap(&c, x, ylow)
		e.drawCap(&c, x, yhigh)
	}
}

// drawCap draws the cap if it is not clipped.
func (e *YErrorBars) drawCap(c *draw.Canvas, x, y vg.Length) {
	if !c.Contains(vg.Point{X: x, Y: y}) {
		return
	}
	c.StrokeLine2(e.LineStyle, x-e.CapWidth/2, y, x+e.CapWidth/2, y)
}

// DataRange implements the plot.DataRanger interface.
func (e *YErrorBars) DataRange() (xmin, xmax, ymin, ymax float64) {
	xmin, xmax = Range(XValues{e})
	ymin = math.Inf(1)
	ymax = math.Inf(-1)
	for i, err := range e.YErrors {
		y := e.XYs[i].Y
		ylow := y - math.Abs(err.Low)
		yhigh := y + math.Abs(err.High)
		ymin = math.Min(math.Min(math.Min(ymin, y), ylow), yhigh)
		ymax = math.Max(math.Max(math.Max(ymax, y), ylow), yhigh)
	}
	return
}

// GlyphBoxes implements the plot.GlyphBoxer interface.
func (e *YErrorBars) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
	rect := vg.Rectangle{
		Min: vg.Point{
			X: -e.CapWidth / 2,
			Y: -e.LineStyle.Width / 2,
		},
		Max: vg.Point{
			X: +e.CapWidth / 2,
			Y: +e.LineStyle.Width / 2,
		},
	}
	var bs []plot.GlyphBox
	for i, err := range e.YErrors {
		x := plt.X.Norm(e.XYs[i].X)
		y := e.XYs[i].Y
		bs = append(bs,
			plot.GlyphBox{X: x, Y: plt.Y.Norm(y - err.Low), Rectangle: rect},
			plot.GlyphBox{X: x, Y: plt.Y.Norm(y + err.High), Rectangle: rect})
	}
	return bs
}

// XErrorBars implements the plot.Plotter, plot.DataRanger,
// and plot.GlyphBoxer interfaces, drawing horizontal error
// bars, denoting error in Y values.
type XErrorBars struct {
	XYs

	// XErrors is a copy of the X errors for each point.
	XErrors

	// LineStyle is the style used to draw the error bars.
	draw.LineStyle

	// CapWidth is the width of the caps drawn at the top
	// of each error bar.
	CapWidth vg.Length
}

// Returns a new XErrorBars plotter, or an error on failure. The error values
// from the XErrorer interface are interpreted as relative to the corresponding
// X value. The errors for a given X value are computed by taking the absolute
// value of the error returned by the XErrorer and subtracting the first and
// adding the second to the X value.
func NewXErrorBars(xerrs interface {
	XYer
	XErrorer
}) (*XErrorBars, error) {

	errors := make(XErrors, xerrs.Len())
	for i := range errors {
		errors[i].Low, errors[i].High = xerrs.XError(i)
		if err := CheckFloats(errors[i].Low, errors[i].High); err != nil {
			return nil, err
		}
	}
	xys, err := CopyXYs(xerrs)
	if err != nil {
		return nil, err
	}

	return &XErrorBars{
		XYs:       xys,
		XErrors:   errors,
		LineStyle: DefaultLineStyle,
		CapWidth:  DefaultCapWidth,
	}, nil
}

// Plot implements the Plotter interface, drawing labels.
func (e *XErrorBars) Plot(c draw.Canvas, p *plot.Plot) {
	trX, trY := p.Transforms(&c)
	for i, err := range e.XErrors {
		y := trY(e.XYs[i].Y)
		xlow := trX(e.XYs[i].X - math.Abs(err.Low))
		xhigh := trX(e.XYs[i].X + math.Abs(err.High))

		bar := c.ClipLinesX([]vg.Point{{xlow, y}, {xhigh, y}})
		c.StrokeLines(e.LineStyle, bar...)
		e.drawCap(&c, xlow, y)
		e.drawCap(&c, xhigh, y)
	}
}

// drawCap draws the cap if it is not clipped.
func (e *XErrorBars) drawCap(c *draw.Canvas, x, y vg.Length) {
	if !c.Contains(vg.Point{X: x, Y: y}) {
		return
	}
	c.StrokeLine2(e.LineStyle, x, y-e.CapWidth/2, x, y+e.CapWidth/2)
}

// DataRange implements the plot.DataRanger interface.
func (e *XErrorBars) DataRange() (xmin, xmax, ymin, ymax float64) {
	ymin, ymax = Range(YValues{e})
	xmin = math.Inf(1)
	xmax = math.Inf(-1)
	for i, err := range e.XErrors {
		x := e.XYs[i].X
		xlow := x - math.Abs(err.Low)
		xhigh := x + math.Abs(err.High)
		xmin = math.Min(math.Min(math.Min(xmin, x), xlow), xhigh)
		xmax = math.Max(math.Max(math.Max(xmax, x), xlow), xhigh)
	}
	return
}

// GlyphBoxes implements the plot.GlyphBoxer interface.
func (e *XErrorBars) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
	rect := vg.Rectangle{
		Min: vg.Point{
			X: -e.LineStyle.Width / 2,
			Y: -e.CapWidth / 2,
		},
		Max: vg.Point{
			X: +e.LineStyle.Width / 2,
			Y: +e.CapWidth / 2,
		},
	}
	var bs []plot.GlyphBox
	for i, err := range e.XErrors {
		x := e.XYs[i].X
		y := plt.Y.Norm(e.XYs[i].Y)
		bs = append(bs,
			plot.GlyphBox{X: plt.X.Norm(x - err.Low), Y: y, Rectangle: rect},
			plot.GlyphBox{X: plt.X.Norm(x + err.High), Y: y, Rectangle: rect})
	}
	return bs
}
