// Copyright 2022 The Ebitengine Authors
//
// Licensed 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 vector

import (
	"image"
	"image/color"
	"math"
	"sync"
	_ "unsafe"

	"github.com/hajimehoshi/ebiten/v2"
)

var (
	whiteImage    = ebiten.NewImage(3, 3)
	whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
)

var (
	theCachedVerticesForUtil []ebiten.Vertex
	theCachedIndicesForUtil  []uint32
	theCacheForUtilM         sync.Mutex
)

func useCachedVerticesAndIndicesForUtil(fn func([]ebiten.Vertex, []uint32) (vs []ebiten.Vertex, is []uint32)) {
	theCacheForUtilM.Lock()
	defer theCacheForUtilM.Unlock()
	theCachedVerticesForUtil, theCachedIndicesForUtil = fn(theCachedVerticesForUtil[:0], theCachedIndicesForUtil[:0])
}

func init() {
	b := whiteImage.Bounds()
	pix := make([]byte, 4*b.Dx()*b.Dy())
	for i := range pix {
		pix[i] = 0xff
	}
	// This is hacky, but WritePixels is better than Fill in term of automatic texture packing.
	whiteImage.WritePixels(pix)
}

// StrokeLine strokes a line (x0, y0)-(x1, y1) with the specified width and color.
func StrokeLine(dst *ebiten.Image, x0, y0, x1, y1 float32, strokeWidth float32, clr color.Color, antialias bool) {
	if antialias {
		var path Path
		path.MoveTo(x0, y0)
		path.LineTo(x1, y1)
		strokeOp := &StrokeOptions{}
		strokeOp.Width = strokeWidth
		drawOp := &DrawPathOptions{}
		drawOp.AntiAlias = true
		drawOp.ColorScale.ScaleWithColor(clr)
		StrokePath(dst, &path, strokeOp, drawOp)
		return
	}

	// Use a regular DrawImage for batching.
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Scale(math.Hypot(float64(x1-x0), float64(y1-y0)), float64(strokeWidth))
	op.GeoM.Translate(0, -float64(strokeWidth)/2)
	op.GeoM.Rotate(math.Atan2(float64(y1-y0), float64(x1-x0)))
	op.GeoM.Translate(float64(x0), float64(y0))
	op.ColorScale.ScaleWithColor(clr)
	dst.DrawImage(whiteSubImage, op)
}

// FillRect fills a rectangle with the specified width and color.
func FillRect(dst *ebiten.Image, x, y, width, height float32, clr color.Color, antialias bool) {
	if antialias {
		var path Path
		path.MoveTo(x, y)
		path.LineTo(x, y+height)
		path.LineTo(x+width, y+height)
		path.LineTo(x+width, y)
		drawOp := &DrawPathOptions{}
		drawOp.AntiAlias = true
		drawOp.ColorScale.ScaleWithColor(clr)
		FillPath(dst, &path, nil, drawOp)
		return
	}

	// Use a regular DrawImage for batching.
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Scale(float64(width), float64(height))
	op.GeoM.Translate(float64(x), float64(y))
	op.ColorScale.ScaleWithColor(clr)
	dst.DrawImage(whiteSubImage, op)
}

// DrawFilledRect fills a rectangle with the specified width and color.
//
// Deprecated: as of v2.9. Use [FillRect] instead.
func DrawFilledRect(dst *ebiten.Image, x, y, width, height float32, clr color.Color, antialias bool) {
	FillRect(dst, x, y, width, height, clr, antialias)
}

// StrokeRect strokes a rectangle with the specified width and color.
func StrokeRect(dst *ebiten.Image, x, y, width, height float32, strokeWidth float32, clr color.Color, antialias bool) {
	if antialias {
		var path Path
		path.MoveTo(x, y)
		path.LineTo(x, y+height)
		path.LineTo(x+width, y+height)
		path.LineTo(x+width, y)
		path.Close()
		strokeOp := &StrokeOptions{}
		strokeOp.Width = strokeWidth
		strokeOp.MiterLimit = 10
		drawOp := &DrawPathOptions{}
		drawOp.AntiAlias = true
		drawOp.ColorScale.ScaleWithColor(clr)
		StrokePath(dst, &path, strokeOp, drawOp)
		return
	}

	if strokeWidth <= 0 {
		return
	}

	if strokeWidth >= width || strokeWidth >= height {
		FillRect(dst, x-strokeWidth/2, y-strokeWidth/2, width+strokeWidth, height+strokeWidth, clr, false)
		return
	}

	// Use a regular DrawImage for batching.
	{
		// Render the top side.
		op := &ebiten.DrawImageOptions{}
		op.GeoM.Scale(float64(width+strokeWidth), float64(strokeWidth))
		op.GeoM.Translate(float64(x-strokeWidth/2), float64(y-strokeWidth/2))
		op.ColorScale.ScaleWithColor(clr)
		dst.DrawImage(whiteSubImage, op)
	}
	{
		// Render the left side.
		op := &ebiten.DrawImageOptions{}
		op.GeoM.Scale(float64(strokeWidth), float64(height-strokeWidth))
		op.GeoM.Translate(float64(x-strokeWidth/2), float64(y+strokeWidth/2))
		op.ColorScale.ScaleWithColor(clr)
		dst.DrawImage(whiteSubImage, op)
	}
	{
		// Render the right side.
		op := &ebiten.DrawImageOptions{}
		op.GeoM.Scale(float64(strokeWidth), float64(height-strokeWidth))
		op.GeoM.Translate(float64(x+width-strokeWidth/2), float64(y+strokeWidth/2))
		op.ColorScale.ScaleWithColor(clr)
		dst.DrawImage(whiteSubImage, op)
	}
	{
		// Render the bottom side.
		op := &ebiten.DrawImageOptions{}
		op.GeoM.Scale(float64(width+strokeWidth), float64(strokeWidth))
		op.GeoM.Translate(float64(x-strokeWidth/2), float64(y+height-strokeWidth/2))
		op.ColorScale.ScaleWithColor(clr)
		dst.DrawImage(whiteSubImage, op)
	}
}

// FillCircle fills a circle with the specified center position (cx, cy), the radius (r), width and color.
func FillCircle(dst *ebiten.Image, cx, cy, r float32, clr color.Color, antialias bool) {
	if antialias {
		var path Path
		path.Arc(cx, cy, r, 0, 2*math.Pi, Clockwise)
		drawOp := &DrawPathOptions{}
		drawOp.AntiAlias = true
		drawOp.ColorScale.ScaleWithColor(clr)
		FillPath(dst, &path, nil, drawOp)
		return
	}

	// Use a regular DrawTriangles32 for batching.
	cr, cg, cb, ca := clr.RGBA()
	crf := float32(cr) / 0xffff
	cgf := float32(cg) / 0xffff
	cbf := float32(cb) / 0xffff
	caf := float32(ca) / 0xffff
	useCachedVerticesAndIndicesForUtil(func(vs []ebiten.Vertex, is []uint32) ([]ebiten.Vertex, []uint32) {
		count := int(math.Ceil(math.Pi * float64(r)))
		for i := range count {
			angle := float64(i) * (2 * math.Pi / float64(count))
			sin, cos := math.Sincos(angle)
			x := cx + r*float32(cos)
			y := cy + r*float32(sin)
			vs = append(vs, ebiten.Vertex{
				DstX:   x,
				DstY:   y,
				SrcX:   1,
				SrcY:   1,
				ColorR: crf,
				ColorG: cgf,
				ColorB: cbf,
				ColorA: caf,
			})
			if i > 1 {
				idx := uint32(len(vs))
				is = append(is, 0, idx-1, idx-2)
			}
		}
		op := &ebiten.DrawTrianglesOptions{}
		op.ColorScaleMode = ebiten.ColorScaleModePremultipliedAlpha
		dst.DrawTriangles32(vs, is, whiteSubImage, op)
		return vs, is
	})
}

// DrawFilledCircle fills a circle with the specified center position (cx, cy), the radius (r), width and color.
//
// Deprecated: as of v2.9. Use [FillCircle] instead.
func DrawFilledCircle(dst *ebiten.Image, cx, cy, r float32, clr color.Color, antialias bool) {
	FillCircle(dst, cx, cy, r, clr, antialias)
}

// StrokeCircle strokes a circle with the specified center position (cx, cy), the radius (r), width and color.
func StrokeCircle(dst *ebiten.Image, cx, cy, r float32, strokeWidth float32, clr color.Color, antialias bool) {
	if antialias {
		var path Path
		path.Arc(cx, cy, r, 0, 2*math.Pi, Clockwise)
		path.Close()
		strokeOp := &StrokeOptions{}
		strokeOp.Width = strokeWidth
		strokeOp.LineJoin = LineJoinRound
		drawOp := &DrawPathOptions{}
		drawOp.AntiAlias = true
		drawOp.ColorScale.ScaleWithColor(clr)
		StrokePath(dst, &path, strokeOp, drawOp)
		return
	}

	if strokeWidth <= 0 {
		return
	}

	if strokeWidth >= r {
		FillCircle(dst, cx, cy, r+strokeWidth/2, clr, false)
		return
	}

	// Use a regular DrawTriangles32 for batching.
	cr, cg, cb, ca := clr.RGBA()
	crf := float32(cr) / 0xffff
	cgf := float32(cg) / 0xffff
	cbf := float32(cb) / 0xffff
	caf := float32(ca) / 0xffff
	useCachedVerticesAndIndicesForUtil(func(vs []ebiten.Vertex, is []uint32) ([]ebiten.Vertex, []uint32) {
		count := int(math.Ceil(math.Pi * float64(r+strokeWidth/2)))
		for i := range count {
			angle := float64(i) * (2 * math.Pi / float64(count))
			sin, cos := math.Sincos(angle)
			x0 := cx + (r+strokeWidth/2)*float32(cos)
			y0 := cy + (r+strokeWidth/2)*float32(sin)
			vs = append(vs, ebiten.Vertex{
				DstX:   x0,
				DstY:   y0,
				SrcX:   1,
				SrcY:   1,
				ColorR: crf,
				ColorG: cgf,
				ColorB: cbf,
				ColorA: caf,
			})
			x1 := cx + (r-strokeWidth/2)*float32(cos)
			y1 := cy + (r-strokeWidth/2)*float32(sin)
			vs = append(vs, ebiten.Vertex{
				DstX:   x1,
				DstY:   y1,
				SrcX:   1,
				SrcY:   1,
				ColorR: crf,
				ColorG: cgf,
				ColorB: cbf,
				ColorA: caf,
			})
			idx := uint32(2 * i)
			total := uint32(2 * count)
			is = append(is, idx, idx+1, (idx+2)%total, idx+1, (idx+2)%total, (idx+3)%total)
		}
		op := &ebiten.DrawTrianglesOptions{}
		op.ColorScaleMode = ebiten.ColorScaleModePremultipliedAlpha
		dst.DrawTriangles32(vs, is, whiteSubImage, op)
		return vs, is
	})
}

// FillRule is the rule whether an overlapped region is rendered or not.
type FillRule int

const (
	// FillRuleNonZero means that triangles are rendered based on the non-zero rule.
	// If and only if the number of overlaps is not 0, the region is rendered.
	FillRuleNonZero FillRule = iota

	// FillRuleEvenOdd means that triangles are rendered based on the even-odd rule.
	// If and only if the number of overlaps is odd, the region is rendered.
	FillRuleEvenOdd
)

var (
	theCallbackTokens      = map[*ebiten.Image]int64{}
	theFillPathsStates     = map[*ebiten.Image]*fillPathsState{}
	theFillPathsStatesPool = sync.Pool{
		New: func() any {
			return &fillPathsState{}
		},
	}
	theFillPathM sync.Mutex
)

// FillOptions is options to fill a path.
type FillOptions struct {
	// FillRule is the rule whether an overlapped region is rendered or not.
	// The default (zero) value is FillRuleNonZero.
	FillRule FillRule
}

// DrawPathOptions is options to draw a path.
type DrawPathOptions struct {
	// AntiAlias is whether the path is drawn with anti-aliasing.
	// The default (zero) value is false.
	AntiAlias bool

	// ColorScale is the color scale to apply to the path.
	// The default (zero) value is identity, which is (1, 1, 1, 1) (white).
	ColorScale ebiten.ColorScale

	// Blend is the blend mode to apply to the path.
	// The default (zero) value is ebiten.BlendSourceOver.
	Blend ebiten.Blend
}

// FillPath fills the specified path with the specified options.
func FillPath(dst *ebiten.Image, path *Path, fillOptions *FillOptions, drawPathOptions *DrawPathOptions) {
	if drawPathOptions == nil {
		drawPathOptions = &DrawPathOptions{}
	}
	if fillOptions == nil {
		fillOptions = &FillOptions{}
	}

	bounds := dst.Bounds()

	// Get the original image if dst is a sub-image to integrate the callbacks.
	dst = originalImage(dst)

	theFillPathM.Lock()
	defer theFillPathM.Unlock()

	// Remove the previous registered callbacks.
	if token, ok := theCallbackTokens[dst]; ok {
		removeUsageCallback(dst, token)
	}
	delete(theCallbackTokens, dst)

	if _, ok := theFillPathsStates[dst]; !ok {
		theFillPathsStates[dst] = theFillPathsStatesPool.Get().(*fillPathsState)
	}
	s := theFillPathsStates[dst]
	if s.antialias != drawPathOptions.AntiAlias || s.blend != drawPathOptions.Blend || s.fillRule != fillOptions.FillRule {
		s.fillPaths(dst)
		s.reset()
	}
	s.antialias = drawPathOptions.AntiAlias
	s.blend = drawPathOptions.Blend
	s.fillRule = fillOptions.FillRule
	s.addPath(path, bounds, drawPathOptions.ColorScale)

	// Use an independent callback function to avoid unexpected captures.
	theCallbackTokens[dst] = addUsageCallback(dst, fillPathCallback)
}

func fillPathCallback(dst *ebiten.Image) {
	if originalImage(dst) != dst {
		panic("vector: dst must be the original image")
	}

	theFillPathM.Lock()
	defer theFillPathM.Unlock()

	// Remove the callback not to call this twice.
	if token, ok := theCallbackTokens[dst]; ok {
		removeUsageCallback(dst, token)
	}
	delete(theCallbackTokens, dst)

	s, ok := theFillPathsStates[dst]
	if !ok {
		panic("vector: fillPathsState must exist here")
	}
	s.fillPaths(dst)
	s.reset()
	delete(theFillPathsStates, dst)
	theFillPathsStatesPool.Put(s)
}

// StrokePath strokes the specified path with the specified options.
func StrokePath(dst *ebiten.Image, path *Path, strokeOptions *StrokeOptions, drawPathOptions *DrawPathOptions) {
	var stroke Path
	op := &AddStrokeOptions{}
	op.StrokeOptions = *strokeOptions
	stroke.AddStroke(path, op)
	FillPath(dst, &stroke, nil, drawPathOptions)
}

//go:linkname originalImage github.com/hajimehoshi/ebiten/v2.originalImage
func originalImage(img *ebiten.Image) *ebiten.Image

//go:linkname addUsageCallback github.com/hajimehoshi/ebiten/v2.addUsageCallback
func addUsageCallback(img *ebiten.Image, fn func(img *ebiten.Image)) int64

//go:linkname removeUsageCallback github.com/hajimehoshi/ebiten/v2.removeUsageCallback
func removeUsageCallback(img *ebiten.Image, token int64)
