<template>
	<div class="roulette-elimination" :class="{ 'roulette-elimination--full-width': fullWidth }">
		<div class="row roulette-main-row">
			<div class="roulette-wheel-col" :class="wheelColumnClass">
				<div class="wheel-stage" role="button" :tabindex="wheelStageTabindex" :aria-disabled="!wheelCanSpin"
					:aria-label="spinning ? 'Roda sedang berputar' : 'Putar roda — klik pada roda'"
					:class="{ 'wheel-stage--can-spin': wheelCanSpin }" @click="spin" @keydown.enter.prevent="spin"
					@keydown.space.prevent="spin">
					<div class="pointer" aria-hidden="true"></div>
					<div class="wheel-wrap" :style="wheelStyle" @transitionend="onSpinEnd">
						<canvas ref="wheelCanvas" class="wheel-canvas"></canvas>
					</div>
					<div v-if="showCenterLogo && centerLogoSrc" class="wheel-center-logo" aria-hidden="true">
						<img :src="centerLogoSrc" alt="" class="wheel-center-logo-img" draggable="false" />
					</div>
				</div>
				<p v-if="wheelCanSpin" class="text-muted roulette-click-hint small">
					Klik pada papan roda untuk putar.
				</p>
				<p v-if="enableSpaceToSpin && showSpaceShortcutHint && remaining.length >= 2 && !spinning"
					class="text-muted roulette-space-hint small">
					Pintasan: tekan <kbd>Space</kbd> untuk putar.
				</p>
				<!-- <p v-if="persistKey && showClearPersistedLink" class="roulette-persist-clear">
					<button type="button" class="btn btn-link btn-xs" :disabled="spinning" @click="clearPersistedState">
						Padam sejarah &amp; mulai semula
					</button>
				</p> -->

				<p v-if="poolSize < 2" class="text-muted hint">
					Tambah sekurang-kurangnya dua nama untuk bermula.
				</p>
				<p v-else-if="remaining.length < 2" class="text-muted hint">
					Kurang daripada dua peserta dalam roda — tidak boleh putar lagi.
				</p>
				<p v-if="lastWinner && !spinning" class="last-winner">
					Terpilih: <strong>{{ lastWinner }}</strong>
				</p>
			</div>

			<div v-if="participantPool.length" class="roulette-side-col" :class="sideColumnClass">
				<div class="panel panel-default roulette-panel text-left">
					<div class="panel-heading">Peserta</div>
					<ul class="list-group participant-list participant-list--scroll">
						<li v-for="p in participantPool" :key="p.id" class="list-group-item participant-row"
							:class="participantRowClass(p)">
							<span class="participant-name">{{ p.label }}</span>
							<span class="participant-badge">{{ participantBadge(p) }}</span>
						</li>
					</ul>
				</div>
				<div class="panel panel-default roulette-panel text-left">
					<div class="panel-heading">Pemenang (urutan cabutan)</div>
					<ol v-if="winnerHistory.length" class="winner-ol winner-ol--scroll">
						<li v-for="(w, i) in winnerHistory" :key="'wh-' + w.id + '-' + i" class="winner-li">
							<span class="winner-li-main">
								<span class="winner-label">{{ w.label }}</span>
								<span class="winner-meta text-muted">Pusingan {{ w.round }}</span>
							</span>
							<button v-if="allowMarkAbsent" type="button" class="btn btn-link btn-xs winner-absent-btn"
								:disabled="spinning" @click.stop="markWinnerAbsentAtHistoryIndex(i)">
								Tidak hadir
							</button>
						</li>
					</ol>
					<div v-else class="panel-body text-muted winner-empty">
						Belum ada cabutan.
					</div>
				</div>
			</div>
		</div>

		<div class="modal fade app-modal roulette-winner-modal" id="roulette-winner-modal" tabindex="-1" role="dialog"
			aria-labelledby="roulette-winner-title" aria-hidden="true">
			<div class="modal-dialog modal-lg roulette-winner-modal__dialog" role="document">
				<div class="modal-content">
					<div class="modal-header">
						<button type="button" class="close" data-dismiss="modal" aria-label="Tutup">
							<span aria-hidden="true">&times;</span>
						</button>
						<h3 class="modal-title roulette-winner-modal__title" id="roulette-winner-title">
							Tahniah
						</h3>
					</div>
					<div class="modal-body text-center" v-if="modalWinnerDisplay">
						<p class="roulette-winner-modal-round text-muted">
							Pusingan {{ modalWinnerDisplay.round }}
						</p>
						<p class="roulette-winner-modal-name">{{ modalWinnerDisplay.label }}</p>
						<!-- <p v-if="allowMarkAbsent" class="roulette-winner-absent-hint text-muted small">
							Jika pemenang tidak hadir, nama dibuang daripada senarai pemenang dan tidak dimasukkan
							semula ke dalam roda.
						</p> -->
					</div>
					<div class="modal-footer clearfix">
						<button v-if="allowMarkAbsent" type="button" class="btn btn-default pull-left"
							:disabled="spinning" @click="onWinnerAbsentFromModal">
							Tidak hadir — keluarkan
						</button>
						<button type="button" class="btn btn-primary pull-right" data-dismiss="modal">
							Tutup
						</button>
					</div>
				</div>
			</div>
		</div>
	</div>
</template>

<script>
/**
 * Roulette wheel: random winner per spin, then that entry is removed (elimination).
 * Vue 2 Options API — drop into a parent or register globally.
 */
export default {
	name: 'RouletteEliminationWheel',

	props: {
		/**
		 * Wheel entries: strings (legacy index id) or { id, label } with stable id.
		 * Prefer objects when the list can grow (imports) without resetting game state.
		 */
		items: {
			type: Array,
			default: function () {
				return ['Ali', 'Siti', 'Muthu', 'Aminah', 'David'];
			},
		},
		/** Minimum full rotations before stopping. */
		minSpins: {
			type: Number,
			default: 6,
		},
		/** Spin duration in ms (CSS transition length). */
		durationMs: {
			type: Number,
			default: 9000,
		},
		/** Press Space to spin (ignored in inputs / when winner modal is open). */
		enableSpaceToSpin: {
			type: Boolean,
			default: true,
		},
		/** Show one-line Space shortcut hint under the wheel. */
		showSpaceShortcutHint: {
			type: Boolean,
			default: true,
		},
		/** Show a modal with the winner after each spin. */
		showWinnerModal: {
			type: Boolean,
			default: true,
		},
		/**
		 * localStorage key for persisting cabutan across refresh (same peserta list).
		 * Set to empty string to disable persistence.
		 */
		persistKey: {
			type: String,
			default: 'voting_roulette_elimination',
		},
		/** Show link to clear saved state and restart from current list. */
		showClearPersistedLink: {
			type: Boolean,
			default: true,
		},
		/**
		 * Allow marking a drawn winner as absent: removed from pemenang and excluded
		 * from the wheel (dikeluarkan daripada acara).
		 */
		allowMarkAbsent: {
			type: Boolean,
			default: true,
		},
		/** Show logo hub in the center (decorative); click anywhere on the wheel to spin. */
		showCenterLogo: {
			type: Boolean,
			default: true,
		},
		/** Public URL for center logo (e.g. `/images/MyKoPKB-logo.png`). Empty hides image. */
		centerLogoSrc: {
			type: String,
			default: '/images/MyKoPKB-logo.png',
		},
		/** When true, stretch to parent width (no 980px cap). */
		fullWidth: {
			type: Boolean,
			default: false,
		},
		/**
		 * URL of a short spin sound (served from `public/`, e.g. `/audio/spin-wheel.mp3`).
		 * Empty string disables audio. Plays on user-triggered spin (click / Space).
		 */
		spinSoundSrc: {
			type: String,
			default: '/audio/spin-wheel.mp3',
		},
		/** Playback volume for `spinSoundSrc` (0–1). */
		spinSoundVolume: {
			type: Number,
			default: 0.85,
		},
		/**
		 * When true, the spin sound loops until the wheel stops; then it is stopped.
		 * Use with a short mechanical/ticking loop; leave false for a one-shot whoosh.
		 */
		spinSoundLoop: {
			type: Boolean,
			default: false,
		},
	},

	data: function () {
		return {
			/** Snapshot of { id, label } for the list UI (fixed order). */
			participantPool: [],
			/** Wheel pool: { id, label }[] */
			remaining: [],
			winnerHistory: [],
			poolSize: 0,
			rotationDeg: 0,
			spinning: false,
			lastWinner: null,
			pendingRemoveIndex: null,
			transitionMs: 9000,
			/** Set when a spin ends; drives winner modal body. */
			modalWinnerDisplay: null,
			/** Participant ids eliminated as "tidak hadir" — not on wheel, not in pemenang. */
			absentParticipantIds: [],
		};
	},

	computed: {
		wheelStyle: function () {
			return {
				transform: 'rotate(' + this.rotationDeg + 'deg)',
				transitionDuration: this.spinning ? this.transitionMs + 'ms' : '0ms',
				transitionTimingFunction: this.spinning
					? 'cubic-bezier(0.12, 0.8, 0.12, 1)'
					: 'linear',
			};
		},
		/** Wider wheel column when `fullWidth` (demo / large layout). */
		wheelColumnClass: function () {
			if (!this.participantPool.length) {
				return 'col-xs-12';
			}
			if (this.fullWidth) {
				return 'col-sm-12 col-md-8';
			}
			return 'col-sm-12 col-md-7';
		},
		sideColumnClass: function () {
			if (!this.participantPool.length) {
				return '';
			}
			if (this.fullWidth) {
				return 'col-sm-12 col-md-4';
			}
			return 'col-sm-12 col-md-5';
		},
		wheelCanSpin: function () {
			return this.remaining.length >= 2 && !this.spinning;
		},
		wheelStageTabindex: function () {
			return this.wheelCanSpin ? 0 : -1;
		},
	},

	watch: {
		items: {
			immediate: true,
			deep: true,
			handler: function (val) {
				if (!this.spinning) {
					this.syncFromItems(val);
					this.$nextTick(this.drawWheel);
				}
			},
		},
		remaining: function () {
			this.$nextTick(this.drawWheel);
		},
		spinSoundSrc: function (val) {
			if (!val) {
				this.disposeSpinSoundAudio();
			} else if (this._spinSoundAudio) {
				this._spinSoundAudio.src = val;
			}
		},
		spinSoundVolume: function (v) {
			if (this._spinSoundAudio) {
				this._spinSoundAudio.volume = this.clampSpinSoundVolume(v);
			}
		},
		spinSoundLoop: function (loop) {
			if (this._spinSoundAudio) {
				this._spinSoundAudio.loop = !!loop;
			}
		},
	},

	mounted: function () {
		window.addEventListener('resize', this.drawWheel);
		window.addEventListener('keydown', this.onDocumentKeydown, true);
		this.$nextTick(this.drawWheel);
	},

	beforeDestroy: function () {
		window.removeEventListener('resize', this.drawWheel);
		window.removeEventListener('keydown', this.onDocumentKeydown, true);
		this.disposeSpinSoundAudio();
	},

	methods: {
		clampSpinSoundVolume: function (v) {
			var n = typeof v === 'number' ? v : parseFloat(v);
			if (n !== n) {
				return 0.85;
			}
			if (n < 0) {
				return 0;
			}
			if (n > 1) {
				return 1;
			}
			return n;
		},

		disposeSpinSoundAudio: function () {
			if (!this._spinSoundAudio) {
				return;
			}
			try {
				this._spinSoundAudio.pause();
				this._spinSoundAudio.src = '';
			} catch (e) {
				/* ignore */
			}
			this._spinSoundAudio = null;
		},

		ensureSpinSoundAudio: function () {
			if (!this.spinSoundSrc || typeof Audio === 'undefined') {
				return null;
			}
			if (!this._spinSoundAudio) {
				this._spinSoundAudio = new Audio();
				this._spinSoundAudio.preload = 'auto';
			}
			this._spinSoundAudio.src = this.spinSoundSrc;
			this._spinSoundAudio.volume = this.clampSpinSoundVolume(this.spinSoundVolume);
			this._spinSoundAudio.loop = !!this.spinSoundLoop;
			return this._spinSoundAudio;
		},

		playSpinSound: function () {
			var a = this.ensureSpinSoundAudio();
			if (!a) {
				return;
			}
			try {
				a.currentTime = 0;
			} catch (e) {
				/* ignore */
			}
			var p = a.play();
			if (p && typeof p.catch === 'function') {
				p.catch(function () {
					/* Autoplay policy or missing file — silent failure. */
				});
			}
		},

		stopSpinSoundAfterSpin: function () {
			if (!this._spinSoundAudio || !this.spinSoundLoop) {
				return;
			}
			try {
				this._spinSoundAudio.loop = false;
				this._spinSoundAudio.pause();
				this._spinSoundAudio.currentTime = 0;
			} catch (e) {
				/* ignore */
			}
		},

		isWinnerModalOpen: function () {
			if (typeof window.$ === 'undefined') {
				return false;
			}
			try {
				return window.$('#roulette-winner-modal').hasClass('in');
			} catch (e) {
				return false;
			}
		},

		onDocumentKeydown: function (e) {
			if (!this.enableSpaceToSpin) {
				return;
			}
			if (e.key !== ' ' && e.key !== 'Spacebar' && e.code !== 'Space') {
				return;
			}
			if (e.repeat) {
				return;
			}
			var el = e.target;
			if (el) {
				var tag = el.tagName && el.tagName.toLowerCase();
				if (
					tag === 'input' ||
					tag === 'textarea' ||
					tag === 'select' ||
					(el.isContentEditable === true)
				) {
					return;
				}
			}
			if (this.isWinnerModalOpen()) {
				return;
			}
			if (this.spinning || this.remaining.length < 2) {
				return;
			}
			e.preventDefault();
			this.spin();
		},

		normalizeId: function (x) {
			if (x === null || x === undefined) {
				return '';
			}
			return String(x);
		},

		/**
		 * @returns {{ id: string, label: string }[]}
		 */
		normalizeItems: function (val) {
			if (!val || !val.length) {
				return [];
			}
			var out = [];
			var seen = {};
			for (var i = 0; i < val.length; i++) {
				var row = val[i];
				var id;
				var label;
				if (row != null && typeof row === 'object' && !Array.isArray(row)) {
					id =
						row.id !== undefined && row.id !== null
							? this.normalizeId(row.id)
							: 'legacy:' + i;
					label = String(row.label != null ? row.label : '');
				} else {
					id = 'legacy:' + i;
					label = String(row != null ? row : '');
				}
				if (!label.trim()) {
					label = id.indexOf('legacy:') === 0 ? id : label;
				}
				if (seen[id]) {
					continue;
				}
				seen[id] = true;
				out.push({ id: id, label: label });
			}
			return out;
		},

		/**
		 * Merge persisted v2 game state with current items (same ids + new ids).
		 * Winners / absent kept; new ids join the wheel only.
		 */
		applyMergeFromPersisted: function (norm, persisted) {
			if (!persisted || persisted.v !== 2) {
				return false;
			}
			var normList = [];
			var normSet = {};
			var labelById = {};
			for (var i = 0; i < norm.length; i++) {
				var nid = this.normalizeId(norm[i].id);
				var lab = norm[i].label;
				normList.push({ id: nid, label: lab });
				normSet[nid] = true;
				labelById[nid] = lab;
			}

			var whRaw = persisted.winnerHistory || [];
			var winnerHistory = [];
			var winnerSeen = {};
			for (var w = 0; w < whRaw.length; w++) {
				var wr = whRaw[w];
				var wid = this.normalizeId(wr.id != null ? wr.id : wr.idx);
				if (!normSet[wid]) {
					continue;
				}
				if (winnerSeen[wid]) {
					return false;
				}
				winnerSeen[wid] = true;
				winnerHistory.push({
					id: wid,
					round: typeof wr.round === 'number' ? wr.round : winnerHistory.length + 1,
					label:
						labelById[wid] != null
							? labelById[wid]
							: String(wr.label != null ? wr.label : ''),
				});
			}

			var absentRaw = persisted.absentParticipantIds || persisted.absentParticipantIdxs || [];
			var absentParticipantIds = [];
			var absSeen = {};
			for (var a = 0; a < absentRaw.length; a++) {
				var aid = this.normalizeId(absentRaw[a]);
				if (!normSet[aid]) {
					continue;
				}
				if (absSeen[aid] || winnerSeen[aid]) {
					return false;
				}
				absSeen[aid] = true;
				absentParticipantIds.push(aid);
			}
			absentParticipantIds.sort();

			for (var h = 0; h < winnerHistory.length; h++) {
				winnerHistory[h].round = h + 1;
			}

			var remaining = [];
			for (var r = 0; r < normList.length; r++) {
				var rid = normList[r].id;
				if (absSeen[rid]) {
					continue;
				}
				if (winnerSeen[rid]) {
					continue;
				}
				remaining.push({ id: rid, label: normList[r].label });
			}

			this.participantPool = normList.slice();
			this.remaining = remaining;
			this.winnerHistory = winnerHistory;
			this.absentParticipantIds = absentParticipantIds;
			this.poolSize = normList.length;
			this.updateLastWinnerFromHistory();
			this.rotationDeg = 0;
			this.pendingRemoveIndex = null;
			this.modalWinnerDisplay = null;
			return true;
		},

		persistState: function () {
			if (!this.persistKey || typeof localStorage === 'undefined') {
				return;
			}
			var norm = this.normalizeItems(this.items);
			if (!norm.length) {
				return;
			}
			try {
				localStorage.setItem(
					this.persistKey,
					JSON.stringify({
						v: 2,
						winnerHistory: this.winnerHistory,
						remaining: this.remaining,
						absentParticipantIds: this.absentParticipantIds,
					})
				);
			} catch (e) {
				// quota / private mode
			}
		},

		clearPersistedState: function () {
			if (this.persistKey && typeof localStorage !== 'undefined') {
				try {
					localStorage.removeItem(this.persistKey);
				} catch (e) { }
			}
			if (typeof window.$ !== 'undefined') {
				window.$('#roulette-winner-modal').modal('hide');
			}
			this.initFreshFromNormalized(
				this.normalizeItems(this.items && this.items.length ? this.items : [])
			);
			this.$nextTick(this.drawWheel);
		},

		initFreshFromNormalized: function (norm) {
			this.participantPool = [];
			this.remaining = [];
			this.winnerHistory = [];
			this.absentParticipantIds = [];
			this.lastWinner = null;
			this.rotationDeg = 0;
			this.pendingRemoveIndex = null;
			this.modalWinnerDisplay = null;
			if (!norm || !norm.length) {
				this.poolSize = 0;
				return;
			}
			for (var i = 0; i < norm.length; i++) {
				var e = { id: norm[i].id, label: norm[i].label };
				this.participantPool.push(e);
				this.remaining.push({ id: e.id, label: e.label });
			}
			this.poolSize = this.remaining.length;
		},

		updateLastWinnerFromHistory: function () {
			if (this.winnerHistory.length) {
				this.lastWinner =
					this.winnerHistory[this.winnerHistory.length - 1].label;
			} else {
				this.lastWinner = null;
			}
		},

		syncFromItems: function (val) {
			this.spinning = false;
			this.pendingRemoveIndex = null;
			this.modalWinnerDisplay = null;
			var norm = this.normalizeItems(val);
			if (!norm.length) {
				this.participantPool = [];
				this.remaining = [];
				this.winnerHistory = [];
				this.absentParticipantIds = [];
				this.poolSize = 0;
				this.lastWinner = null;
				this.rotationDeg = 0;
				if (this.persistKey && typeof localStorage !== 'undefined') {
					try {
						localStorage.removeItem(this.persistKey);
					} catch (e) { }
				}
				return;
			}

			var raw;
			var data = null;
			if (this.persistKey && typeof localStorage !== 'undefined') {
				try {
					raw = localStorage.getItem(this.persistKey);
					if (raw) {
						data = JSON.parse(raw);
					}
				} catch (e) {
					data = null;
				}
			}

			if (data && data.v === 2 && this.applyMergeFromPersisted(norm, data)) {
				this.persistState();
				return;
			}

			if (this.persistKey && typeof localStorage !== 'undefined') {
				try {
					localStorage.removeItem(this.persistKey);
				} catch (e2) { }
			}
			this.initFreshFromNormalized(norm);
			this.persistState();
		},

		isStillInPool: function (id) {
			var sid = this.normalizeId(id);
			for (var i = 0; i < this.remaining.length; i++) {
				if (this.remaining[i].id === sid) {
					return true;
				}
			}
			return false;
		},

		winRoundForParticipant: function (id) {
			var sid = this.normalizeId(id);
			for (var i = 0; i < this.winnerHistory.length; i++) {
				if (this.winnerHistory[i].id === sid) {
					return this.winnerHistory[i].round;
				}
			}
			return null;
		},

		isAbsentParticipant: function (id) {
			var sid = this.normalizeId(id);
			return this.absentParticipantIds.indexOf(sid) >= 0;
		},

		participantRowClass: function (p) {
			if (this.isStillInPool(p.id)) {
				return 'participant-row--active';
			}
			if (this.isAbsentParticipant(p.id)) {
				return 'participant-row--absent';
			}
			if (this.winRoundForParticipant(p.id) !== null) {
				return 'participant-row--won';
			}
			return '';
		},

		participantBadge: function (p) {
			if (this.isStillInPool(p.id)) {
				return 'Dalam roda';
			}
			if (this.isAbsentParticipant(p.id)) {
				return 'Tidak hadir (dikeluarkan)';
			}
			var r = this.winRoundForParticipant(p.id);
			if (r !== null) {
				return 'Pemenang pusingan ' + r;
			}
			return '—';
		},

		spin: function () {
			var n = this.remaining.length;
			if (n < 2 || this.spinning) {
				return;
			}

			var winIndex = Math.floor(Math.random() * n);
			this.pendingRemoveIndex = winIndex;

			var sliceDeg = 360 / n;
			// Canvas: segment i is centered at (winIndex + 0.5) * sliceDeg clockwise from top.
			// CSS rotate(R) turns the wheel CW. The rim point now at the pointer came from
			// clockwise-from-top angle T where T + R ≡ 360 (mod 360), so T ≡ 360 - R.
			// To land segment winIndex under the arrow: T = (winIndex + 0.5) * sliceDeg, hence
			// R mod 360 === 360 - T (not T).
			var segmentCenterCW = winIndex * sliceDeg + sliceDeg / 2;
			var targetMod = (360 - segmentCenterCW + 360) % 360;

			var currentNorm = ((this.rotationDeg % 360) + 360) % 360;
			var needed = targetMod - currentNorm;
			if (needed < 0) {
				needed += 360;
			}

			this.transitionMs = this.durationMs;
			this.spinning = true;
			this.lastWinner = null;

			this.playSpinSound();

			var extra = 360 * this.minSpins;
			this.rotationDeg = this.rotationDeg + extra + needed;
		},

		onSpinEnd: function (e) {
			if (!this.spinning || e.propertyName !== 'transform') {
				return;
			}
			this.stopSpinSoundAfterSpin();
			this.spinning = false;

			if (this.pendingRemoveIndex === null) {
				return;
			}

			var entry = this.remaining[this.pendingRemoveIndex];
			var name = entry.label;
			this.lastWinner = name;
			var round = this.winnerHistory.length + 1;
			this.winnerHistory.push({
				id: entry.id,
				label: entry.label,
				round: round,
			});
			this.remaining.splice(this.pendingRemoveIndex, 1);
			this.pendingRemoveIndex = null;

			var remainingLabels = this.remaining.map(function (x) {
				return x.label;
			});
			var pid = entry.id;
			var idxNum = parseInt(pid, 10);
			this.$emit('winner', {
				name: name,
				participantId: pid,
				participantIndex: idxNum === idxNum ? idxNum : pid,
				round: round,
				remaining: remainingLabels,
			});
			this.$emit('eliminate', {
				name: name,
				participantId: pid,
				participantIndex: idxNum === idxNum ? idxNum : pid,
				round: round,
				remaining: remainingLabels,
			});

			if (this.showWinnerModal) {
				this.modalWinnerDisplay = {
					label: name,
					round: round,
					historyIndex: this.winnerHistory.length - 1,
				};
				var vm = this;
				this.$nextTick(function () {
					vm.openWinnerModal();
				});
			}

			this.persistState();
		},

		hideWinnerModal: function () {
			if (typeof window.$ !== 'undefined') {
				window.$('#roulette-winner-modal').modal('hide');
			}
		},

		onWinnerAbsentFromModal: function () {
			if (
				this.spinning ||
				!this.modalWinnerDisplay ||
				this.modalWinnerDisplay.historyIndex === undefined
			) {
				return;
			}
			var hi = this.modalWinnerDisplay.historyIndex;
			this.hideWinnerModal();
			var vm = this;
			this.$nextTick(function () {
				vm.markWinnerAbsentAtHistoryIndex(hi);
			});
		},

		/**
		 * Pemenang tidak hadir: buang daripada senarai pemenang; tidak dimasukkan semula ke roda.
		 */
		markWinnerAbsentAtHistoryIndex: function (historyIndex) {
			if (this.spinning) {
				return;
			}
			if (historyIndex < 0 || historyIndex >= this.winnerHistory.length) {
				return;
			}
			this.hideWinnerModal();
			var w = this.winnerHistory[historyIndex];
			if (!w) {
				return;
			}
			if (this.isAbsentParticipant(w.id)) {
				return;
			}

			var roundRemoved = w.round;
			this.winnerHistory.splice(historyIndex, 1);
			for (var r = 0; r < this.winnerHistory.length; r++) {
				this.winnerHistory[r].round = r + 1;
			}

			var wid = this.normalizeId(w.id);
			var label = w.label;
			for (var pi = 0; pi < this.participantPool.length; pi++) {
				if (this.participantPool[pi].id === wid) {
					label = this.participantPool[pi].label;
					break;
				}
			}
			if (this.absentParticipantIds.indexOf(wid) < 0) {
				this.absentParticipantIds.push(wid);
				this.absentParticipantIds.sort();
			}

			this.updateLastWinnerFromHistory();
			this.rotationDeg = 0;
			this.modalWinnerDisplay = null;

			var remainingLabels = this.remaining.map(function (x) {
				return x.label;
			});
			var widx = parseInt(wid, 10);
			this.$emit('winner-absent', {
				name: label,
				participantId: wid,
				participantIndex: widx === widx ? widx : wid,
				roundRemoved: roundRemoved,
				remaining: remainingLabels,
				eliminated: true,
			});

			this.persistState();
			this.$nextTick(this.drawWheel);
		},

		openWinnerModal: function () {
			if (typeof window.$ === 'undefined') {
				return;
			}
			if (this.util && typeof this.util.showModal === 'function') {
				this.util.showModal('#roulette-winner-modal');
			} else {
				window.$('#roulette-winner-modal').modal('show');
			}
		},

		drawWheel: function () {
			var canvas = this.$refs.wheelCanvas;
			if (!canvas) {
				return;
			}
			var wrap = canvas.parentElement;
			var maxPx = this.fullWidth ? 880 : 600;
			var size = Math.min(wrap.clientWidth || 320, maxPx);
			var dpr = window.devicePixelRatio || 1;
			canvas.width = size * dpr;
			canvas.height = size * dpr;
			canvas.style.width = size + 'px';
			canvas.style.height = size + 'px';

			var ctx = canvas.getContext('2d');
			ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

			var cx = size / 2;
			var cy = size / 2;
			var r = size / 2 - 4;

			ctx.clearRect(0, 0, size, size);

			var labels = this.remaining.map(function (x) {
				return x.label;
			});
			var n = labels.length;
			if (n === 0) {
				ctx.beginPath();
				ctx.arc(cx, cy, r, 0, Math.PI * 2);
				ctx.fillStyle = '#e8e8e8';
				ctx.fill();
				return;
			}

			var slice = (Math.PI * 2) / n;
			var palette = [
				'#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6',
				'#1abc9c', '#e67e22', '#34495e',
			];

			for (var i = 0; i < n; i++) {
				var start = -Math.PI / 2 + i * slice;
				ctx.beginPath();
				ctx.moveTo(cx, cy);
				ctx.arc(cx, cy, r, start, start + slice);
				ctx.closePath();
				ctx.fillStyle = palette[i % palette.length];
				ctx.fill();
				ctx.strokeStyle = '#fff';
				ctx.lineWidth = 2;
				ctx.stroke();

				var mid = start + slice / 2;
				ctx.save();
				ctx.translate(cx, cy);
				ctx.rotate(mid);
				ctx.fillStyle = '#fff';
				ctx.font = 'bold ' + Math.max(11, Math.floor(size / 28)) + 'px sans-serif';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'middle';
				var text = String(labels[i]);
				if (text.length > 14) {
					text = text.slice(0, 12) + '…';
				}
				ctx.fillText(text, r * 0.62, 0);
				ctx.restore();
			}

			ctx.beginPath();
			ctx.arc(cx, cy, 16, 0, Math.PI * 2);
			ctx.fillStyle = '#fff';
			ctx.fill();
			ctx.strokeStyle = '#333';
			ctx.lineWidth = 2;
			ctx.stroke();
		},
	},
};
</script>

<style scoped>
.roulette-elimination {
	max-width: 980px;
	margin: 0 auto;
	text-align: center;
}

.roulette-elimination--full-width {
	max-width: none;
	width: 100%;
	margin-left: 0;
	margin-right: 0;
}

.roulette-elimination--full-width .wheel-stage {
	max-width: min(100%, 880px);
}

.roulette-main-row {
	align-items: flex-start;
}

.roulette-wheel-col {
	text-align: center;
}

.roulette-side-col {
	text-align: left;
	margin-top: 1.25rem;
}

@media (min-width: 992px) {
	.roulette-side-col {
		margin-top: 0;
		padding-left: 8px;
	}
}

.wheel-stage {
	position: relative;
	width: 100%;
	max-width: 600px;
	margin: 0 auto 1rem;
	outline: none;
	user-select: none;
}

.wheel-stage--can-spin {
	cursor: pointer;
}

.wheel-stage--can-spin:focus {
	box-shadow: 0 0 0 3px rgba(51, 122, 183, 0.45);
	border-radius: 4px;
}

.wheel-stage:not(.wheel-stage--can-spin) {
	cursor: not-allowed;
}

.pointer {
	position: absolute;
	top: -8px;
	left: 50%;
	transform: translateX(-50%);
	width: 0;
	height: 0;
	border-left: 17px solid transparent;
	border-right: 17px solid transparent;
	border-top: 26px solid #222;
	z-index: 2;
	filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.25));
}

.wheel-wrap {
	position: relative;
	z-index: 1;
	width: 100%;
	border-radius: 50%;
	box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
	transition-property: transform;
	will-change: transform;
}

.wheel-center-logo {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	z-index: 3;
	width: 22%;
	min-width: 72px;
	max-width: 150px;
	padding: 6px;
	border: 3px solid rgba(255, 255, 255, 0.95);
	border-radius: 50%;
	background: #fff;
	box-shadow: 0 4px 18px rgba(0, 0, 0, 0.22);
	pointer-events: none;
	display: flex;
	align-items: center;
	justify-content: center;
	transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}

.wheel-stage--can-spin:hover .wheel-center-logo {
	transform: translate(-50%, -50%) scale(1.06);
	box-shadow: 0 6px 22px rgba(0, 0, 0, 0.28);
}

.wheel-stage--can-spin:active .wheel-center-logo {
	transform: translate(-50%, -50%) scale(0.97);
}

.wheel-stage:not(.wheel-stage--can-spin) .wheel-center-logo {
	opacity: 0.5;
}

.wheel-center-logo-img {
	width: 100%;
	height: auto;
	max-height: 100%;
	object-fit: contain;
	border-radius: 50%;
	pointer-events: none;
	user-select: none;
}

.wheel-canvas {
	display: block;
	width: 100%;
	height: auto;
	border-radius: 50%;
}

.roulette-click-hint {
	margin-top: 0.35rem;
	margin-bottom: 0;
}

.roulette-space-hint {
	margin-top: 0.25rem;
	margin-bottom: 0;
}

.roulette-space-hint kbd {
	display: inline-block;
	padding: 1px 6px;
	font-size: 11px;
	line-height: 1.4;
	color: #333;
	background-color: #f7f7f7;
	border: 1px solid #ccc;
	border-radius: 3px;
	box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08);
}

.roulette-persist-clear {
	margin-top: 0.35rem;
	margin-bottom: 0;
}

.hint {
	font-size: 0.9rem;
	margin-top: 0.5rem;
}

.last-winner {
	margin-top: 0.5rem;
}

.remaining-count {
	font-size: 20px;
	color: #666;
	margin-top: 0.25rem;
}

.roulette-side-col .roulette-panel {
	margin-bottom: 15px;
}

.roulette-side-col .roulette-panel:last-child {
	margin-bottom: 0;
}

.participant-list--scroll {
	max-height: min(42vh, 340px);
	overflow-y: auto;
}

.winner-ol--scroll {
	max-height: min(28vh, 220px);
	overflow-y: auto;
}

.roulette-panel .panel-heading {
	font-size: 14px;
	font-weight: 600;
}

.participant-list {
	margin-bottom: 0;
}

.participant-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 0.5rem;
	font-size: 13px;
}

.participant-row--active {
	background-color: #f0fdf4;
	border-left: 3px solid #22c55e;
}

.participant-row--won {
	color: #6b7280;
	background-color: #f9fafb;
}

.participant-row--absent {
	color: #92400e;
	background-color: #fffbeb;
	border-left: 3px solid #f59e0b;
}

.participant-name {
	flex: 1;
	min-width: 0;
	word-break: break-word;
}

.participant-badge {
	font-size: 11px;
	white-space: nowrap;
	color: #6b7280;
}

.participant-row--active .participant-badge {
	color: #15803d;
	font-weight: 600;
}

.winner-ol {
	margin: 0;
	padding: 10px 15px 10px 28px;
	font-size: 13px;
}

.winner-li {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 6px;
	margin-bottom: 8px;
}

.winner-li-main {
	flex: 1;
	min-width: 0;
}

.winner-absent-btn {
	white-space: nowrap;
	padding: 0 4px;
	flex-shrink: 0;
}

.winner-label {
	font-weight: 600;
}

.winner-meta {
	margin-left: 8px;
	font-size: 12px;
}

.winner-empty {
	margin: 0;
	font-size: 13px;
}

/* Hall / projection: wide dialog, very large type */
.roulette-winner-modal__dialog {
	width: 96%;
	max-width: min(96vw, 1320px);
	margin: 12px auto;
}

@media (min-width: 768px) {
	.roulette-winner-modal__dialog {
		width: 94%;
		max-width: min(94vw, 1400px);
	}
}

@media (min-width: 1200px) {
	.roulette-winner-modal__dialog {
		max-width: 1440px;
	}
}

.roulette-winner-modal .modal-content {
	border-radius: 16px;
	overflow: hidden;
	border-width: 2px;
}

.roulette-winner-modal .modal-header {
	padding: clamp(20px, 3vw, 36px) clamp(18px, 3vw, 40px);
	position: relative;
}

.roulette-winner-modal .modal-header .close {
	font-size: clamp(2rem, 4vw, 3.25rem);
	font-weight: 300;
	line-height: 1;
	padding: 8px 12px;
	margin-top: -4px;
	opacity: 0.55;
}

.roulette-winner-modal .modal-body {
	padding: clamp(32px, 5vw, 72px) clamp(20px, 4vw, 48px);
}

.roulette-winner-modal .modal-footer {
	padding: clamp(16px, 2.5vw, 28px) clamp(18px, 3vw, 40px);
}

.roulette-winner-modal .modal-footer .btn {
	font-size: clamp(1.15rem, 2.2vw, 1.65rem);
	font-weight: 700;
	padding: clamp(10px, 1.5vw, 16px) clamp(18px, 2.5vw, 28px);
}

.roulette-winner-modal__title {
	font-weight: 800;
	font-size: clamp(2.25rem, 5.5vw, 5rem);
	line-height: 1.15;
	margin: 0;
	padding-right: 2.5rem;
}

.roulette-winner-modal-round {
	font-size: clamp(1.5rem, 3.5vw, 3.25rem);
	margin-bottom: clamp(16px, 2.5vw, 28px);
	font-weight: 700;
	/* Stronger than .text-muted for projection / rear seats */
	color: #4b5563 !important;
}

.roulette-winner-modal-name {
	font-size: clamp(3rem, 12vw, 8.5rem);
	font-weight: 800;
	line-height: 1.12;
	margin: 0;
	word-break: break-word;
	letter-spacing: -0.03em;
	text-shadow: 0 2px 0 rgba(0, 0, 0, 0.06);
}

.roulette-winner-absent-hint {
	margin-top: 12px;
}
</style>
