package web

import (
	"bytes"
	_ "embed"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"log/slog"
	"math"
	"net/http"
	"sync"
	"time"

	"golang.org/x/image/font"
	"golang.org/x/image/font/opentype"
	"golang.org/x/image/math/fixed"
	"golang.org/x/image/vector"
)

// The card that shows up when somebody pastes the casino into a chat window.
//
// It is drawn here, in Go, rather than checked in as a picture, because the
// casino has two names on a clock and the share card keeps the joke: paste the
// link in daylight and you get Casinopolis on green felt; paste it after six and
// the neon is on and the sign says Casino Night Zone. Same tables, different room,
// same rule as roomAt() everywhere else.
//
// The clock that decides is the *server's*, because an unfurl bot has no evening
// of its own. That's the one place the room rule can't be the player's own clock.

//go:embed assets/fonts/Fredoka-SemiBold.ttf
var fredokaTTF []byte

// Share cards are 1200x630: the size every unfurler crops to and the one thing
// about Open Graph that everybody agrees on.
const (
	ogWidth  = 1200
	ogHeight = 630
)

// ogPalette is a room's colours, lifted from the CSS block of the same name in
// input.css. Two copies of a palette is a thing that drifts, so if you retune a
// room, retune it here as well — TestTheShareCardKnowsBothRooms will not catch a
// colour, only a missing room.
type ogPalette struct {
	bg     color.RGBA // the room, behind the table
	feltA  color.RGBA // the felt, lit
	feltC  color.RGBA // the felt, in shadow
	ink    color.RGBA // type
	accent color.RGBA // the sign, and the lamp over the table
}

var ogPalettes = map[string]ogPalette{
	roomDay.Slug: {
		bg:     rgb(0x16, 0x21, 0x1c),
		feltA:  rgb(0x2f, 0x7d, 0x5b),
		feltC:  rgb(0x1c, 0x4d, 0x3c),
		ink:    rgb(0xf6, 0xec, 0xd8),
		accent: rgb(0xf2, 0xb5, 0x3d),
	},
	roomNight.Slug: {
		bg:     rgb(0x14, 0x0f, 0x2e),
		feltA:  rgb(0x4a, 0x2f, 0xa8),
		feltC:  rgb(0x24, 0x16, 0x59),
		ink:    rgb(0xf2, 0xec, 0xff),
		accent: rgb(0xff, 0xcc, 0x2f),
	},
}

// A card is drawn once per room and then kept. There are two of them, they never
// change, and an unfurl bot is not worth a rasterizer.
var (
	ogOnce  sync.Once
	ogCards map[string][]byte // room slug -> PNG
	ogErr   error
)

func (s *Server) handleGamesOG(w http.ResponseWriter, r *http.Request) {
	if !s.gamesReady() {
		http.NotFound(w, r)
		return
	}
	// No requirePlayer here, and that is the whole point: the thing fetching this
	// is a chat server's link preview, which has never signed in and never will.
	ogOnce.Do(func() { ogCards, ogErr = drawShareCards() })
	room := roomAt(time.Now().Hour())
	card := ogCards[room.Slug]
	if ogErr != nil || len(card) == 0 {
		// A room with no card is a room somebody added to roomAt and not to
		// ogPalettes. Say so rather than serving a zero-byte PNG, which an unfurler
		// will happily cache as the casino's face.
		slog.Error("games: no share card", "room", room.Slug, "err", ogErr)
		http.Error(w, "share card unavailable", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "image/png")
	// Long enough that a busy channel isn't redrawing it, short enough that the
	// room actually turns over: the lights come on at six and the card follows
	// within the hour.
	w.Header().Set("Cache-Control", "public, max-age=900")
	// ServeContent sets Content-Length itself, and gets it right for a range
	// request, which a hand-written one would not.
	http.ServeContent(w, r, "og.png", time.Time{}, bytes.NewReader(card))
}

// drawShareCards renders every room's card up front. If the font is broken every
// card is broken, so they fail together or not at all.
func drawShareCards() (map[string][]byte, error) {
	f, err := opentype.Parse(fredokaTTF)
	if err != nil {
		return nil, fmt.Errorf("parse fredoka: %w", err)
	}
	out := make(map[string][]byte, len(ogPalettes))
	for _, rm := range []room{roomDay, roomNight} {
		img, err := drawShareCard(f, rm, ogPalettes[rm.Slug])
		if err != nil {
			return nil, err
		}
		var buf bytes.Buffer
		if err := png.Encode(&buf, img); err != nil {
			return nil, fmt.Errorf("encode %s: %w", rm.Slug, err)
		}
		out[rm.Slug] = buf.Bytes()
	}
	return out, nil
}

func drawShareCard(f *opentype.Font, rm room, p ogPalette) (image.Image, error) {
	title, err := face(f, 96)
	if err != nil {
		return nil, err
	}
	defer title.Close()
	line, err := face(f, 33)
	if err != nil {
		return nil, err
	}
	defer line.Close()
	small, err := face(f, 25)
	if err != nil {
		return nil, err
	}
	defer small.Close()

	img := image.NewRGBA(image.Rect(0, 0, ogWidth, ogHeight))
	draw.Draw(img, img.Bounds(), &image.Uniform{p.bg}, image.Point{}, draw.Src)

	// The table: a rounded rect of felt, lit from the top and falling into shadow
	// at the rail, which is the same trick the room's CSS plays with a gradient.
	table := func(rz *vector.Rasterizer) {
		roundRect(rz, 36, 36, ogWidth-72, ogHeight-72, 44, nil)
	}
	fillPath(img, verticalGradient(p.feltA, p.feltC), table)
	// The lamp over it. Nothing else in the picture explains where the light is
	// coming from, and a felt with no lamp reads as a green rectangle. It is
	// painted through the table's own shape, because light that spills onto the
	// floor outside the rail is not a lamp, it's a mistake.
	fillPath(img, lamp(ogWidth/2, 40, 520, p.accent, 0.26), table)

	// The sign over the door.
	centerText(img, title, p.accent, rm.Name, ogHeight/2-42)
	centerText(img, line, alpha(p.ink, 0.92), "Blackjack, Hold'em, UNO, Trivia, Hangman, Solitaire", ogHeight/2+26)
	centerText(img, small, alpha(p.ink, 0.62), "Played for real gogobee euros", ogHeight/2+78)

	// Bottom left: two cards, one face down and one face up, because a casino that
	// shows you only the backs is a casino that isn't dealing.
	drawCardBack(img, p, 118, 430, -13)
	drawCardFace(img, 196, 420, 7)

	// Bottom right: what you're playing for.
	chips := []color.RGBA{p.accent, rgb(0xd9, 0x4f, 0x4f), rgb(0xf6, 0xf1, 0xe6), p.accent}
	for i, c := range chips {
		drawChip(img, 1042, float32(536-i*23), 46, c, p)
	}

	// The address, small, in the corner. A share card is also a signpost.
	rightText(img, small, alpha(p.ink, 0.45), "games.parodia.dev", ogWidth-70, 96)
	return img, nil
}

// ---- shapes -----------------------------------------------------------------
//
// Everything below draws through a vector.Rasterizer the size of the whole card,
// which is wasteful and completely fine: it happens twice, at boot, forever.

// xform moves a point before it's rasterized. It is how anything here gets to sit
// at an angle — font.Drawer can't rotate, but a path can be rotated on its way in.
type xform func(x, y float32) (float32, float32)

func rotate(cx, cy, deg float32) xform {
	rad := float64(deg) * math.Pi / 180
	sin, cos := float32(math.Sin(rad)), float32(math.Cos(rad))
	return func(x, y float32) (float32, float32) {
		dx, dy := x-cx, y-cy
		return cx + dx*cos - dy*sin, cy + dx*sin + dy*cos
	}
}

func (t xform) at(x, y float32) (float32, float32) {
	if t == nil {
		return x, y
	}
	return t(x, y)
}

// fillPath rasterizes a path and paints src through it.
func fillPath(dst *image.RGBA, src image.Image, path func(*vector.Rasterizer)) {
	rz := vector.NewRasterizer(ogWidth, ogHeight)
	path(rz)
	rz.Draw(dst, dst.Bounds(), src, image.Point{})
}

func fill(dst *image.RGBA, c color.Color, path func(*vector.Rasterizer)) {
	fillPath(dst, &image.Uniform{c}, path)
}

// roundRect lays a rounded rectangle into the rasterizer, optionally through a
// transform, so the same helper draws a table and a card held at an angle.
func roundRect(rz *vector.Rasterizer, x, y, w, h, r float32, t xform) {
	move := func(px, py float32) { rz.MoveTo(t.at(px, py)) }
	line := func(px, py float32) { rz.LineTo(t.at(px, py)) }
	quad := func(cx, cy, px, py float32) {
		qx, qy := t.at(cx, cy)
		ex, ey := t.at(px, py)
		rz.QuadTo(qx, qy, ex, ey)
	}
	move(x+r, y)
	line(x+w-r, y)
	quad(x+w, y, x+w, y+r)
	line(x+w, y+h-r)
	quad(x+w, y+h, x+w-r, y+h)
	line(x+r, y+h)
	quad(x, y+h, x, y+h-r)
	line(x, y+r)
	quad(x, y, x+r, y)
	rz.ClosePath()
}

func circle(rz *vector.Rasterizer, cx, cy, r float32) {
	// Four cubics, the usual 0.5523 magic number for a circle out of beziers.
	const k = 0.5523
	rz.MoveTo(cx+r, cy)
	rz.CubeTo(cx+r, cy+r*k, cx+r*k, cy+r, cx, cy+r)
	rz.CubeTo(cx-r*k, cy+r, cx-r, cy+r*k, cx-r, cy)
	rz.CubeTo(cx-r, cy-r*k, cx-r*k, cy-r, cx, cy-r)
	rz.CubeTo(cx+r*k, cy-r, cx+r, cy-r*k, cx+r, cy)
	rz.ClosePath()
}

// hexagon is Pete's mark — the same six-sided badge as the favicon, which is what
// the back of the house's cards is printed with.
func hexagon(rz *vector.Rasterizer, cx, cy, r float32, t xform) {
	for i := 0; i < 6; i++ {
		ang := float64(i)*math.Pi/3 - math.Pi/2
		x := cx + r*float32(math.Cos(ang))
		y := cy + r*float32(math.Sin(ang))
		px, py := t.at(x, y)
		if i == 0 {
			rz.MoveTo(px, py)
		} else {
			rz.LineTo(px, py)
		}
	}
	rz.ClosePath()
}

const (
	cardW = 132
	cardH = 186
)

// drawCardBack is the house's card: dark, with the hexagon on it.
func drawCardBack(dst *image.RGBA, p ogPalette, x, y, deg float32) {
	t := rotate(x+cardW/2, y+cardH/2, deg)
	fill(dst, color.RGBA{0, 0, 0, 70}, func(rz *vector.Rasterizer) {
		roundRect(rz, x+5, y+8, cardW, cardH, 14, t)
	})
	fill(dst, alpha(p.ink, 0.96), func(rz *vector.Rasterizer) {
		roundRect(rz, x, y, cardW, cardH, 14, t)
	})
	fill(dst, darken(p.feltC, 0.55), func(rz *vector.Rasterizer) {
		roundRect(rz, x+9, y+9, cardW-18, cardH-18, 9, t)
	})
	fill(dst, p.accent, func(rz *vector.Rasterizer) {
		hexagon(rz, x+cardW/2, y+cardH/2, 34, t)
	})
	fill(dst, darken(p.feltC, 0.55), func(rz *vector.Rasterizer) {
		hexagon(rz, x+cardW/2, y+cardH/2, 20, t)
	})
}

// drawCardFace is the one that's been turned over: an ace of hearts, near enough.
func drawCardFace(dst *image.RGBA, x, y, deg float32) {
	t := rotate(x+cardW/2, y+cardH/2, deg)
	red := rgb(0xd9, 0x3b, 0x3b)
	fill(dst, color.RGBA{0, 0, 0, 80}, func(rz *vector.Rasterizer) {
		roundRect(rz, x+5, y+9, cardW, cardH, 14, t)
	})
	fill(dst, rgb(0xfb, 0xf7, 0xef), func(rz *vector.Rasterizer) {
		roundRect(rz, x, y, cardW, cardH, 14, t)
	})
	heart(dst, red, x+cardW/2, y+cardH/2+4, 40, t)
	// The pip in the corner, which is how you know it's a card and not a tile.
	heart(dst, red, x+24, y+30, 11, t)
}

// heart, as two lobes and a point.
func heart(dst *image.RGBA, c color.RGBA, cx, cy, r float32, t xform) {
	fill(dst, c, func(rz *vector.Rasterizer) {
		move := func(px, py float32) { rz.MoveTo(t.at(px, py)) }
		cube := func(ax, ay, bx, by, px, py float32) {
			a1, a2 := t.at(ax, ay)
			b1, b2 := t.at(bx, by)
			e1, e2 := t.at(px, py)
			rz.CubeTo(a1, a2, b1, b2, e1, e2)
		}
		bottom := cy + r*0.95
		move(cx, bottom)
		cube(cx-r*1.15, cy+r*0.18, cx-r*1.0, cy-r*0.95, cx, cy-r*0.28)
		cube(cx+r*1.0, cy-r*0.95, cx+r*1.15, cy+r*0.18, cx, bottom)
		rz.ClosePath()
	})
}

// drawChip is a chip seen face on: a disc, a ring of notches around the rim, and a
// pale centre. Stack four and it reads as money.
func drawChip(dst *image.RGBA, cx, cy, r float32, c color.RGBA, p ogPalette) {
	fill(dst, color.RGBA{0, 0, 0, 60}, func(rz *vector.Rasterizer) { circle(rz, cx+3, cy+5, r) })
	fill(dst, c, func(rz *vector.Rasterizer) { circle(rz, cx, cy, r) })
	// Six notches on the rim, in the room's ink, the way a real chip is edge-marked.
	for i := 0; i < 6; i++ {
		ang := float64(i) * math.Pi / 3
		nx := cx + (r-7)*float32(math.Cos(ang))
		ny := cy + (r-7)*float32(math.Sin(ang))
		fill(dst, alpha(p.ink, 0.85), func(rz *vector.Rasterizer) { circle(rz, nx, ny, 6) })
	}
	fill(dst, alpha(p.ink, 0.28), func(rz *vector.Rasterizer) { circle(rz, cx, cy, r-15) })
	fill(dst, darken(c, 0.82), func(rz *vector.Rasterizer) { circle(rz, cx, cy, r-21) })
}

// lamp is the light over the table: the accent colour, brightest under the bulb
// and gone by the rail. It's an image rather than a paint, so it can be laid down
// through the table's shape and stop at the felt's edge.
//
// Note the alpha() on the way out. color.RGBA is *alpha-premultiplied*, and the
// first version of this wrote the raw channels next to a low alpha — which is not
// a dim honey glow, it's an invalid colour, and image/draw ran it straight past
// 255 and wrapped it. The card came out with a blue dome over a green stripe. If
// something here ever turns an impossible colour, look for a premultiply first.
func lamp(cx, cy, radius float32, c color.RGBA, strength float64) image.Image {
	img := image.NewRGBA(image.Rect(0, 0, ogWidth, ogHeight))
	for y := 0; y < ogHeight; y++ {
		for x := 0; x < ogWidth; x++ {
			dx, dy := float64(float32(x)-cx), float64(float32(y)-cy)
			d := math.Sqrt(dx*dx+dy*dy) / float64(radius)
			if d >= 1 {
				continue
			}
			// Squared falloff: a lamp is bright under itself and dim at the edges.
			img.SetRGBA(x, y, alpha(c, strength*(1-d)*(1-d)))
		}
	}
	return img
}

// verticalGradient is the felt: lit at the top, in shadow at the rail.
func verticalGradient(top, bottom color.RGBA) image.Image {
	g := image.NewRGBA(image.Rect(0, 0, 1, ogHeight))
	for y := 0; y < ogHeight; y++ {
		f := float64(y) / float64(ogHeight-1)
		g.SetRGBA(0, y, color.RGBA{
			R: lerp(top.R, bottom.R, f),
			G: lerp(top.G, bottom.G, f),
			B: lerp(top.B, bottom.B, f),
			A: 255,
		})
	}
	// One column, stretched sideways forever: the felt doesn't change across the table.
	return &stretched{g}
}

type stretched struct{ src *image.RGBA }

func (s *stretched) ColorModel() color.Model { return s.src.ColorModel() }
func (s *stretched) Bounds() image.Rectangle { return image.Rect(0, 0, ogWidth, ogHeight) }
func (s *stretched) At(x, y int) color.Color { return s.src.At(0, y) }

// ---- type -------------------------------------------------------------------

func face(f *opentype.Font, size float64) (font.Face, error) {
	return opentype.NewFace(f, &opentype.FaceOptions{Size: size, DPI: 72, Hinting: font.HintingFull})
}

func centerText(dst *image.RGBA, f font.Face, c color.Color, s string, baseline int) {
	w := font.MeasureString(f, s)
	drawText(dst, f, c, s, (ogWidth-w.Round())/2, baseline)
}

func rightText(dst *image.RGBA, f font.Face, c color.Color, s string, right, baseline int) {
	w := font.MeasureString(f, s)
	drawText(dst, f, c, s, right-w.Round(), baseline)
}

func drawText(dst *image.RGBA, f font.Face, c color.Color, s string, x, baseline int) {
	d := &font.Drawer{
		Dst:  dst,
		Src:  &image.Uniform{c},
		Face: f,
		Dot:  fixed.P(x, baseline),
	}
	d.DrawString(s)
}

// ---- colour -----------------------------------------------------------------

func rgb(r, g, b uint8) color.RGBA { return color.RGBA{r, g, b, 255} }

// alpha returns c at a fraction of its opacity, pre-multiplied, which is what
// image/draw wants.
func alpha(c color.RGBA, f float64) color.RGBA {
	return color.RGBA{
		R: uint8(float64(c.R) * f),
		G: uint8(float64(c.G) * f),
		B: uint8(float64(c.B) * f),
		A: uint8(255 * f),
	}
}

func darken(c color.RGBA, f float64) color.RGBA {
	return color.RGBA{
		R: uint8(float64(c.R) * f),
		G: uint8(float64(c.G) * f),
		B: uint8(float64(c.B) * f),
		A: c.A,
	}
}

func lerp(a, b uint8, f float64) uint8 {
	return uint8(float64(a) + (float64(b)-float64(a))*f)
}
