// Copyright 2016 Minho. All rights reserved.
// Copyright 2021 Flamego. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package captchautil

import (
	"image"
	"image/color"
	"image/draw"
	"image/jpeg"
	"io"
	"math"
	"math/rand"
	"time"

	"github.com/golang/freetype"
	"github.com/pkg/errors"
	"golang.org/x/image/font"
)

var r = rand.New(rand.NewSource(time.Now().UnixNano()))

type Complexity int

const (
	ComplexityLow Complexity = iota
	ComplexityMedium
	ComplexityHigh
)

// Image is a captcha image.
type Image struct {
	width  int
	height int
	dpi    float64
	nrgba  *image.NRGBA
}

type Option func(*Image) error

// CreateImage creates and returns a new image with given width, height, DPI,
// background color and additional options.
func CreateImage(width, height, dpi int, bgColor color.RGBA, opts ...Option) (*Image, error) {
	nrgba := image.NewNRGBA(image.Rect(0, 0, width, height))
	draw.Draw(nrgba, nrgba.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src)

	img := &Image{
		height: height,
		width:  width,
		dpi:    float64(dpi),
		nrgba:  nrgba,
	}

	for _, opt := range opts {
		err := opt(img)
		if err != nil {
			return nil, err
		}
	}
	return img, nil
}

// Encode encodes the final JPEG image to the given writer.
func (img *Image) Encode(w io.Writer) error {
	return jpeg.Encode(w, img.nrgba, &jpeg.Options{Quality: 100})
}

// Noise draws background noise on the image.
func Noise(complex Complexity) Option {
	return func(img *Image) error {
		density := 18
		if complex == ComplexityLow {
			density = 28
		} else if complex == ComplexityMedium {
			density = 18
		} else if complex == ComplexityHigh {
			density = 8
		}
		maxSize := (img.height * img.width) / density
		for i := 0; i < maxSize; i++ {
			rw := r.Intn(img.width)
			rh := r.Intn(img.height)
			img.nrgba.Set(rw, rh, randomColor())
			size := r.Intn(maxSize)
			if size%3 == 0 {
				img.nrgba.Set(rw+1, rh+1, randomColor())
			}
		}
		return nil
	}
}

// TextNoise draws background text noise on the image.
func TextNoise(complex Complexity) Option {
	return func(img *Image) error {
		density := 1500
		if complex == ComplexityLow {
			density = 2000
		} else if complex == ComplexityMedium {
			density = 1500
		} else if complex == ComplexityHigh {
			density = 1000
		}
		maxSize := (img.height * img.width) / density

		c := freetype.NewContext()
		c.SetDPI(img.dpi)
		c.SetClip(img.nrgba.Bounds())
		c.SetDst(img.nrgba)
		c.SetHinting(font.HintingFull)
		rawFontSize := float64(img.height) / (1 + float64(r.Intn(7))/float64(10))

		for i := 0; i < maxSize; i++ {
			text, err := RandomText(1)
			if err != nil {
				return errors.Wrap(err, "TextNoise: get random text")
			}
			fontSize := rawFontSize/2 + float64(r.Intn(5))

			c.SetSrc(image.NewUniform(RandomLightColor()))
			c.SetFontSize(fontSize)

			f, err := randFontFamily()
			if err != nil {
				return errors.Wrap(err, "TextNoise: get random font family")
			}
			c.SetFont(f)

			rw := r.Intn(img.width)
			rh := r.Intn(img.height)
			pt := freetype.Pt(rw, rh)

			_, err = c.DrawString(text, pt)
			if err != nil {
				return errors.Wrap(err, "TextNoise: draw string")
			}
		}
		return nil
	}
}

// Text draws text on the image.
func Text(text string) Option {
	return func(img *Image) error {
		c := freetype.NewContext()
		c.SetDPI(img.dpi)
		c.SetClip(img.nrgba.Bounds())
		c.SetDst(img.nrgba)
		c.SetHinting(font.HintingFull)

		fontWidth := img.width / len(text)
		for i, s := range text {
			fontSize := float64(img.height) / (1 + float64(r.Intn(7))/float64(9))
			c.SetSrc(image.NewUniform(randomDeepColor()))
			c.SetFontSize(fontSize)

			f, err := randFontFamily()
			if err != nil {
				return errors.Wrap(err, "Text: get random font family")
			}
			c.SetFont(f)

			x := (fontWidth)*i + (fontWidth)/int(fontSize)
			y := 5 + r.Intn(img.height/2) + int(fontSize/2)
			pt := freetype.Pt(x, y)

			_, err = c.DrawString(string(s), pt)
			if err != nil {
				return errors.Wrap(err, "Text: draw string")
			}
		}
		return nil
	}
}

// Border draws border on the image.
func Border(color color.RGBA) Option {
	return func(img *Image) error {
		for x := 0; x < img.width; x++ {
			img.nrgba.Set(x, 0, color)
			img.nrgba.Set(x, img.height-1, color)
		}
		for y := 0; y < img.height; y++ {
			img.nrgba.Set(0, y, color)
			img.nrgba.Set(img.width-1, y, color)
		}
		return nil
	}
}

// Curve draws a curve on the image.
func Curve() Option {
	return func(img *Image) error {
		random := func(min, max int64) float64 {
			decimal := rand.Float64()
			if max <= 0 {
				return (float64(rand.Int63n((min*-1)-(max*-1))+(max*-1)) + decimal) * -1
			}
			if min < 0 && max > 0 {
				if rand.Int()%2 == 0 {
					return float64(rand.Int63n(max)) + decimal
				} else {
					return (float64(rand.Int63n(min*-1)) + decimal) * -1
				}
			}
			return float64(rand.Int63n(max-min)+min) + decimal
		}

		a := r.Intn(img.height / 2)                            // Amplitude
		b := random(int64(-img.height/4), int64(img.height/4)) // Y-axis offset
		f := random(int64(-img.height/4), int64(img.height/4)) // X-axis offset
		// Period
		var t float64
		if img.height > img.width/2 {
			t = random(int64(img.width/2), int64(img.height))
		} else {
			t = random(int64(img.height), int64(img.width/2))
		}
		w := (2 * math.Pi) / t

		end := int(random(int64(float64(img.width)*0.8), int64(img.width)))
		c := color.RGBA{
			R: uint8(r.Intn(150)),
			G: uint8(r.Intn(150)),
			B: uint8(r.Intn(150)),
			A: uint8(255),
		}

		py := float64(0)
		for px := 0; px < end; px++ {
			if w != 0 {
				py = float64(a)*math.Sin(w*float64(px)+f) + b + (float64(img.width) / float64(5))
				i := img.height / 5
				for i > 0 {
					img.nrgba.Set(px+i, int(py), c)
					i--
				}
			}
		}
		return nil
	}
}
