package main

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"math"
	"os"
)

type circle struct {
	p image.Point
	r int
}

const (
	white = 0xFF
	black = 0x17
)

func (c *circle) ColorModel() color.Model {
	return color.AlphaModel
}

func (c *circle) Bounds() image.Rectangle {
	return image.Rect(
		c.p.X-c.r,
		c.p.Y-c.r,
		c.p.X+c.r,
		c.p.Y+c.r,
	)
}

const (
	halfPixel      = 0.5
	doublingFactor = 2
)

func double(i int) int { return i * doublingFactor }
func half(i int) int   { return i / doublingFactor }

func (c *circle) At(x, y int) color.Color {
	// Prepare points for circle calculations.
	// We subtract 1 from the radius to leave space for
	// antialiased pixels.
	xx := float64(x-c.p.X) + halfPixel
	yy := float64(y-c.p.Y) + halfPixel
	rr := float64(c.r) - 1

	// The distance from this pixel to the closest point
	// in the circle.
	dist := math.Sqrt(xx*xx+yy*yy) - rr

	if dist < 0 {
		// This pixel is inside the circle
		return color.Alpha{white}
	} else if dist <= 1 {
		// This pixel is partly inside the circle
		// and needs antialiasing
		return color.Alpha{
			uint8((1 - dist) * white),
		}
	}

	// This pixel is outside the circle
	// and should be fully transparent
	return color.Alpha{0x00}
}

type rect struct {
	pa image.Point
	pb image.Point
}

func (r *rect) ColorModel() color.Model {
	return color.AlphaModel
}

func (r *rect) Bounds() image.Rectangle {
	return image.Rect(r.pa.X, r.pa.Y, r.pb.X, r.pb.Y)
}

func (r *rect) At(x, y int) color.Color {
	if (x >= r.pa.X) &&
		(x < r.pb.X) &&
		(y >= r.pa.Y) &&
		(y < r.pb.Y) {
		return color.Alpha{white}
	}
	return color.Alpha{0x00}
}

type roundedrect struct {
	pa     image.Point
	pb     image.Point
	radius int
}

func (r *roundedrect) ColorModel() color.Model {
	return color.AlphaModel
}

func (r *roundedrect) Bounds() image.Rectangle {
	return image.Rect(r.pa.X, r.pa.Y, r.pb.X, r.pb.Y)
}

func (r *roundedrect) At(x, y int) color.Color {
	// Top-left corner
	if (x >= r.pa.X) &&
		(x < r.pa.X+r.radius) &&
		(y >= r.pa.Y) &&
		(y < r.pa.Y+r.radius) {
		c := circle{
			image.Point{
				r.radius,
				r.radius,
			},
			// Add one to corner radius so that
			// fully-opaque pixels match the rectangle.
			// The outermost pixels of a circle are
			// always antialiased and thus transparent.
			r.radius + 1,
		}
		return c.At(x, y)
	}

	// Top-right corner
	if (x >= r.pb.X-r.radius) &&
		(x < r.pb.X) &&
		(y >= r.pa.Y) &&
		(y < r.pa.Y+r.radius) {
		c := circle{
			image.Point{
				r.pb.X - r.radius,
				r.radius,
			},
			r.radius + 1,
		}
		return c.At(x, y)
	}

	// Bottom-left corner
	if (x >= r.pa.X) &&
		(x < r.pa.X+r.radius) &&
		(y >= r.pb.Y-r.radius) &&
		(y < r.pb.Y) {
		c := circle{
			image.Point{
				r.radius,
				r.pb.Y - r.radius,
			},
			r.radius + 1,
		}
		return c.At(x, y)
	}

	// Bottom-right corner
	if (x >= r.pb.X-r.radius) &&
		(x < r.pb.X) &&
		(y >= r.pb.Y-r.radius) &&
		(y < r.pb.Y) {
		c := circle{
			image.Point{
				r.pb.X - r.radius,
				r.pb.Y - r.radius,
			},
			r.radius + 1,
		}
		return c.At(x, y)
	}

	return color.Alpha{white}
}

// MakeBorderRadiusMask a mask to round a terminal's corners.
func MakeBorderRadiusMask(width, height, radius int, targetpng string) {
	img := image.NewGray(
		image.Rectangle{
			image.Point{0, 0},
			image.Point{width, height},
		},
	)

	// Fill image with black
	draw.DrawMask(
		img, img.Bounds(), &image.Uniform{color.Gray{0x00}}, image.Point{0, 0},
		&rect{image.Point{0, 0}, image.Point{width, height}},
		image.Point{0, 0}, draw.Src,
	)

	// Put mask in white on top
	draw.DrawMask(
		img, img.Bounds(), &image.Uniform{color.Gray{white}}, image.Point{0, 0},
		&roundedrect{image.Point{0, 0}, image.Point{width, height}, radius},
		image.Point{0, 0}, draw.Over,
	)

	f, err := os.Create(targetpng)
	if err != nil {
		fmt.Println(ErrorStyle.Render("Could not draw Border Mask: unable to save file."))
	} else {
		err = png.Encode(f, img)
	}

	if err != nil {
		fmt.Println(ErrorStyle.Render("Could not draw Border Mask: encoding failed."))
	}
}

// MakeWindowBar a window bar and save it to a file.
func MakeWindowBar(termWidth, termHeight int, opts StyleOptions, file string) {
	var err error
	switch opts.WindowBar {
	case "Colorful":
		err = makeColorfulBar(termWidth, termHeight, false, opts, file)
	case "ColorfulRight":
		err = makeColorfulBar(termWidth, termHeight, true, opts, file)
	case "Rings":
		err = makeRingBar(termWidth, termHeight, false, opts, file)
	case "RingsRight":
		err = makeRingBar(termWidth, termHeight, true, opts, file)
	}

	if err != nil {
		fmt.Println(ErrorStyle.Render("Couldn't draw Bar: encoding failed"))
	}
}

const (
	barToDotRatio       = 6
	barToDotBorderRatio = 5
)

func makeColorfulBar(termWidth int, termHeight int, isRight bool, opts StyleOptions, targetpng string) error {
	// Radius of dots
	dotRad := opts.WindowBarSize / barToDotRatio
	dotDia := double(dotRad)
	// Space between dots and edge
	dotGap := half(opts.WindowBarSize - dotDia)
	// Space between dot centers
	dotSpace := dotDia + opts.WindowBarSize/barToDotRatio

	// Dimensions of bar image
	width := termWidth
	height := termHeight + opts.WindowBarSize

	img := image.NewRGBA(
		image.Rectangle{
			image.Point{0, 0},
			image.Point{width, height},
		},
	)

	bg, _ := parseHexColor(opts.WindowBarColor)
	dotA := color.RGBA{white, 0x4F, 0x4D, white}
	dotB := color.RGBA{0xFE, 0xBB, 0x00, white}
	dotC := color.RGBA{0x00, 0xCC, 0x1D, white}

	var pta, ptb, ptc image.Point
	if isRight {
		pta = image.Point{termWidth - (dotGap + dotRad), dotRad + dotGap}
		ptb = image.Point{termWidth - (dotGap + dotRad + dotSpace), dotRad + dotGap}
		ptc = image.Point{termWidth - (dotGap + dotRad + 2*dotSpace), dotRad + dotGap}
	} else {
		pta = image.Point{dotGap + dotRad, dotRad + dotGap}
		ptb = image.Point{dotGap + dotRad + dotSpace, dotRad + dotGap}
		ptc = image.Point{dotGap + dotRad + 2*dotSpace, dotRad + dotGap}
	}

	draw.DrawMask(
		img, img.Bounds(), &image.Uniform{bg}, image.Point{0, 0},
		&rect{image.Point{0, 0}, image.Point{width, height}},
		image.Point{0, 0}, draw.Src,
	)

	draw.DrawMask(
		img,
		img.Bounds(),
		&image.Uniform{dotA},
		image.Point{0, 0},
		&circle{pta, dotRad},
		image.Point{0, 0},
		draw.Over,
	)

	draw.DrawMask(
		img,
		img.Bounds(),
		&image.Uniform{dotB},
		image.Point{0, 0},
		&circle{ptb, dotRad},
		image.Point{0, 0},
		draw.Over,
	)

	draw.DrawMask(
		img,
		img.Bounds(),
		&image.Uniform{dotC},
		image.Point{0, 0},
		&circle{ptc, dotRad},
		image.Point{0, 0},
		draw.Over,
	)

	f, err := os.Create(targetpng)
	if err != nil {
		fmt.Println(ErrorStyle.Render("Couldn't draw colorful bar: unable to save file."))
	} else {
		err = png.Encode(f, img)
	}
	return err //nolint:wrapcheck
}

func makeRingBar(termWidth int, termHeight int, isRight bool, opts StyleOptions, targetpng string) error {
	// Radius of dots
	outerRad := opts.WindowBarSize / barToDotBorderRatio
	outerDia := double(outerRad)
	innerRad := double(outerDia) / barToDotBorderRatio
	// Space between dots and edge
	ringGap := half(opts.WindowBarSize - outerDia)
	// Space between dot centers
	ringSpace := outerDia + opts.WindowBarSize/barToDotRatio

	// Dimensions of bar image
	width := termWidth
	height := termHeight + opts.WindowBarSize

	img := image.NewRGBA(
		image.Rectangle{
			image.Point{0, 0},
			image.Point{width, height},
		},
	)

	bg, _ := parseHexColor(opts.WindowBarColor)
	ring := color.RGBA{0x33, 0x33, 0x33, white}

	draw.DrawMask(
		img, img.Bounds(), &image.Uniform{bg}, image.Point{0, 0},
		&rect{image.Point{0, 0}, image.Point{width, height}},
		image.Point{0, 0}, draw.Src,
	)

	for i := 0; i <= 2; i++ {
		var pt image.Point
		if isRight {
			pt = image.Point{
				termWidth - (ringGap + outerRad + i*ringSpace),
				outerRad + ringGap,
			}
		} else {
			pt = image.Point{
				ringGap + outerRad + i*ringSpace,
				outerRad + ringGap,
			}
		}

		draw.DrawMask(
			img,
			img.Bounds(),
			&image.Uniform{ring},
			image.Point{0, 0},
			&circle{pt, outerRad},
			image.Point{0, 0},
			draw.Over,
		)

		draw.DrawMask(
			img,
			img.Bounds(),
			&image.Uniform{bg},
			image.Point{0, 0},
			&circle{pt, innerRad},
			image.Point{0, 0},
			draw.Over,
		)
	}

	f, err := os.Create(targetpng)
	if err != nil {
		fmt.Println(ErrorStyle.Render("Couldn't draw ring bar: unable to save file."))
	} else {
		err = png.Encode(f, img)
	}
	return err //nolint:wrapcheck
}

//nolint:mnd
func parseHexColor(s string) (c color.RGBA, err error) {
	c.R, c.G, c.B, c.A = black, black, black, white
	switch len(s) {
	case 7:
		_, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B)
	case 6:
		_, err = fmt.Sscanf(s, "%02x%02x%02x", &c.R, &c.G, &c.B)
	case 4:
		_, err = fmt.Sscanf(s, "#%1x%1x%1x", &c.R, &c.G, &c.B)
		// Double the hex digits:
		c.R *= 17
		c.G *= 17
		c.B *= 17
	case 3:
		_, err = fmt.Sscanf(s, "%1x%1x%1x", &c.R, &c.G, &c.B)
		// Double the hex digits:
		c.R *= 17
		c.G *= 17
		c.B *= 17
	default:
		err = fmt.Errorf("%s color of invalid length", s)
	}
	return
}
