// SPDX-License-Identifier: Unlicense OR MIT

package paint

import (
	"encoding/binary"
	"image"
	"image/color"
	"image/draw"
	"math"

	"gioui.org/f32"
	"gioui.org/internal/ops"
	"gioui.org/op"
	"gioui.org/op/clip"
)

// ImageFilter is the scaling filter for images.
type ImageFilter byte

const (
	// FilterLinear uses linear interpolation for scaling.
	FilterLinear ImageFilter = iota
	// FilterNearest uses nearest neighbor interpolation for scaling.
	FilterNearest
)

// ImageOp sets the brush to an image.
type ImageOp struct {
	Filter ImageFilter

	uniform bool
	color   color.NRGBA
	src     *image.RGBA

	// handle is a key to uniquely identify this ImageOp
	// in a map of cached textures.
	handle interface{}
}

// ColorOp sets the brush to a constant color.
type ColorOp struct {
	Color color.NRGBA
}

// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and
// ending at stop2 with color2.
type LinearGradientOp struct {
	Stop1  f32.Point
	Color1 color.NRGBA
	Stop2  f32.Point
	Color2 color.NRGBA
}

// PaintOp fills the current clip area with the current brush.
type PaintOp struct {
}

// OpacityStack represents an opacity applied to all painting operations
// until Pop is called.
type OpacityStack struct {
	id      ops.StackID
	macroID uint32
	ops     *ops.Ops
}

// NewImageOp creates an ImageOp backed by src.
//
// NewImageOp assumes the backing image is immutable, and may cache a
// copy of its contents in a GPU-friendly way. Create new ImageOps to
// ensure that changes to an image is reflected in the display of
// it.
func NewImageOp(src image.Image) ImageOp {
	switch src := src.(type) {
	case *image.Uniform:
		col := color.NRGBAModel.Convert(src.C).(color.NRGBA)
		return ImageOp{
			uniform: true,
			color:   col,
		}
	case *image.RGBA:
		return ImageOp{
			src:    src,
			handle: new(int),
		}
	}

	sz := src.Bounds().Size()
	// Copy the image into a GPU friendly format.
	dst := image.NewRGBA(image.Rectangle{
		Max: sz,
	})
	draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
	return ImageOp{
		src:    dst,
		handle: new(int),
	}
}

func (i ImageOp) Size() image.Point {
	if i.src == nil {
		return image.Point{}
	}
	return i.src.Bounds().Size()
}

func (i ImageOp) Add(o *op.Ops) {
	if i.uniform {
		ColorOp{
			Color: i.color,
		}.Add(o)
		return
	} else if i.src == nil || i.src.Bounds().Empty() {
		return
	}
	data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle)
	data[0] = byte(ops.TypeImage)
	data[1] = byte(i.Filter)
}

func (c ColorOp) Add(o *op.Ops) {
	data := ops.Write(&o.Internal, ops.TypeColorLen)
	data[0] = byte(ops.TypeColor)
	data[1] = c.Color.R
	data[2] = c.Color.G
	data[3] = c.Color.B
	data[4] = c.Color.A
}

func (c LinearGradientOp) Add(o *op.Ops) {
	data := ops.Write(&o.Internal, ops.TypeLinearGradientLen)
	data[0] = byte(ops.TypeLinearGradient)

	bo := binary.LittleEndian
	bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X))
	bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y))
	bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X))
	bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y))

	data[17+0] = c.Color1.R
	data[17+1] = c.Color1.G
	data[17+2] = c.Color1.B
	data[17+3] = c.Color1.A
	data[21+0] = c.Color2.R
	data[21+1] = c.Color2.G
	data[21+2] = c.Color2.B
	data[21+3] = c.Color2.A
}

func (d PaintOp) Add(o *op.Ops) {
	data := ops.Write(&o.Internal, ops.TypePaintLen)
	data[0] = byte(ops.TypePaint)
}

// FillShape fills the clip shape with a color.
func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) {
	defer shape.Push(ops).Pop()
	Fill(ops, c)
}

// Fill paints an infinitely large plane with the provided color. It
// is intended to be used with a clip.Op already in place to limit
// the painted area. Use FillShape unless you need to paint several
// times within the same clip.Op.
func Fill(ops *op.Ops, c color.NRGBA) {
	ColorOp{Color: c}.Add(ops)
	PaintOp{}.Add(ops)
}

// PushOpacity creates a drawing layer with an opacity in the range [0;1].
// The layer includes every subsequent drawing operation until [OpacityStack.Pop]
// is called.
//
// The layer is drawn in two steps. First, the layer operations are
// drawn to a separate image. Then, the image is blended on top of
// the frame, with the opacity used as the blending factor.
func PushOpacity(o *op.Ops, opacity float32) OpacityStack {
	if opacity > 1 {
		opacity = 1
	}
	if opacity < 0 {
		opacity = 0
	}
	id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack)
	data := ops.Write(&o.Internal, ops.TypePushOpacityLen)
	bo := binary.LittleEndian
	data[0] = byte(ops.TypePushOpacity)
	bo.PutUint32(data[1:], math.Float32bits(opacity))
	return OpacityStack{ops: &o.Internal, id: id, macroID: macroID}
}

func (t OpacityStack) Pop() {
	ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID)
	data := ops.Write(t.ops, ops.TypePopOpacityLen)
	data[0] = byte(ops.TypePopOpacity)
}
