#!/usr/bin/env python3
"""
One-time render: 2022 ward results baked into still images.

For each ward 1-15, takes the blank touchscreen/PNG/OLD Images/Ward<N>-3way-2022.png
and draws the top three candidates from candidates_2022 (by total_votes)
plus the "Ward N" title and the ballots cast from wards_2022. The top
vote-getter gets the ELECTED bar and checkmark.

Output: touchscreen/PNG/2022WardResults/Ward<N>-3way-2022.png
(the source images are left untouched).

Positions and fonts copy the LIVE overlay in touchscreen/touchscreen/Map.css,
so a 2022 image looks the same as the LIVE page drawn over its image.
Map.css coordinates are in the overlay's space; the ward PNGs sit 1px left
and 2px up of it (#liveResults.ward), hence OFFSET below.

Requires Pillow and PyMySQL:
    python3 generate_2022_ward_images.py
"""

import configparser
import os

import pymysql
from PIL import Image, ImageDraw, ImageFont

ROOT = "/var/www/html/election_municipal_2026/touchscreen"
PNG_DIR = os.path.join(ROOT, "PNG")
FONT_DIR = os.path.join(ROOT, "Fonts")
HEADSHOT_DIR = os.path.join(ROOT, "Headshots", "2022")
BLANK_DIR = os.path.join(PNG_DIR, "OLD Images")		# The original, empty Ward<N>-3way-2022.png
OUT_DIR = os.path.join(PNG_DIR, "2022WardResults")
CONFIG_FILE = os.path.join(ROOT, "Private", "config.ini")

TEXT_COLOR = (0xE8, 0xE8, 0xE8, 255)
OFFSET = (-1, -2)			# Map.css overlay space -> ward PNG pixels
ROW_TOPS = [173, 371, 569]

# Baseline below the top of a CSS line box whose line-height equals font-size
MONTSERRAT_BASELINE = 0.8585

def font(name, size):
	return ImageFont.truetype(os.path.join(FONT_DIR, name), size)

F_TITLE = font("Montserrat-Bold.ttf", 48)
F_SUBTITLE = font("Montserrat-Regular.ttf", 36)
F_PERCENT = font("Montserrat-Bold.ttf", 60)
F_VOTES = font("Montserrat-Regular.ttf", 30)
F_FIRST = font("Montserrat-Regular.ttf", 48)
F_LAST = font("Montserrat-SemiBold.ttf", 48)
F_STAT = font("UniversLTStd-Cn.otf", 30)
F_STAT_BOLD = font("UniversLTStd-BoldCn.otf", 30)

BANNER = Image.open(os.path.join(PNG_DIR, "ElectedGreenBar.png")).convert("RGBA")
CHECK = Image.open(os.path.join(PNG_DIR, "GreenCheckmark.png")).convert("RGBA")


def xy(x, y):
	return (round(x + OFFSET[0]), round(y + OFFSET[1]))


def univers_baseline(top, size, line_height):
	"""Baseline y of a Univers CSS box: top + half-leading + ascent, with the
	ascent/descent overrides from Map.css (93.6% / 25%)."""
	return top + (line_height - 1.186 * size) / 2 + 0.936 * size


def draw_squeezed(img, x, baseline, text, fnt, max_width):
	"""Left/baseline-anchored text, squeezed horizontally past max_width
	(same as fitTextWidth() in Map.js: keeps size and baseline)."""
	draw = ImageDraw.Draw(img)
	width = draw.textlength(text, font=fnt)
	if width <= max_width:
		draw.text(xy(x, baseline), text, font=fnt, fill=TEXT_COLOR, anchor="ls")
		return
	ascent, descent = fnt.getmetrics()
	pad = 4
	layer = Image.new("RGBA", (int(width) + pad * 2, ascent + descent + pad * 2), (0, 0, 0, 0))
	ImageDraw.Draw(layer).text((pad, pad + ascent), text, font=fnt, fill=TEXT_COLOR, anchor="ls")
	new_w = max(1, round(layer.width * max_width / width))
	layer = layer.resize((new_w, layer.height), Image.LANCZOS)
	px, py = xy(x, baseline)
	img.alpha_composite(layer, (px - round(pad * max_width / width), py - pad - ascent))


def paste_headshot(img, row_top, file_name):
	"""Candidate photo in the grey placeholder (774, 148x182), cover-scaled,
	centred horizontally, top kept -- as .liveHeadshot. Skipped if the
	candidate has no headshot file name or the file is missing."""
	file_name = (file_name or "").strip()
	if not file_name:
		return False
	path = os.path.join(HEADSHOT_DIR, file_name)
	if not os.path.isfile(path):
		print("  Headshot not found: %s" % path)
		return False
	photo = Image.open(path).convert("RGBA")
	box_w, box_h = 148, 182
	scale = max(box_w / photo.width, box_h / photo.height)
	photo = photo.resize((round(photo.width * scale), round(photo.height * scale)), Image.LANCZOS)
	left = (photo.width - box_w) // 2
	photo = photo.crop((left, 0, left + box_w, box_h))
	img.alpha_composite(photo, xy(774, row_top))
	return True


def draw_candidate(img, row_top, c, elected):
	draw = ImageDraw.Draw(img)
	paste_headshot(img, row_top, c["headshot"])

	if elected:
		img.alpha_composite(BANNER, xy(922, row_top))
		img.alpha_composite(CHECK, xy(1244, row_top + 58))

	# Names: Montserrat Regular / SemiBold 48; narrower while the checkmark is there
	first_top, last_top = (37, 89) if elected else (40, 92)
	name_width = 280 if elected else 440
	draw_squeezed(img, 949, row_top + first_top + MONTSERRAT_BASELINE * 48, c["first_name"], F_FIRST, name_width)
	draw_squeezed(img, 949, row_top + last_top + MONTSERRAT_BASELINE * 48, c["last_name"], F_LAST, name_width)

	# Percentage and votes, right-aligned at x=1613
	pct_top, votes_top = (54, 126) if elected else (42, 114)
	votes = int(c["total_votes"])
	draw.text(xy(1613, row_top + pct_top + MONTSERRAT_BASELINE * 60),
			  "%.1f%%" % float(c["vote_percentage"]), font=F_PERCENT, fill=TEXT_COLOR, anchor="rs")
	draw.text(xy(1613, row_top + votes_top + MONTSERRAT_BASELINE * 30),
			  "{:,} {}".format(votes, "vote" if votes == 1 else "votes"), font=F_VOTES, fill=TEXT_COLOR, anchor="rs")


def draw_header(img, ward_id, ballots_cast):
	draw = ImageDraw.Draw(img)
	draw.text(xy(770, 28 + MONTSERRAT_BASELINE * 48), "Ward %d" % ward_id, font=F_TITLE, fill=TEXT_COLOR, anchor="ls")
	# Subtitle line of the ward design (Ward-3way-Type Ref.png): Montserrat Regular 36, baseline y=111
	draw.text(xy(770, 111), "Councillor", font=F_SUBTITLE, fill=TEXT_COLOR, anchor="ls")
	if ballots_cast is None:
		return
	# "<b>8,818</b> Ballots Cast"
	x, baseline = 773, univers_baseline(127, 30, 30)
	number = "{:,}".format(int(ballots_cast))
	draw.text(xy(x, baseline), number, font=F_STAT_BOLD, fill=TEXT_COLOR, anchor="ls")
	x += draw.textlength(number, font=F_STAT_BOLD)
	draw.text(xy(x, baseline), " Ballots Cast", font=F_STAT, fill=TEXT_COLOR, anchor="ls")


def main():
	config = configparser.ConfigParser(interpolation=None)
	config.read(CONFIG_FILE)
	db = config["CONNECTION_INFO"]
	conn = pymysql.connect(host=db["database_ip"], user=db["database_user"],
						   password=db["database_password"].strip('"'), database=db["database_name"],
						   charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor)
	with conn.cursor() as cur:
		cur.execute("SELECT ward_id, ballots_cast FROM wards_2022")
		ballots = {r["ward_id"]: r["ballots_cast"] for r in cur.fetchall()}
		cur.execute("SELECT ward_id, candidate_name, first_name, last_name, total_votes, vote_percentage, headshot "
					"FROM candidates_2022 ORDER BY ward_id, total_votes DESC, last_name, first_name")
		rows = cur.fetchall()
	conn.close()

	by_ward = {}
	for r in rows:
		by_ward.setdefault(r["ward_id"], []).append(r)

	os.makedirs(OUT_DIR, exist_ok=True)
	for ward_id in range(1, 16):
		img = Image.open(os.path.join(BLANK_DIR, "Ward%d-3way-2022.png" % ward_id)).convert("RGBA")
		draw_header(img, ward_id, ballots.get(ward_id))
		top3 = by_ward.get(ward_id, [])[:3]
		for i, c in enumerate(top3):
			draw_candidate(img, ROW_TOPS[i], c, elected=(i == 0))
		out = os.path.join(OUT_DIR, "Ward%d-3way-2022.png" % ward_id)
		img.save(out, optimize=True)
		print("%s  %s" % (out, ", ".join("%s %d" % (c["candidate_name"], c["total_votes"]) for c in top3)))


if __name__ == "__main__":
	main()
