// ==UserScript==
// @name ChatGPT Realtime Model Switcher: 4o-mini, o4-mini, o3 and more!
// @namespace http://tampermonkey.net/
// @version 3.33.33
// @description Allowing you to switch models during a single conversation, and highlight responses by color based on the model generating them
// @match *://chatgpt.com/*
// @author sickn33 (UI enhancements by Athena) (https://greasyfork.org/en/users/99408-d0gkiller87) (https://greasyfork.org/en/scripts/514276-chatgpt-realtime-model-switcher-4o-mini-o4-mini-o3-and-more)
// @license MIT
// @grant unsafeWindow
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM_registerMenuCommand
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @run-at document-idle
// @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @downloadURL https://update.greasyfork.org/scripts/514276/ChatGPT%20Realtime%20Model%20Switcher%3A%204o-mini%2C%20o4-mini%2C%20o3%20and%20more%21.user.js
// @updateURL https://update.greasyfork.org/scripts/514276/ChatGPT%20Realtime%20Model%20Switcher%3A%204o-mini%2C%20o4-mini%2C%20o3%20and%20more%21.meta.js
// ==/UserScript==


(async function() {
'use strict';

function injectStyle( style, isDisabled = false ) {
const styleNode = document.createElement( 'style' );
styleNode.type = 'text/css';
styleNode.textContent = style;
document.head.appendChild( styleNode );
styleNode.disabled = isDisabled;
return styleNode;
}

const PlanType = Object.freeze({
free: 0,
plus: 1,
pro : 2
});

class ModelSwitcher {
constructor() {
// Cache per performance
this.cache = new Map();
this.throttledCallbacks = new Map();
this.debouncedCallbacks = new Map();

// Pool di eventi per ridurre memory allocation
this.eventPool = [];
this.maxPoolSize = 10;
}

// Utility per throttling - limita frequenza esecuzione
throttle(func, limit, key) {
if (this.throttledCallbacks.has(key)) {
return this.throttledCallbacks.get(key);
}

let inThrottle;
const throttledFunc = (...args) => {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};

this.throttledCallbacks.set(key, throttledFunc);
return throttledFunc;
}

// Utility per debouncing - ritarda esecuzione fino a pausa
debounce(func, delay, key) {
if (this.debouncedCallbacks.has(key)) {
return this.debouncedCallbacks.get(key);
}

let timeoutId;
const debouncedFunc = (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};

this.debouncedCallbacks.set(key, debouncedFunc);
return debouncedFunc;
}

// Cache intelligente con TTL
setCache(key, value, ttl = 300000) { // 5 minuti default
this.cache.set(key, {
value,
timestamp: Date.now(),
ttl
});
}

getCache(key) {
const cached = this.cache.get(key);
if (!cached) return null;

if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}

return cached.value;
}

// Cleanup cache periodico
startCacheCleanup() {
const cleanup = () => {
const now = Date.now();
for (const [key, data] of this.cache.entries()) {
if (now - data.timestamp > data.ttl) {
this.cache.delete(key);
}
}
};

// Cleanup ogni 5 minuti
setInterval(cleanup, 300000);
}

// Pool di oggetti event per ridurre GC
getEventFromPool() {
return this.eventPool.length > 0 ? this.eventPool.pop() : {};
}

returnEventToPool(event) {
if (this.eventPool.length < this.maxPoolSize) {
// Reset object
Object.keys(event).forEach(key => delete event[key]);
this.eventPool.push(event);
}
}

getPlanType() {
// Check cache first
const cached = this.getCache('planType');
if (cached) return cached;

// Ottimizzazione: cerca solo nei script con contenuto rilevante
const scripts = document.querySelectorAll('script');
const planTypeRegex = /\\"planType\\"\s*,\s*\\"(\w+?)\\"/ ;

for (let i = 0; i < scripts.length; i++) {
const scriptContent = scripts[i].innerHTML;

// Skip script vuoti o troppo piccoli
if (scriptContent.length < 100) continue;

// Cerca solo se contiene "planType"
if (!scriptContent.includes('planType')) continue;

const match = planTypeRegex.exec(scriptContent);
if (match && match[1]) {
const planType = match[1];
// Cache per 10 minuti
this.setCache('planType', planType, 600000);
return planType;
}
}

// Fallback con cache
this.setCache('planType', 'free', 600000);
return 'free';
}

async init() {
this.model = await GM.getValue( 'model', 'auto' );
this.buttons = {};
this.offsetX = 0;
this.offsetY = 0;
this.isDragging = false;
this.shouldCancelClick = false;
this.modelSelector = null;
this.isMenuVisible = await GM.getValue( 'isMenuVisible', true );
this.isMenuVisibleCommandId = null;
this.modelHighlightStyleNode = null;
this.isModelHighlightEnabled = await GM.getValue( 'isModelHighlightEnabled', true );
this.isModelHighlightEnabledCommandId = null;
this.isMenuVertical = await GM.getValue( 'isMenuVertical', true );
this.isMenuVerticalCommandId = null;
this.conversationUrlRegex = new RegExp( /https:\/\/chatgpt\.com\/backend-api\/.*conversation/ );

const planType = PlanType[ this.getPlanType() ];

const models = [
[ PlanType.pro, "o1-pro", "o1-pro" ],
[ PlanType.plus, "o3", "o3" ],
[ PlanType.free, "o4-mini", "o4-mini" ],
[ PlanType.plus, "o4-mini-high", "o4-mini-high" ],
[ PlanType.free, "gpt-3.5", "gpt-3-5" ],
[ PlanType.free, "4o-mini", "gpt-4o-mini" ],
[ PlanType.free, "4.1-mini", "gpt-4-1-mini" ],
[ PlanType.free, "gpt-4o", "gpt-4o" ],
[ PlanType.plus, "gpt-4.1", "gpt-4-1" ],
[ PlanType.plus, "gpt-4.5", "gpt-4-5" ],
[ PlanType.free, "default", "auto" ],
];

this.availableModels = {};
for ( const [ minimumPlan, modelName, modelValue ] of models ) {
if ( planType >= minimumPlan ) {
this.availableModels[modelName] = modelValue;
}
}
}

hookFetch() {
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async ( resource, config = {} ) => {
if (
typeof resource === 'string' &&
resource.match( this.conversationUrlRegex ) &&
config.method === 'POST' &&
config.headers &&
config.headers['Content-Type'] === 'application/json' &&
config.body &&
this.model !== 'auto'
) {
const body = JSON.parse( config.body );
body.model = this.model;
config.body = JSON.stringify( body );
}
return originalFetch( resource, config );
};
}

injectToggleButtonStyle() {
// UI completamente ridisegnata per allinearsi perfettamente a ChatGPT
let style = `
:root {
/* Palette colori ChatGPT ufficiale */
--chatgpt-text-primary: #2d333a;
--chatgpt-text-secondary: #6b7280;
--chatgpt-text-tertiary: #9ca3af;
--chatgpt-surface-primary: #ffffff;
--chatgpt-surface-secondary: #f7f7f8;
--chatgpt-surface-tertiary: #ececf1;
--chatgpt-border-light: #d1d5db;
--chatgpt-border-medium: #9ca3af;
--chatgpt-accent-blue: #10a37f;
--chatgpt-accent-hover: #0d8a6b;
--chatgpt-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--chatgpt-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--chatgpt-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);

/* Dark mode - ChatGPT style */
--chatgpt-dark-text-primary: #ececf1;
--chatgpt-dark-text-secondary: #c5c5d2;
--chatgpt-dark-text-tertiary: #8e8ea0;
--chatgpt-dark-surface-primary: #212121;
--chatgpt-dark-surface-secondary: #2f2f2f;
--chatgpt-dark-surface-tertiary: #424242;
--chatgpt-dark-border-light: #4d4d4f;
--chatgpt-dark-border-medium: #565869;
--chatgpt-dark-accent-blue: #19c37d;
--chatgpt-dark-accent-hover: #15a670;
}

@media (prefers-color-scheme: dark) {
:root {
--chatgpt-text-primary: var(--chatgpt-dark-text-primary);
--chatgpt-text-secondary: var(--chatgpt-dark-text-secondary);
--chatgpt-text-tertiary: var(--chatgpt-dark-text-tertiary);
--chatgpt-surface-primary: var(--chatgpt-dark-surface-primary);
--chatgpt-surface-secondary: var(--chatgpt-dark-surface-secondary);
--chatgpt-surface-tertiary: var(--chatgpt-dark-surface-tertiary);
--chatgpt-border-light: var(--chatgpt-dark-border-light);
--chatgpt-border-medium: var(--chatgpt-dark-border-medium);
--chatgpt-accent-blue: var(--chatgpt-dark-accent-blue);
--chatgpt-accent-hover: var(--chatgpt-dark-accent-hover);
}
}

#model-selector {
position: absolute;
display: flex;
flex-direction: column;
gap: 4px;
cursor: grab;
background: var(--chatgpt-surface-primary);
border: 1px solid var(--chatgpt-border-light);
border-radius: 12px;
padding: 8px;
box-shadow: var(--chatgpt-shadow-lg);
font-family: 'Söhne', 'ui-sans-serif', 'system-ui', '-apple-system', 'Segoe UI', sans-serif;
z-index: 10000;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
min-width: 120px;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

#model-selector:hover {
box-shadow: var(--chatgpt-shadow-lg), 0 0 0 1px var(--chatgpt-border-medium);
}

#model-selector.horizontal {
flex-direction: row;
flex-wrap: wrap;
}

#model-selector.hidden {
opacity: 0;
pointer-events: none;
transform: scale(0.95);
}

#model-selector:active {
cursor: grabbing;
}

/* Stile bottoni - identico a ChatGPT */
#model-selector button {
background: var(--chatgpt-surface-secondary);
border: 1px solid var(--chatgpt-border-light);
color: var(--chatgpt-text-primary);
padding: 8px 12px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
line-height: 1.25;
user-select: none;
border-radius: 8px;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 60px;
text-align: center;
position: relative;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-family: inherit;
}

#model-selector button:hover {
background: var(--chatgpt-surface-tertiary);
border-color: var(--chatgpt-border-medium);
transform: translateY(-1px);
box-shadow: var(--chatgpt-shadow-sm);
}

#model-selector button:active {
transform: translateY(0);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}

#model-selector button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.1);
border-color: var(--chatgpt-accent-blue);
}

/* Stato selezionato - stile ChatGPT */
#model-selector button.selected {
background: var(--chatgpt-accent-blue);
border-color: var(--chatgpt-accent-blue);
color: white;
font-weight: 600;
box-shadow: var(--chatgpt-shadow-md), inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
position: relative;
}

#model-selector button.selected::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 100%);
border-radius: inherit;
pointer-events: none;
}

#model-selector button.selected:hover {
background: var(--chatgpt-accent-hover);
border-color: var(--chatgpt-accent-hover);
transform: translateY(-1px);
box-shadow: var(--chatgpt-shadow-lg), inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
}

#model-selector button.selected:active {
transform: translateY(0);
box-shadow: var(--chatgpt-shadow-md), inset 0 2px 4px 0 rgba(0, 0, 0, 0.2);
}

/* Indicatori modello colorati - sottili come ChatGPT */
#model-selector button.selected::after {
content: '';
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}

/* Animazioni smooth */
@keyframes modelSelectorAppear {
from {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}

#model-selector:not(.hidden) {
animation: modelSelectorAppear 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Responsive design */
@media (max-width: 768px) {
#model-selector {
padding: 6px;
gap: 3px;
border-radius: 10px;
}

#model-selector button {
padding: 6px 10px;
font-size: 12px;
min-width: 50px;
}
}

/* Accessibility */
@media (prefers-reduced-motion: reduce) {
#model-selector,
#model-selector button {
transition: none;
}

#model-selector:not(.hidden) {
animation: none;
}
}

/* Focus ring per navigazione keyboard */
#model-selector:focus-within {
box-shadow: var(--chatgpt-shadow-lg), 0 0 0 3px rgba(16, 163, 127, 0.1);
}
`;

injectStyle( style );
}

refreshButtons() {
for ( const [ modelKey, button ] of Object.entries( this.buttons ) ) { // modelKey è come "btn-gpt-4o"
const modelValue = modelKey.substring(4); // Estrae "gpt-4o" da "btn-gpt-4o"
const isSelected = modelValue === this.model;

// Gestisce la classe selected
button.classList.toggle( 'selected', isSelected );

// Mantiene sempre la classe del modello per il targeting CSS
button.classList.add(`btn-${modelValue}`);

// Aggiunge attributo per accessibility
button.setAttribute('aria-pressed', isSelected.toString());

// Aggiorna il tooltip con feedback più ricco
if (isSelected) {
button.title = `Modello attivo: ${modelValue}`;
} else {
button.title = `Passa a ${modelValue}`;
}
}
}

async reloadMenuVisibleToggle() {
if (this.isMenuVisibleCommandId) {
try { await GM.unregisterMenuCommand(this.isMenuVisibleCommandId); } catch (e) { console.warn("Failed to unregister menu command for visibility:", e); }
}
this.isMenuVisibleCommandId = await GM.registerMenuCommand(
`${ this.isMenuVisible ? '☑︎' : '☐' } Mostra selettore modelli`,
async () => {
this.isMenuVisible = !this.isMenuVisible;
await GM.setValue( 'isMenuVisible', this.isMenuVisible );
if (this.modelSelector) this.modelSelector.classList.toggle( 'hidden', !this.isMenuVisible );
await this.reloadMenuVisibleToggle();
}
);
}

async reloadMenuVerticalToggle() {
if (this.isMenuVerticalCommandId) {
try { await GM.unregisterMenuCommand(this.isMenuVerticalCommandId); } catch (e) { console.warn("Failed to unregister menu command for orientation:", e); }
}
this.isMenuVerticalCommandId = await GM.registerMenuCommand(
`┖ Stile: ${ this.isMenuVertical ? 'Verticale ↕' : 'Orizzontale ↔' }`,
async () => {
this.isMenuVertical = !this.isMenuVertical;
await GM.setValue( 'isMenuVertical', this.isMenuVertical );

if (!this.modelSelector) return;

const currentRect = this.modelSelector.getBoundingClientRect();
const currentRight = currentRect.right;
const currentBottom = currentRect.bottom;

this.modelSelector.style.visibility = 'hidden';

this.modelSelector.style.left = '0px';
this.modelSelector.style.top = '0px';
this.modelSelector.classList.toggle( 'horizontal', !this.isMenuVertical );

this.modelSelector.offsetHeight;

const newWidth = this.modelSelector.offsetWidth;
const newHeight = this.modelSelector.offsetHeight;

let newLeft = currentRight - newWidth;
let newTop = currentBottom - newHeight;

newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - newWidth));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - newHeight));

this.modelSelector.style.left = `${newLeft}px`;
this.modelSelector.style.top = `${newTop}px`;
this.modelSelector.style.visibility = 'visible';

await GM.setValue( 'relativeMenuPosition', this.getCurrentRelativeMenuPosition() );
await this.reloadMenuVerticalToggle();
},
);
}

injectMessageModelHighlightStyle() {
let style = `
/* Evidenziazione messaggi - stile ChatGPT neutro e uniforme */
div[data-message-model-slug] {
position: relative;
border-radius: 8px;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
margin: 2px 0;
}

/* Indicatore sottile sul bordo sinistro - colore neutro */
div[data-message-model-slug]::before {
content: '';
position: absolute;
left: -4px;
top: 8px;
bottom: 8px;
width: 3px;
border-radius: 2px;
background: rgba(156, 163, 175, 0.4);
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hover effect sottile */
div[data-message-model-slug]:hover {
background: rgba(156, 163, 175, 0.03);
}

div[data-message-model-slug]:hover::before {
background: rgba(156, 163, 175, 0.7);
width: 4px;
left: -5px;
}

/* Badge sottile con nome modello - stile ChatGPT neutro */
div[data-message-model-slug]::after {
content: attr(data-message-model-slug);
position: absolute;
top: 4px;
right: 8px;
background: rgba(156, 163, 175, 0.1);
color: rgba(156, 163, 175, 0.8);
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.6;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
font-family: 'Söhne', 'ui-sans-serif', 'system-ui', sans-serif;
}

div[data-message-model-slug]:hover::after {
opacity: 0.9;
transform: translateY(-1px);
background: rgba(156, 163, 175, 0.15);
color: rgb(156, 163, 175);
}
`;

this.modelHighlightStyleNode = injectStyle( style, !this.isModelHighlightEnabled );
}

async reloadMessageModelHighlightToggle() {
if (this.isModelHighlightEnabledCommandId) {
try { await GM.unregisterMenuCommand(this.isModelHighlightEnabledCommandId); } catch (e) { console.warn("Failed to unregister menu command for highlight:", e); }
}
this.isModelHighlightEnabledCommandId = await GM.registerMenuCommand(
`${ this.isModelHighlightEnabled ? '☑︎' : '☐' } Mostra identificatore modello`,
async () => {
this.isModelHighlightEnabled = !this.isModelHighlightEnabled;
await GM.setValue( 'isModelHighlightEnabled', this.isModelHighlightEnabled );
if (this.modelHighlightStyleNode) this.modelHighlightStyleNode.disabled = !this.isModelHighlightEnabled;
await this.reloadMessageModelHighlightToggle();
},
);
}

createModelSelectorMenu() {
this.modelSelector = document.createElement( 'div' );
this.modelSelector.id = 'model-selector';

// Attributi accessibility
this.modelSelector.setAttribute('role', 'radiogroup');
this.modelSelector.setAttribute('aria-label', 'Selettore modelli ChatGPT');

for ( const [ modelName, modelValue ] of Object.entries( this.availableModels ) ) {
const button = document.createElement( 'button' );
button.textContent = modelName;
button.title = `Passa a ${modelValue}`;

// Attributi accessibility
button.setAttribute('role', 'radio');
button.setAttribute('aria-checked', (modelValue === this.model).toString());
button.setAttribute('tabindex', modelValue === this.model ? '0' : '-1');

// Classi CSS
button.classList.add(`btn-${modelValue}`);

button.addEventListener(
'click',
async event => {
if ( this.shouldCancelClick ) {
event.preventDefault();
event.stopImmediatePropagation();
return;
}
this.model = modelValue;
await GM.setValue( 'model', modelValue );
this.refreshButtons();

// Aggiorna focus per accessibility
this.updateTabIndex();
}
);

// Navigazione keyboard
button.addEventListener('keydown', (event) => {
this.handleKeyNavigation(event, modelValue);
});

this.modelSelector.appendChild( button );
this.buttons[`btn-${ modelValue }`] = button;
}

this.modelSelector.classList.toggle( 'hidden', !this.isMenuVisible );
this.modelSelector.classList.toggle( 'horizontal', !this.isMenuVertical );
return this.modelSelector;
}

// Nuova funzione per gestire la navigazione keyboard
handleKeyNavigation(event, currentModelValue) {
const models = Object.values(this.availableModels);
const currentIndex = models.indexOf(currentModelValue);
let newIndex = currentIndex;

switch(event.key) {
case 'ArrowDown':
case 'ArrowRight':
event.preventDefault();
newIndex = (currentIndex + 1) % models.length;
break;
case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault();
newIndex = (currentIndex - 1 + models.length) % models.length;
break;
case 'Enter':
case ' ':
event.preventDefault();
event.target.click();
return;
case 'Home':
event.preventDefault();
newIndex = 0;
break;
case 'End':
event.preventDefault();
newIndex = models.length - 1;
break;
default:
return;
}

const newModelValue = models[newIndex];
const newButton = this.buttons[`btn-${newModelValue}`];
if (newButton) {
newButton.focus();
}
}

// Nuova funzione per aggiornare il tabindex
updateTabIndex() {
for (const [modelKey, button] of Object.entries(this.buttons)) {
const modelValue = modelKey.substring(4);
const isSelected = modelValue === this.model;
button.setAttribute('tabindex', isSelected ? '0' : '-1');
button.setAttribute('aria-checked', isSelected.toString());
}
}

injectMenu() {
if (this.modelSelector && !document.body.querySelector('#model-selector')) {
document.body.appendChild( this.modelSelector );
}
}

monitorBodyChanges() {
// Throttled handler per ridurre overhead
const handleMutations = this.throttle((mutationsList) => {
// Check rapido se il selector esiste prima di operazioni costose
if (document.getElementById('model-selector')) return;

// Verifica più specifica: solo se il selector è stato rimosso
if (this.modelSelector && !document.body.contains(this.modelSelector)) {
this.injectMenu();
}
}, 16, 'mutations'); // ~60fps throttling

const observer = new MutationObserver((mutationsList) => {
// Early exit se non ci sono cambiamenti rilevanti
let hasRelevantChanges = false;

for (let i = 0; i < mutationsList.length; i++) {
const mutation = mutationsList[i];
if (mutation.type === 'childList' &&
(mutation.removedNodes.length > 0 || mutation.addedNodes.length > 0)) {
hasRelevantChanges = true;
break;
}
}

if (hasRelevantChanges) {
handleMutations(mutationsList);
}
});

// Observer più specifico per ridurre falsi trigger
observer.observe(document.body, {
childList: true,
subtree: false,
attributes: false,
characterData: false
});

// Cleanup su unload
window.addEventListener('beforeunload', () => {
observer.disconnect();
}, { once: true });

return observer;
}

// Default position: bottom-right corner
getDefaultRelativeMenuPosition() {
return {
offsetRight: 20,
offsetBottom: 20 // Changed from offsetTop to offsetBottom
};
}

relativeToAbsolutePosition( relativeMenuPosition ) {
if (!this.modelSelector || this.modelSelector.offsetWidth === 0) {
// Fallback if dimensions are not ready
return {
left: `${window.innerWidth - 150 - (relativeMenuPosition.offsetRight || 20)}px`,
top: `${window.innerHeight - 100 - (relativeMenuPosition.offsetBottom || 20)}px` // Approx height
};
}
return {
left: `${ window.innerWidth - this.modelSelector.offsetWidth - relativeMenuPosition.offsetRight }px`,
top: `${ window.innerHeight - this.modelSelector.offsetHeight - relativeMenuPosition.offsetBottom }px` // Use offsetBottom
}
}

getCurrentRelativeMenuPosition() {
if (!this.modelSelector) return this.getDefaultRelativeMenuPosition();
const currentLeft = parseInt(this.modelSelector.style.left, 10) || 0;
const currentTop = parseInt(this.modelSelector.style.top, 10) || 0;
return {
offsetRight: window.innerWidth - currentLeft - this.modelSelector.offsetWidth,
offsetBottom: window.innerHeight - currentTop - this.modelSelector.offsetHeight // Use offsetBottom
}
}

async restoreMenuPosition() {
const oldMenuPosition = await GM.getValue( 'menuPosition', null ); // For <= v0.53.1 migration
if ( oldMenuPosition && oldMenuPosition.left && oldMenuPosition.top ) {
const oldLeftPx = parseInt(oldMenuPosition.left, 10);
const oldTopPx = parseInt(oldMenuPosition.top, 10);

if (this.modelSelector && this.modelSelector.offsetWidth > 0) {
await GM.setValue('relativeMenuPosition', {
offsetRight: window.innerWidth - oldLeftPx - this.modelSelector.offsetWidth,
offsetBottom: window.innerHeight - oldTopPx - this.modelSelector.offsetHeight // Convert to offsetBottom
});
} else {
await GM.setValue('relativeMenuPosition', this.getDefaultRelativeMenuPosition());
}
await GM.deleteValue( 'menuPosition' );
}

const relativeMenuPosition = await GM.getValue( 'relativeMenuPosition', this.getDefaultRelativeMenuPosition() );
const absoluteMenuPosition = this.relativeToAbsolutePosition( relativeMenuPosition );

if (this.modelSelector) {
this.modelSelector.style.left = absoluteMenuPosition.left;
this.modelSelector.style.top = absoluteMenuPosition.top;
this.ensureMenuInBounds();
}
}

ensureMenuInBounds() {
if (!this.modelSelector) return;
const rect = this.modelSelector.getBoundingClientRect();
let newLeft = rect.left;
let newTop = rect.top;

if (rect.right > window.innerWidth) {
newLeft = window.innerWidth - rect.width;
}
if (rect.bottom > window.innerHeight) {
newTop = window.innerHeight - rect.height;
}
newLeft = Math.max(0, newLeft);
newTop = Math.max(0, newTop);

if (newLeft !== rect.left || newTop !== rect.top) {
this.modelSelector.style.left = `${newLeft}px`;
this.modelSelector.style.top = `${newTop}px`;
}
}

monitorWindowResize() {
// Cache delle dimensioni per evitare ricalcoli
let lastWindowWidth = window.innerWidth;
let lastWindowHeight = window.innerHeight;

const handleResize = this.throttle(async () => {
const currentWidth = window.innerWidth;
const currentHeight = window.innerHeight;

// Early exit se le dimensioni non sono cambiate significativamente
const widthDiff = Math.abs(currentWidth - lastWindowWidth);
const heightDiff = Math.abs(currentHeight - lastWindowHeight);

if (widthDiff < 10 && heightDiff < 10) return;

lastWindowWidth = currentWidth;
lastWindowHeight = currentHeight;

if (!this.modelSelector) return;

// Cache della posizione relativa per evitare await in loop
const relativeMenuPosition = this.getCache('relativeMenuPosition') ||
await GM.getValue('relativeMenuPosition', this.getDefaultRelativeMenuPosition());

// Batch DOM operations
const menuWidth = this.modelSelector.offsetWidth;
const menuHeight = this.modelSelector.offsetHeight;

let targetLeft = currentWidth - menuWidth - relativeMenuPosition.offsetRight;
let targetTop = currentHeight - menuHeight - relativeMenuPosition.offsetBottom;

// Clamp values
targetLeft = Math.max(0, Math.min(targetLeft, currentWidth - menuWidth));
targetTop = Math.max(0, Math.min(targetTop, currentHeight - menuHeight));

// Single style update per ridurre reflow
this.modelSelector.style.cssText +=
`left: ${targetLeft}px; top: ${targetTop}px;`;

// Cache della nuova posizione
this.setCache('relativeMenuPosition', relativeMenuPosition, 30000);
}, 16, 'resize'); // 60fps throttling

window.addEventListener('resize', handleResize, { passive: true });

// Cleanup
window.addEventListener('beforeunload', () => {
window.removeEventListener('resize', handleResize);
}, { once: true });
}

async registerResetMenuPositionCommand() {
if (this.resetMenuPositionCommandId) {
try { await GM.unregisterMenuCommand(this.resetMenuPositionCommandId); } catch(e) { console.warn("Failed to unregister reset command:", e); }
}
this.resetMenuPositionCommandId = await GM.registerMenuCommand(
'⟲ Reimposta posizione menu',
async () => {
const defaultRelativeMenuPosition = this.getDefaultRelativeMenuPosition();
await this.waitForElementDimensions(this.modelSelector);
const defaultAbsoluteMenuPosition = this.relativeToAbsolutePosition( defaultRelativeMenuPosition );

if (this.modelSelector) {
this.modelSelector.style.left = defaultAbsoluteMenuPosition.left;
this.modelSelector.style.top = defaultAbsoluteMenuPosition.top;
await GM.setValue( 'relativeMenuPosition', defaultRelativeMenuPosition );
this.ensureMenuInBounds();
}
}
);
}

getPoint( event ) {
return event.touches ? event.touches[0] : event;
}

mouseDownHandler( event ) {
if (!this.modelSelector) return;
if (event.target.tagName === 'BUTTON') return;

const point = this.getPoint( event );
this.offsetX = point.clientX - this.modelSelector.offsetLeft;
this.offsetY = point.clientY - this.modelSelector.offsetTop;
this.isDragging = true;
this.shouldCancelClick = false;
this.modelSelector.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
if (event.cancelable && event.type === 'touchstart') event.preventDefault();
}

mouseMoveHandler( event ) {
if ( !this.isDragging || !this.modelSelector ) return;

// Throttling per mousemove (performance critica)
if (!this.lastMoveTime) this.lastMoveTime = 0;
const now = performance.now();
if (now - this.lastMoveTime < 16) return; // ~60fps
this.lastMoveTime = now;

const point = this.getPoint( event );
const oldLeft = this.modelSelector.offsetLeft;
const oldTop = this.modelSelector.offsetTop;

let newLeft = point.clientX - this.offsetX;
let newTop = point.clientY - this.offsetY;

// Batch style updates per ridurre reflow
this.modelSelector.style.cssText +=
`left: ${newLeft}px; top: ${newTop}px;`;

if ( !this.shouldCancelClick && (this.modelSelector.offsetLeft !== oldLeft || this.modelSelector.offsetTop !== oldTop ) ) {
this.shouldCancelClick = true;
}

if ( event.cancelable && event.type === 'touchmove') event.preventDefault();
}

async mouseUpHandler( event ) {
if (!this.isDragging || !this.modelSelector) return;
this.isDragging = false;
this.lastMoveTime = 0; // Reset throttling

// Batch style updates
this.modelSelector.style.cssText += 'cursor: grab;';
document.body.style.userSelect = '';

this.ensureMenuInBounds();

// Debounce position saving per evitare troppe scritture
const savePosition = this.debounce(async () => {
await GM.setValue( 'relativeMenuPosition', this.getCurrentRelativeMenuPosition() );
}, 200, 'savePosition');

savePosition();
}

registerGrabbing() {
if (!this.modelSelector) return;

// Bind methods once per ridurre allocazioni
this.boundMouseDown = this.mouseDownHandler.bind(this);
this.boundMouseMove = this.mouseMoveHandler.bind(this);
this.boundMouseUp = this.mouseUpHandler.bind(this);

// Event options ottimizzate
const passiveOptions = { passive: true };
const activeOptions = { passive: false };

this.modelSelector.addEventListener( 'mousedown', this.boundMouseDown, passiveOptions );
document.addEventListener( 'mousemove', this.boundMouseMove, passiveOptions );
document.addEventListener( 'mouseup', this.boundMouseUp, passiveOptions );

this.modelSelector.addEventListener( 'touchstart', this.boundMouseDown, activeOptions );
document.addEventListener( 'touchmove', this.boundMouseMove, activeOptions );
document.addEventListener( 'touchend', this.boundMouseUp, passiveOptions );

// Cleanup automatico
window.addEventListener('beforeunload', () => {
this.cleanupEventListeners();
}, { once: true });
}

cleanupEventListeners() {
if (this.boundMouseDown) {
this.modelSelector?.removeEventListener('mousedown', this.boundMouseDown);
document.removeEventListener('mousemove', this.boundMouseMove);
document.removeEventListener('mouseup', this.boundMouseUp);
this.modelSelector?.removeEventListener('touchstart', this.boundMouseDown);
document.removeEventListener('touchmove', this.boundMouseMove);
document.removeEventListener('touchend', this.boundMouseUp);
}
}

async waitForElementDimensions(element, timeout = 500) {
return new Promise(resolve => {
if (element && element.offsetWidth > 0 && element.offsetHeight > 0) {
resolve();
return;
}
if (!element) {
console.warn("waitForElementDimensions: Element is null or undefined.");
resolve();
return;
}
const startTime = Date.now();
const interval = setInterval(() => {
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
clearInterval(interval);
resolve();
} else if (Date.now() - startTime > timeout) {
clearInterval(interval);
console.warn("Element dimensions not ready after timeout:", element);
resolve();
}
}, 50);
});
}

async run() {
try {
// Avvia il sistema di cache cleanup
this.startCacheCleanup();

await this.init();
this.hookFetch();

// Lazy loading degli stili solo quando necessario
const injectStyles = () => {
this.injectToggleButtonStyle();
this.injectMessageModelHighlightStyle();
};

// Defer style injection per non bloccare rendering
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectStyles, { once: true });
} else {
// Use requestIdleCallback se disponibile per non bloccare main thread
if (window.requestIdleCallback) {
requestIdleCallback(injectStyles, { timeout: 1000 });
} else {
setTimeout(injectStyles, 0);
}
}

this.createModelSelectorMenu();

this.injectMenu();

// Async operations che non bloccano UI
const asyncInitialization = async () => {
if (this.modelSelector) {
await this.waitForElementDimensions(this.modelSelector);
}

await Promise.all([
this.registerResetMenuPositionCommand(),
this.reloadMenuVisibleToggle(),
this.reloadMenuVerticalToggle(),
this.reloadMessageModelHighlightToggle()
]);

this.refreshButtons();
await this.restoreMenuPosition();
};

// Setup dei monitor in parallelo
const setupMonitors = () => {
this.monitorBodyChanges();
this.monitorWindowResize();
this.registerGrabbing();
};

// Esegui setup in parallelo
if (window.requestIdleCallback) {
requestIdleCallback(() => {
asyncInitialization().catch(console.warn);
setupMonitors();
}, { timeout: 2000 });
} else {
setTimeout(() => {
asyncInitialization().catch(console.warn);
setupMonitors();
}, 100);
}

} catch (error) {
console.error("Error running ModelSwitcher:", error);
// Fallback: tenta un'inizializzazione minimale
try {
await this.init();
this.hookFetch();
this.createModelSelectorMenu();
this.injectMenu();
} catch (fallbackError) {
console.error("Fallback initialization failed:", fallbackError);
}
}
}
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const switcher = new ModelSwitcher();
switcher.run();
}, { once: true, passive: true });
} else {
// Use requestIdleCallback per non interferire con il caricamento della pagina
if (window.requestIdleCallback) {
requestIdleCallback(() => {
const switcher = new ModelSwitcher();
switcher.run();
}, { timeout: 1000 });
} else {
// Fallback per browser più vecchi
setTimeout(() => {
const switcher = new ModelSwitcher();
switcher.run();
}, 0);
}
}

})();