// Copyright 2011-2012 Paulo Augusto Peccin. See licence.txt distributed with this file.

package org.javatari.pc.screen;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.security.AccessControlException;
import java.util.Arrays;
import java.util.List;

import javax.jnlp.FileContents;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

import org.javatari.atari.cartridge.Cartridge;
import org.javatari.atari.cartridge.CartridgeInsertionListener;
import org.javatari.atari.cartridge.CartridgeSocket;
import org.javatari.atari.console.savestate.SaveStateSocket;
import org.javatari.general.av.video.VideoMonitor;
import org.javatari.general.av.video.VideoSignal;
import org.javatari.general.av.video.VideoStandard;
import org.javatari.general.board.Clock;
import org.javatari.general.board.ClockDriven;
import org.javatari.parameters.Parameters;
import org.javatari.pc.cartridge.FileROMChooser;
import org.javatari.pc.cartridge.FileServiceROMChooser;
import org.javatari.pc.cartridge.ROMLoader;
import org.javatari.pc.cartridge.URLROMChooser;
import org.javatari.utils.Environment;
import org.javatari.utils.SwingHelper;

public final class Monitor implements ClockDriven, VideoMonitor, CartridgeInsertionListener {
	
	public Monitor() {
		super();
		this.fps = DEFAULT_FPS;
		init();
	}

	public void connect(VideoSignal videoSignal, CartridgeSocket cartridgeSocket, SaveStateSocket savestateSocket) {
		this.cartridgeSocket = cartridgeSocket;
		this.savestateSocket = savestateSocket;
		cartridgeSocket.addInsertionListener(this);
		this.videoSignal = videoSignal;
		videoSignal.connectMonitor(this);
		adjustToVideoSignal();
	}

	public synchronized void setDisplay(MonitorDisplay monitorDisplay) {
		display = monitorDisplay;
		float scX = display.displayDefaultOpenningScaleX(displayWidth, displayHeight);
		setDisplayScale(scX, scX / DEFAULT_SCALE_ASPECT_X);
		displayCenter();
	}
	
	public void setFixedSize(boolean fixed) {
		fixedSizeMode = fixed;
	}
	
	public boolean isFixedSize() {
		return fixedSizeMode;
	}

	public void setCartridgeChangeEnabled(boolean state) {
		cartridgeChangeEnabled = state;
	}

	public boolean isCartridgeChangeEnabled() {
		return cartridgeChangeEnabled;
	}

	public void addControlInputComponents(List<Component> inputs) {
		monitorControls.addInputComponents(inputs);
	}
	
	public void powerOn() {
		synchronized(refreshMonitor) {
			cleanFrontBuffer();
			cleanBackBuffer();
			powerOn = true;
			signalState(false);
			clock.go();
		}
	}

	public void powerOff() {
		synchronized(refreshMonitor) {
			clock.pause();
			powerOn = false;
			signalState(false);
		}
	}

	public void destroy() {
		synchronized(refreshMonitor) {
			clock.terminate();
		}
	}

	@Override
	// Synchronize to avoid changing the standard while receiving lines 
	public synchronized	boolean nextLine(final int[] pixels, boolean vSynchSignal) {
		// Adjusts to the new signal state (on or off) as necessary
		if (!signalState(pixels != null))		// If signal is off, we are done
			return false;
		// Process new line received
		boolean vSynched = false;
		if (line < signalHeight) {
			// Copy only contents that will be displayed
			if (line >= displayOriginY && line < displayOriginY + displayHeight)
				System.arraycopy(pixels, displayOriginX, backBuffer, (line - displayOriginY) * displayWidth, displayWidth);
		} else 
			vSynched = maxLineExceeded();
		line++;
		if (videoStandardDetected == null) videoStandardDetectionFrameLineCount++;
		if (vSynchSignal) {
			if (videoStandardDetected == null) videoStandardDetectionNewFrame();
			vSynched = newFrame() || vSynched;
		}
		return vSynched;
	}

	@Override
	public VideoStandard videoStandardDetected() {
		return videoStandardDetected;
	}

	@Override
	public void videoStandardDetectionStart() {
		videoStandardDetected = null;
		videoStandardDetectionFrameCount = 0;
		videoStandardDetectionFrameLineCount = 0;		
	}

	@Override
	public int currentLine() {
		return line;
	}

	@Override
	public void synchOutput() {
		if (fps < 0) clock.interrupt();		// Just ask for a refresh if in Adaptive mode
		else synchOutputInSwing();
	}

	@Override
	public void showOSD(String message, boolean overlap) {
		if (!overlap && osdFramesLeft > 0) return;
		osdMessage = message;
		osdFramesLeft = message == null ? 0 : OSD_FRAMES;
	}
	
	@Override
	public void clockPulse() {
		synchOutputInSwing();
		// If in "On Demand" mode (fps < 0) then just wait for the next frame to interrupt the sleep, but no more than 2 frames
		if (fps < 0 && !Thread.interrupted()) try { Thread.sleep(1000 / 60 * 2,  0); } catch (InterruptedException e) { /* Awake! */ };
	}

	public void cartridgeInsert(Cartridge cart, boolean autoPower) {
		cartridgeSocket.insert(cart, autoPower);
		display.displayRequestFocus();
	};

	@Override
	public void cartridgeInserted(Cartridge cartridge) {
		// Only change mode if not forced
		if (CRT_MODE >= 0) return;
		if (crtMode == 0 || crtMode == 1)
			setCrtMode(cartridge == null ? 0 : cartridge.getInfo().crtMode == -1 ? 0 : cartridge.getInfo().crtMode);
	}

	private void synchOutputInSwing() {
		if (BUFFER_SYNC_WAIT) SwingHelper.edtSmartInvokeAndWait(refresher);
		else SwingHelper.edtInvokeLater(refresher);
	}

	private boolean newFrame() {
		if (line < signalHeight - VSYNC_TOLERANCE) return false;

		// Flip front and back buffers
		int[] aux = frontBuffer;
		frontBuffer = backBuffer;
		backBuffer = aux;
		
		// Start a new frame
		if (debug > 0) cleanBackBuffer();
		if (showStats) showOSD(videoSignal.standard() + "  " + line + " lines,  CRT mode: " + crtModeNames[crtMode], true);
		line = 0;
		frame++;
		return true;
	}

	private boolean maxLineExceeded() {
		if (line > signalHeight + VSYNC_TOLERANCE + EXTRA_UPPER_VSYNC_TOLERANCE) {
			// if (debug > 0) System.out.println("Display maximum scanlines exceeded: " + line);
			return newFrame();
		}
		return false;
	}
	
	private boolean signalState(boolean state) {
		if (state) {
			signalOn = true;
			adjustToVideoSignal();
		} else { 
			signalOn = false;
			adjustToVideoSignalOff();
		}
		return state;
	}

	private void cleanFrontBuffer() {
		Arrays.fill(frontBuffer, Color.BLACK.getRGB());		 
	}

	private void cleanBackBuffer() {
		// If in debug mode, put a nice green for detection of undrawn lines
		Arrays.fill(backBuffer, debug > 0 ? Color.GREEN.getRGB() : Color.BLACK.getRGB());		 
	}

	private void videoStandardDetectionNewFrame() {
		int linesCount = videoStandardDetectionFrameLineCount;
		videoStandardDetectionFrameLineCount = 0;
		// Only consider frames with linesCount in range with tolerances (NTSC 262, PAL 312)
		if ((linesCount >= 250 && linesCount <= 281)
				|| (linesCount >= 300 && linesCount <= 325))
			if (++videoStandardDetectionFrameCount >= 5)
				videoStandardDetectionFinish(linesCount);
	}

	private void videoStandardDetectionFinish(int linesCount) {
		videoStandardDetected = linesCount < 290 ? VideoStandard.NTSC : VideoStandard.PAL;
		
		// Compute an additional number of lines to make the display bigger, if needed
		// Only used when the detected number of lines per frame is bigger than standard by a reasonable amount 
		int prevAdd = videoStandardDetectionAdtLinesPerFrame;
		int newAdd = linesCount - videoStandardDetected.height;
		if (newAdd > 2) newAdd = (newAdd > 6 ? 6 : newAdd) - 2; 
		else newAdd = 0;

		// Only sets size now if additional lines changed
		if (newAdd != prevAdd) {
			videoStandardDetectionAdtLinesPerFrame = newAdd;
			adjustToVideoStandard(videoStandardDetected);
		}
	}

	private void displayUpdateSize() {
		if (display == null) return;
		synchronized (refreshMonitor) {
			Dimension size = new Dimension((int) (displayWidth * displayScaleX), (int) (displayHeight * displayScaleY));
			display.displaySize(size);
			display.displayMinimumSize(new Dimension((int) (displayWidth * DEFAULT_SCALE_X / DEFAULT_SCALE_Y), displayHeight));
		}
	}

	private void displayCenter() {
		if (display == null) return;
		synchronized (refreshMonitor) {
			display.displayCenter();
		}
	}

	private Graphics2D displayGraphics() {
		if (display == null) return null;
		Graphics2D displayGraphics = display.displayGraphics();
		displayGraphics.setComposite(AlphaComposite.Src);
		// Adjusts the Render Quality if needed
		if (displayGraphics != null && crtFilter) 
			displayGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		return displayGraphics;
	}

	private void displayFrameFinished(Graphics2D graphics) {
		if (display == null) return;
		display.displayFinishFrame(graphics);
	}

	private void init() {
		monitorControls = new MonitorControls(this);
		prepareResources();	 	
		adjustToVideoStandard(VideoStandard.NTSC);
		setDisplayDefaultSize();	
		clock = new Clock("Video Monitor", this, fps);
		paintLogo();
	}

	private void prepareResources() {
		// Prepare Buffers and FrameImage with maximum possible sizes (PAL)
		backBuffer = new int[VideoStandard.PAL.width * VideoStandard.PAL.height];
		frontBuffer = new int[VideoStandard.PAL.width * VideoStandard.PAL.height];
		frameImage = new BufferedImage(VideoStandard.PAL.width, VideoStandard.PAL.height, BufferedImage.TYPE_INT_ARGB);
		if (FRAME_ACCELERATION >= 0) frameImage.setAccelerationPriority(FRAME_ACCELERATION);
		// Prepare the Logo image
		try {
			logoIcon = SwingHelper.loadAsCompatibleImage("org/javatari/pc/screen/images/Logo.png");
		} catch (IOException e) {}
		// Prepare the OSD paint component
		osdComponent = new JLabel();
		osdComponent.setForeground(Color.GREEN);
		osdComponent.setBackground(new Color(0x50000000, true));
		osdComponent.setFont(new Font(Environment.ARIAL_FONT ? "Arial" 
				: Environment.LIBERATION_FONT ? "Liberation Sans" : "SansSerif", Font.BOLD, 15));
		osdComponent.setBorder(new EmptyBorder(5, 12, 5, 12));
		osdComponent.setOpaque(true);
		// Prepare CRT mode 2 texture
		scanlinesTextureImage = new BufferedImage(2048, 1280, BufferedImage.TYPE_INT_ARGB_PRE);
		Graphics2D g = scanlinesTextureImage.createGraphics();
		g.setColor(new Color((int)(SCANLINES_STRENGTH * 255) << 24, true));
		for(int i = 1; i < scanlinesTextureImage.getHeight(); i += 2)
			g.drawLine(0, i, scanlinesTextureImage.getWidth(), i);
		g.dispose();
		if (SCANLINES_ACCELERATION >= 0) scanlinesTextureImage.setAccelerationPriority(SCANLINES_ACCELERATION);
		// Prepare CRT mode 3 and 4 composition
		crtTriadComposite = new CRTTriadComposite();
		// Prepare intermediate image for CRT modes or OSD rendering in SingleBuffer mode
		intermFrameImage = new BufferedImage(2048, 1280, BufferedImage.TYPE_INT_RGB);
		if (IMTERM_FRAME_ACCELERATION >= 0) intermFrameImage.setAccelerationPriority(IMTERM_FRAME_ACCELERATION);
	}

	private void adjustToVideoSignal() {
		if (signalStandard != videoSignal.standard())
			adjustToVideoStandard(videoSignal.standard());
	}

	// Synchronize to avoid changing the standard while refreshing frame or receiving lines 
	private synchronized void adjustToVideoStandard(VideoStandard videoStandard) {
		synchronized (refreshMonitor) {
			signalStandard = videoStandard;
			signalWidth = videoStandard.width;
			signalHeight = videoStandard.height;
			setDisplaySize(displayWidth, displayHeightPct);
			setDisplayOrigin(displayOriginX, displayOriginYPct);
		}
	}

	private void adjustToVideoSignalOff() {
		line = 0;
		display.displayClear();
		paintLogo();
		if (fps == 0 && powerOn && signalOffRefresher == null) {
			SignalOffRefresher ref = new SignalOffRefresher();
			signalOffRefresher = ref;
			ref.start();
		}
	}

	private void paintLogo() {
		synchronized (refreshMonitor) {
			Graphics2D canvasGraphics = displayGraphics();
			if (canvasGraphics == null) return;
			Dimension ces = display.displayEffectiveSize();
			int w = ces.width;
			int h = ces.height;
			Graphics2D intermGraphics = intermFrameImage.createGraphics();
			intermGraphics.setBackground(Color.BLACK);
			intermGraphics.clearRect(0, 0, w, h);
			int lw = logoIcon.getWidth(null);
			int lh = logoIcon.getHeight(null);
			float r = h < lh ? (float)h / lh : 1;
			lw *= r; lh *= r;
			intermGraphics.drawImage(logoIcon, (w - lw) / 2, (h - lh) / 2, lw, lh, null);
			paintOSD(intermGraphics);
			canvasGraphics.drawImage(intermFrameImage, 0, 0, w, h, 0, 0, w, h, null);
			intermGraphics.dispose();
			displayFrameFinished(canvasGraphics);
		}
	}
		
	private void paintOSD(Graphics2D canvasGraphics) {
		if (--osdFramesLeft < 0) return;
		canvasGraphics.setComposite(AlphaComposite.SrcOver);
		osdComponent.setText(osdMessage);
		Dimension s = osdComponent.getPreferredSize();
		SwingUtilities.paintComponent(
			canvasGraphics, osdComponent, display.displayContainer(), 
			(display.displayEffectiveSize().width - s.width) - 12, 12, 
			s.width, s.height
		);
	}

	private void refresh() {
		if (!signalOn) {
			paintLogo();
			return;
		}
		// Synchronize to avoid changing image properties while refreshing frame
		synchronized (refreshMonitor) {
			int f = frame;
			if (lastFrameRendered == f) return;

			int[] drawBuffer = frontBuffer;
			Graphics2D displayGraphics = displayGraphics();
			if (displayGraphics == null) return;
			// Get the entire Canvas
			Dimension ces = display.displayEffectiveSize();
			int displayEffectiveWidth = ces.width;
			int displayEffectiveHeight = ces.height;
			// CRT mode 3 OR no MultiBuffering active and needs to superimpose (CRT mode 1, 2 or OSD)
			// draw frameImage to intermediate image with composite then transfer to Canvas
			if (crtMode >= 3 || (MULTI_BUFFERING < 2 && (osdFramesLeft >= 0 || crtMode > 0))) {
				int intermWidth = Math.min(displayEffectiveWidth, 2048);
				int intermHeight = Math.min(displayEffectiveHeight, 1280);
				Graphics2D intermGraphics = intermFrameImage.createGraphics();
				intermGraphics.setComposite(AlphaComposite.Src);
				// Renders to intermediate image
				renderFrame(intermGraphics, drawBuffer, intermWidth, intermHeight);
				// If CRT mode 2, alpha-superimpose the prepared scanlines image
				if (crtMode == 2)
					renderScanlines(intermGraphics, intermWidth, intermHeight);
				// If CRT mode 3 or 4, sets the CRTTriadComposite and rewrite
				if (crtMode >= 3) {
					intermGraphics.setComposite(crtTriadComposite);
					intermGraphics.drawImage(
						intermFrameImage,
						0, 0, intermWidth, intermHeight,
						0, 0, intermWidth, intermHeight,
						null);
				}
				paintOSD(intermGraphics);
				intermGraphics.dispose();
				// Then transfer to Canvas
				displayGraphics.drawImage(
					intermFrameImage,
					0, 0, intermWidth, intermHeight,
					0, 0, intermWidth, intermHeight,
					null);
			} else {
				// Renders directly to Canvas
				renderFrame(displayGraphics, drawBuffer, displayEffectiveWidth, displayEffectiveHeight);
				// If CRT mode 2, alpha-superimpose the prepared scanlines image
				if (crtMode == 2)
					renderScanlines(displayGraphics, displayEffectiveWidth, displayEffectiveHeight);
				paintOSD(displayGraphics);
			}
			displayFrameFinished(displayGraphics);
			lastFrameRendered = f;
		}
	}

	private void renderFrame(Graphics2D graphics, int[] drawBuffer, int effectiveWidth, int effectiveHeight) {
		// If CRT mode 1, 2 or 4, set composite for last and new frame over each other, and draw old frame
		if (crtMode > 0 && crtMode != 3) {
			// Clear last image
			graphics.setBackground(Color.BLACK);
			graphics.clearRect(0, 0, effectiveWidth, effectiveHeight);
			// Draw old frame
			graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, CRT_RETENTION_ALPHA));
			graphics.drawImage(frameImage, 0, 0, effectiveWidth, effectiveHeight, 0, 0, displayWidth, displayHeight, null);
		}
		// Update the image to draw with contents stored in the frontBuffer
		frameImage.getRaster().setDataElements(0, 0, displayWidth, displayHeight, drawBuffer);
		// Draw new frame
		graphics.drawImage(frameImage, 0, 0, effectiveWidth, effectiveHeight, 0, 0, displayWidth, displayHeight, null);
	}

	private void renderScanlines(Graphics2D graphics, int effectiveWidth, int effectiveHeight) {
		graphics.setComposite(AlphaComposite.SrcOver);
		graphics.drawImage(scanlinesTextureImage, 0, 0, effectiveWidth, effectiveHeight, 0, 0, effectiveWidth, effectiveHeight, null);
	}

	private synchronized void setDisplayDefaultSize() {
		setDisplaySize(DEFAULT_WIDTH, DEFAULT_HEIGHT_PCT);
		setDisplayOrigin(DEFAULT_ORIGIN_X, DEFAULT_ORIGIN_Y_PCT);
		if (display != null) {
			float scX = display.displayDefaultOpenningScaleX(displayWidth, displayHeight);
			setDisplayScale(scX, scX / DEFAULT_SCALE_ASPECT_X);
		} else
			setDisplayScale(DEFAULT_SCALE_X, DEFAULT_SCALE_Y);
		displayCenter();
	}

	private synchronized void setDisplayOrigin(int x, double yPct) {
		displayOriginX = x;
		if (displayOriginX < 0) displayOriginX = 0;
		else if (displayOriginX > signalWidth - displayWidth) displayOriginX = signalWidth - displayWidth;
		
		displayOriginYPct = yPct;
		if (displayOriginYPct < 0) displayOriginYPct = 0;
		else if ((displayOriginYPct / 100 * signalHeight) > signalHeight - displayHeight) 
			displayOriginYPct = ((double)signalHeight - displayHeight) / signalHeight * 100; 

		// Compute final display originY, adding a little for additional lines as discovered in last video standard detection
		int adtOriginY = videoStandardDetectionAdtLinesPerFrame / 2;
		displayOriginY = (int) (displayOriginYPct / 100 * signalHeight) + adtOriginY;
		if ((displayOriginY + displayHeight) > signalHeight) displayOriginY = signalHeight - displayHeight;
	}

	private synchronized void setDisplaySize(int width, double heightPct) {
		displayWidth = width;
		if (displayWidth < 10) displayWidth = 10;
		else if (displayWidth > signalWidth) displayWidth = signalWidth;
		
		displayHeightPct = heightPct;
		if (displayHeightPct < 10) displayHeightPct = 10;
		else if (displayHeightPct > 100) displayHeightPct = 100;
		
		// Compute final display height, considering additional lines as discovered in last video standard detection
		displayHeight = (int) (displayHeightPct / 100 * (signalHeight + videoStandardDetectionAdtLinesPerFrame));
		if (displayHeight > signalHeight) displayHeight = signalHeight;

		setDisplayOrigin(displayOriginX, displayOriginYPct);
		displayUpdateSize();
	}

	private void setDisplayScale(float x, float y) {
		displayScaleX = x;
		if (displayScaleX < 1) displayScaleX = 1;
		displayScaleY = y;
		if (displayScaleY < 1) displayScaleY = 1;
		displayUpdateSize();
	}

	private void setDisplayScaleDefaultAspect(float y) {
		int scaleY = (int) y;
		if (scaleY < 1) scaleY = 1;
		setDisplayScale(scaleY * DEFAULT_SCALE_ASPECT_X, scaleY);
	}

	private void loadCartridgeFromFile(boolean autoPower) {
		if (cartridgeChangeDisabledWarning()) return;
		display.displayLeaveFullscreen();
		Cartridge cart = null;
		try {
			File file = FileROMChooser.chooseFileToLoad();
			if (file != null) cart = ROMLoader.load(file);
		} catch (AccessControlException e) {
			// Automatically tries FileServiceChooser if access is denied
			FileContents fileContents = FileServiceROMChooser.chooseFileToLoad();
			if (fileContents != null) cart = ROMLoader.load(fileContents);
		}
		if (cart != null) cartridgeInsert(cart, autoPower);
		else display.displayRequestFocus();
	}

	private void loadCartridgeFromURL(boolean autoPower) {
		if (cartridgeChangeDisabledWarning()) return;
		display.displayLeaveFullscreen();
		Cartridge cart = null;
		String url = URLROMChooser.chooseURLToLoad();
		if (url != null) cart = ROMLoader.load(url, false);
		if (cart != null) cartridgeInsert(cart, autoPower);
		else display.displayRequestFocus();
	}

	private void loadCartridgeEmpty() {
		if (cartridgeChangeDisabledWarning()) return;
		cartridgeSocket.insert(null, false);
	}

	private void loadCartridgePaste() {
		if (cartridgeChangeDisabledWarning()) return;
		try {
			Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
			Transferable transf = clip.getContents("Ignored");
			if (transf == null) return;
			Cartridge cart = ROMTransferHandlerUtil.importCartridgeData(transf);
			if (cart != null) cartridgeInsert(cart, true);
		} catch (Exception ex) {
			// Simply give up
		}
	}

	private void saveStateCartridge() {
		savestateSocket.saveStateFile();
		display.displayRequestFocus();
	}

	private boolean cartridgeChangeDisabledWarning() {
		if (!isCartridgeChangeEnabled()) {
			showOSD("Cartridge change is disabled", true);
			return true;
		}
		return false;
	}

	private void crtModeToggle() {
		setCrtMode(crtMode + 1);
	}

	private void setCrtMode(int mode) {
		synchronized (refreshMonitor) {
			int newMode = mode > 4 || mode < 0 ? 0 : mode;
			if (crtMode == newMode) return;
			crtMode = newMode;
			showOSD("CRT mode: " + crtModeNames[crtMode], true);
		}
	}

	public void controlActivated(Control control) {
		// All controls are Press-only and repeatable
		switch(control) {
			case LOAD_CARTRIDGE_FILE:
				loadCartridgeFromFile(true); break;
			case LOAD_CARTRIDGE_FILE_NO_AUTO_POWER:
				loadCartridgeFromFile(false); break;
			case LOAD_CARTRIDGE_URL:
				loadCartridgeFromURL(true); break;
			case LOAD_CARTRIDGE_URL_NO_AUTO_POWER:
				loadCartridgeFromURL(false); break;
			case LOAD_CARTRIDGE_EMPTY:
				loadCartridgeEmpty(); break;
			case LOAD_CARTRIDGE_PASTE:
				loadCartridgePaste(); break;
			case SAVE_STATE_CARTRIDGE:
				saveStateCartridge(); break;
			case CRT_FILTER:
				crtFilter = !crtFilter;
				showOSD(crtFilter ? "CRT Filter: ON" : "CRT Filter: OFF", true);
				break;
			case CRT_MODES:
				crtModeToggle(); break;
			case STATS:
				showStats = !showStats; showOSD(null, true); break;
			case DEBUG:
				debug++;
				if (debug > 4) debug = 0;
				break;
			case ORIGIN_X_MINUS:
				setDisplayOrigin(displayOriginX + 1, displayOriginYPct); break;
			case ORIGIN_X_PLUS:		
				setDisplayOrigin(displayOriginX - 1, displayOriginYPct); break;
			case ORIGIN_Y_MINUS:
				setDisplayOrigin(displayOriginX, displayOriginYPct + 0.5); break;
			case ORIGIN_Y_PLUS:
				setDisplayOrigin(displayOriginX, displayOriginYPct - 0.5); break;
			case SIZE_DEFAULT:
				setDisplayDefaultSize(); break;
		}
		if (fixedSizeMode) return;
		switch(control) {
			case WIDTH_MINUS:
				setDisplaySize(displayWidth - 1, displayHeightPct); break;
			case WIDTH_PLUS:		
				setDisplaySize(displayWidth + 1, displayHeightPct); break;
			case HEIGHT_MINUS:
				setDisplaySize(displayWidth, displayHeightPct - 0.5); break;
			case HEIGHT_PLUS:
				setDisplaySize(displayWidth, displayHeightPct + 0.5); break;
			case SCALE_X_MINUS:
				setDisplayScale(displayScaleX - 0.5f, displayScaleY); break;
			case SCALE_X_PLUS:		
				setDisplayScale(displayScaleX + 0.5f, displayScaleY); break;
			case SCALE_Y_MINUS:
				setDisplayScale(displayScaleX, displayScaleY - 0.5f); break;
			case SCALE_Y_PLUS:
				setDisplayScale(displayScaleX, displayScaleY + 0.5f); break;
			case SIZE_MINUS:
				setDisplayScaleDefaultAspect(displayScaleY - 1); break;
			case SIZE_PLUS:
				setDisplayScaleDefaultAspect(displayScaleY + 1); break;
		}
	}


	public Clock clock;
	
	public String refreshMonitor = "refreshMonitor";		// Used only for synchronization

	private MonitorControls monitorControls;

	private boolean fixedSizeMode = FIXED_SIZE;
	private boolean cartridgeChangeEnabled = CARTRIDGE_CHANGE;
	
	private VideoSignal videoSignal;
	private CartridgeSocket cartridgeSocket;
	private SaveStateSocket savestateSocket;
	private final double fps;
	
	private VideoStandard signalStandard;
	private int signalWidth;
	private int signalHeight;

	private VideoStandard videoStandardDetected;
	private int videoStandardDetectionFrameCount;
	private int videoStandardDetectionFrameLineCount = 0;
	private int videoStandardDetectionAdtLinesPerFrame = 0;

	private int[] backBuffer;
	private int[] frontBuffer;

	private int displayWidth;
	private int displayHeight;
	private double displayHeightPct;
	private int displayOriginX;
	private int displayOriginY;
	private double displayOriginYPct;
	private float displayScaleX;
	private float displayScaleY;
	
	private boolean powerOn = false;
	private boolean signalOn = false;
	private SignalOffRefresher signalOffRefresher;
	
	private int osdFramesLeft = -1;
	private String osdMessage; 
	private JLabel osdComponent;
	
	private boolean crtFilter = CRT_FILTER;
	private int crtMode = CRT_MODE < 0 ? 0 : CRT_MODE;

	private int debug = 0;
	private boolean showStats = false;
	
	private int line = 0;
	private int frame = 0;
	private int lastFrameRendered = -1;

	private MonitorDisplay display;

	private BufferedImage frameImage;
	
	private BufferedImage scanlinesTextureImage;
	private CRTTriadComposite crtTriadComposite;
	private BufferedImage intermFrameImage;
	
	private Image logoIcon;
	
	private final Runnable refresher = new Runnable() {
		@Override
		public void run() {
			refresh();
		}
	};
	
	private final class SignalOffRefresher extends Thread {
		@Override
		public void run() {
			try { sleep(1000/30); } catch (InterruptedException e) {}
			while(powerOn && !signalOn) {
				synchOutput();
				try { sleep(1000/30); } catch (InterruptedException e) {}	// 30 fps
			}
			signalOffRefresher = null;
		}
	};

	private static final String[] crtModeNames = { "OFF", "Phosphor", "Phosphor Scanlines", "RGB", "RGB Phosphor" };
	
	private static final int EXTRA_UPPER_VSYNC_TOLERANCE = 5;

	public static final double   DEFAULT_FPS = Parameters.SCREEN_DEFAULT_FPS;
	public static final int      BUFFER_VSYNC = Parameters.SCREEN_BUFFER_VSYNC;
	public static final int      MULTI_BUFFERING = Parameters.SCREEN_MULTI_BUFFERING;
	public static final boolean  PAGE_FLIPPING = Parameters.SCREEN_PAGE_FLIPPING;
	private static final int     VSYNC_TOLERANCE = Parameters.SCREEN_VSYNC_TOLERANCE;
	private static final boolean BUFFER_SYNC_WAIT = Parameters.SCREEN_BUFFER_SYNC_WAIT;
	private static final int     DEFAULT_ORIGIN_X = Parameters.SCREEN_DEFAULT_ORIGIN_X;
	private static final double  DEFAULT_ORIGIN_Y_PCT = Parameters.SCREEN_DEFAULT_ORIGIN_Y_PCT;		// Percentage of height
	private static final int     DEFAULT_WIDTH = Parameters.SCREEN_DEFAULT_WIDTH;
	private static final double  DEFAULT_HEIGHT_PCT = Parameters.SCREEN_DEFAULT_HEIGHT_PCT;			// Percentage of height
	public static final float    DEFAULT_SCALE_X = Parameters.SCREEN_DEFAULT_SCALE_X;
	public static final float    DEFAULT_SCALE_ASPECT_X = Parameters.SCREEN_DEFAULT_SCALE_ASPECT_X;
	private static final float   DEFAULT_SCALE_Y = Parameters.SCREEN_DEFAULT_SCALE_Y;
	private static final int     OSD_FRAMES = Parameters.SCREEN_OSD_FRAMES;
	private static final boolean CRT_FILTER = Parameters.SCREEN_CRT_FILTER;
	private static final int     CRT_MODE = Parameters.SCREEN_CRT_MODE;
	private static final float   CRT_RETENTION_ALPHA = Parameters.SCREEN_CRT_RETENTION_ALPHA;
	private static final float   SCANLINES_STRENGTH = Parameters.SCREEN_SCANLINES_STRENGTH;
	private static final float   FRAME_ACCELERATION = Parameters.SCREEN_FRAME_ACCELERATION;
	private static final float   IMTERM_FRAME_ACCELERATION = Parameters.SCREEN_INTERM_FRAME_ACCELERATION;
	private static final float   SCANLINES_ACCELERATION = Parameters.SCREEN_SCANLINES_ACCELERATION;
	private static final boolean CARTRIDGE_CHANGE = Parameters.SCREEN_CARTRIDGE_CHANGE;
	private static final boolean FIXED_SIZE = Parameters.SCREEN_FIXED_SIZE;

	public static final long serialVersionUID = 0L;


	public static enum Control {
		WIDTH_PLUS, HEIGHT_PLUS,
		WIDTH_MINUS, HEIGHT_MINUS,
		ORIGIN_X_PLUS, ORIGIN_Y_PLUS,
		ORIGIN_X_MINUS, ORIGIN_Y_MINUS,
		SCALE_X_PLUS, SCALE_Y_PLUS,
		SCALE_X_MINUS, SCALE_Y_MINUS,
		SIZE_PLUS, SIZE_MINUS,
		SIZE_DEFAULT,
		LOAD_CARTRIDGE_FILE, LOAD_CARTRIDGE_FILE_NO_AUTO_POWER,
		LOAD_CARTRIDGE_URL, LOAD_CARTRIDGE_URL_NO_AUTO_POWER,
		LOAD_CARTRIDGE_EMPTY,
		LOAD_CARTRIDGE_PASTE,
		SAVE_STATE_CARTRIDGE,
		CRT_FILTER, CRT_MODES,
		DEBUG, STATS
	}

}

