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

package org.javatari.atari.tia;		


import java.io.Serializable;
import java.util.Arrays;
import java.util.Map;

import org.javatari.atari.board.BUS;
import org.javatari.atari.controls.ConsoleControls;
import org.javatari.atari.controls.ConsoleControlsInput;
import org.javatari.atari.tia.audio.AudioGenerator;
import org.javatari.atari.tia.audio.AudioMonoGenerator;
import org.javatari.atari.tia.video.NTSCPalette;
import org.javatari.atari.tia.video.PALPalette;
import org.javatari.atari.tia.video.VideoGenerator;
import org.javatari.general.av.video.VideoStandard;
import org.javatari.general.board.BUS16Bits;
import org.javatari.general.board.ClockDriven;
import org.javatari.parameters.Parameters;
import org.javatari.utils.ArraysCopy;


public final class TIA implements BUS16Bits, ClockDriven, ConsoleControlsInput {

	public TIA() {
		videoOutput = new VideoGenerator();
		audioOutput = new AudioMonoGenerator();
	}

	public void connectBus(BUS bus) {
		this.bus = bus;
	}

	public VideoGenerator videoOutput() {	// VideoSignal
		return videoOutput;
	}

	public AudioGenerator audioOutput() {	// AudioSignal
		return audioOutput;
	}

	public void videoStandard(VideoStandard standard) {
		videoOutput.standard(standard);
		palette = standard.equals(VideoStandard.NTSC) ? NTSCPalette.getPalette() : PALPalette.getPalette();
	}
	
	public double desiredClockForVideoStandard() {
		if (FORCED_CLOCK != 0) return FORCED_CLOCK;
		return videoOutput.standard().fps;
	}
	
	public void powerOn() {
		framePatternPosition = 0;
		Arrays.fill(linePixels, HBLANK_COLOR);
		Arrays.fill(debugPixels, 0);
		audioOutput.channel0().setVolume(0);
		audioOutput.channel1().setVolume(0);
		initLatchesAtPowerOn();
		observableChangeExtended();
		powerOn = true;
	}

	public void powerOff() {
		powerOn = false;
		// Let monitors know that the signals are off
		videoOutput.signalOff();		
		audioOutput.signalOff();		
	}

	@Override
	// To perform better, TIA is using one clock cycle per frame
	public void clockPulse() {
		
		int frames = 0;
		if (powerOn) {
			if (debugPause) {
				if (debugPausedMoreFrames()) frames = 1;
			} else {
				if (--framePatternPosition < 0) framePatternPosition = framePattern.length - 1;
				frames = framePattern[framePatternPosition];
			}
		}
		
		if (frames <= 0) return;

	 	while (frames > 0 && powerOn) {
			clock = 0;
			// Send the first clock/3 pulse to the CPU and PIA, perceived by TIA at clock 0
			bus.clockPulse();
			// Releases the CPU at the beginning of the line in case a WSYNC has halted it
			if (!bus.cpu.RDY) bus.cpu.RDY = true;
			// HBLANK period
			for (clock = 3; clock < HBLANK_DURATION; clock += 3) {		// 3 .. 66
				if (!repeatLastLine) checkRepeatMode();
				// Send clock/3 pulse to the CPU and PIA each 3rd TIA cycle 
				bus.clockPulse();
			}
			// 67
			// First Audio Sample. 2 samples per scan line ~ 31440 KHz
			audioOutput.clockPulse();
			// Display period
			int subClock3 = 2;	// To control the clock/3 cycles. First at clock 69
			for (clock = 68; clock < LINE_WIDTH; clock++) {			// 68 .. 227
				if (!repeatLastLine) checkRepeatMode();
				// Clock delay decodes
				if (vBlankDecodeActive) vBlankClockDecode();
				// Send clock/3 pulse to the CPU and PIA each 3rd TIA cycle 
				if (--subClock3 == 0) {
					bus.clockPulse();
					subClock3 = 3;
				}
				objectsClockCounters();
				if (!repeatLastLine && (clock >= 76 || !hMoveHitBlank))
					setPixelValue();
				// else linePixels[clock] |= 0x88800080;	// Add a pink dye to show pixels repeated
			}
			// End of scan line
			// Second Audio Sample. 2 samples per scan line ~ 31440 KHz
			audioOutput.clockPulse();
			// Handle Paddles capacitor charging
			if (paddle0Position >= 0 && !paddleCapacitorsGrounded) paddlesChargeCapacitors();	// Only if paddles are connected (position >= 0)
			finishLine();
			// Send the finished line to the output and check if monitor vSynched 
			if (videoOutput.nextLine(linePixels, vSyncOn)) frames--;
		}

		if (powerOn) {
			audioOutput.finishFrame();
			videoOutput.finishFrame();
		}
	}

	private void checkRepeatMode() {
		// If one entire line since last observable change has just completed, enter repeatLastLine mode
		if (clock == lastObservableChangeClock) {
			repeatLastLine = true;
			lastObservableChangeClock = -1;
		}
	}

	private void setPixelValue() {
		// No need to calculate all possibilities in vSync/vBlank. TODO No collisions will be detected
		if (vSyncOn) { linePixels[clock] = vSyncColor; return; }
		if (vBlankOn) { linePixels[clock] = vBlankColor; return; }
		// Updates the current PlayFiled pixel to draw only each 4 pixels, or at the first calculated pixel after stopped using cached line
		if ((clock & 0x03) == 0 || clock == lastObservableChangeClock)		// clock & 0x03 is the same as clock % 4
			playfieldUpdateCurrentPixel();
		// Pixel color
		int color = 1;		// All valid colors are between 0xffffffff ad 0x00000000, therefore <= 0
		// Flags for Collision latches
		boolean P0 = false, P1 = false, M0 = false, M1 = false, FL = false, BL = false;
		// Get the value for the PlayField and Ball first only if PlayField and Ball have higher priority
		if (playfieldPriority) {
			// Get the value for the Ball
			if (ballScanCounter >= 0 && ballScanCounter <= 7) {
				playersPerformDelayedSpriteChanges();		// May trigger Ball delayed enablement
				if (ballEnabled) {
					BL = true;
					color = ballColor;
				}
			}
			if (playfieldCurrentPixel) { 
				FL = true;
				if (color > 0) color = playfieldColor;	// No Score Mode in priority mode
			}
		}
		// Get the value for Player0
		if (player0ScanCounter >= 0 && player0ScanCounter <= 31) {
			playersPerformDelayedSpriteChanges();
			int sprite = player0VerticalDelay ? player0ActiveSprite : player0DelayedSprite;
			if (sprite != 0)
				if (((sprite >> (player0Reflected ? (7 - (player0ScanCounter >>> 2)) : (player0ScanCounter >>> 2))) & 0x01) != 0) {
					P0 = true;
					if (color > 0) color = player0Color;
				}
		}
		if (missile0ScanCounter >= 0 && missile0Enabled && missile0ScanCounter <= 7 && !missile0ResetToPlayer) {
			M0 = true;
			if (color > 0) color = missile0Color;
		}
		// Get the value for Player1
		if (player1ScanCounter >= 0 && player1ScanCounter <= 31) {
			playersPerformDelayedSpriteChanges();
			int sprite = player1VerticalDelay ? player1ActiveSprite : player1DelayedSprite;
			if (sprite != 0)
				if (((sprite >> (player1Reflected ? (7 - (player1ScanCounter >>> 2)) : (player1ScanCounter >>> 2))) & 0x01) != 0) {
					P1 = true;
					if (color > 0) color = player1Color;
				}
		}
		if (missile1ScanCounter >= 0 && missile1Enabled &&  missile1ScanCounter <= 7 && !missile1ResetToPlayer) {
			M1 = true;
			if (color > 0) color = missile1Color;
		}
		if (!playfieldPriority) {
			// Get the value for the Ball (low priority)
			if (ballScanCounter >= 0 && ballScanCounter <= 7) {
				playersPerformDelayedSpriteChanges();		// May trigger Ball delayed enablement
				if (ballEnabled) {
					BL = true;
					if (color > 0) color = ballColor;
				}
			}
			// Get the value for the the PlayField (low priority)
			if (playfieldCurrentPixel) {
				FL = true;
				if (color > 0) color = !playfieldScoreMode ? playfieldColor : (clock < 148 ? player0Color : player1Color);
			}
		}
		// If nothing more is showing, get the PlayField background value (low priority)
		if (color > 0) color = playfieldBackground;
		// Set the correct pixel color
		linePixels[clock] = color;
		// Finish collision latches
		if (debugNoCollisions) return;
		if (P0 && FL) 
			CXP0FB |= 0x80;
		if (P1) {
			if (FL) CXP1FB |= 0x80;
			if (P0) CXPPMM |= 0x80;
		}
		if (BL) {
			if (FL) CXBLPF |= 0x80;
			if (P0) CXP0FB |= 0x40; 
			if (P1) CXP1FB |= 0x40; 
		}
		if (M0) {
			if (P1) CXM0P  |= 0x80;
			if (P0) CXM0P  |= 0x40; 
			if (FL) CXM0FB |= 0x80;
			if (BL) CXM0FB |= 0x40; 
		}
		if (M1) {
			if (P0) CXM1P  |= 0x80;
			if (P1) CXM1P  |= 0x40; 
			if (FL) CXM1FB |= 0x80;
			if (BL) CXM1FB |= 0x40; 
			if (M0) CXPPMM |= 0x40; 
		}
	}

	private void objectsClockCounters() {
		player0ClockCounter();
		player1ClockCounter();
		missile0ClockCounter();
		missile1ClockCounter();
		ballClockCounter();
	}

	private void player0ClockCounter() {
		if (++player0Counter == 160) player0Counter = 0;
		if (player0ScanCounter >= 0) {
			// If missileResetToPlayer is on and the player scan has started the FIRST copy
			if (missile0ResetToPlayer && player0Counter < 12 && player0ScanCounter >= 28 && player0ScanCounter <= 31)
				missile0Counter = 156;
			player0ScanCounter -= player0ScanSpeed;
		}
		// Start scans 4 clocks before each copy. Scan is between 0 and 31, each pixel = 4 scan clocks
		if (player0Counter == 156) {
			if (player0RecentReset) player0RecentReset = false;
			else player0ScanCounter = 31 + player0ScanSpeed * (player0ScanSpeed == 4 ? 5 : 6);	// If Double or Quadruple size, delays 1 additional pixel 
		}
		else if (player0Counter == 12) {
			if (player0CloseCopy) player0ScanCounter = 31 + player0ScanSpeed * 5;
		}
		else if (player0Counter == 28) {
			if (player0MediumCopy) player0ScanCounter = 31 + player0ScanSpeed * 5;
		}
		else if (player0Counter == 60) {
			if (player0WideCopy) player0ScanCounter = 31 + player0ScanSpeed * 5;
		}
	}

	private void player1ClockCounter() {
		if (++player1Counter == 160) player1Counter = 0;
		if (player1ScanCounter >= 0) {
			// If missileResetToPlayer is on and the player scan has started the FIRST copy
			if (missile1ResetToPlayer && player1Counter < 12 && player1ScanCounter >= 28 && player1ScanCounter <= 31)
				missile1Counter = 156;
			player1ScanCounter -= player1ScanSpeed;
		}
		// Start scans 4 clocks before each copy. Scan is between 0 and 31, each pixel = 4 scan clocks
		if (player1Counter == 156) {
			if (player1RecentReset) player1RecentReset = false;
			else player1ScanCounter = 31 + player1ScanSpeed * (player1ScanSpeed == 4 ? 5 : 6);	// If Double or Quadruple size, delays 1 additional pixel 
		}
		else if (player1Counter == 12) {
			if (player1CloseCopy) player1ScanCounter = 31 + player1ScanSpeed * 5;
		}
		else if (player1Counter == 28) {
			if (player1MediumCopy) player1ScanCounter = 31 + player1ScanSpeed * 5;
		}
		else if (player1Counter == 60) {
			if (player1WideCopy) player1ScanCounter = 31 + player1ScanSpeed * 5;
		}
	}

	private void missile0ClockCounter() {
		if (++missile0Counter == 160) missile0Counter = 0;
		if (missile0ScanCounter >= 0) missile0ScanCounter -= missile0ScanSpeed;
		// Start scans 4 clocks before each copy. Scan is between 0 and 7, each pixel = 8 scan clocks
		if (missile0Counter == 156) {
			if (missile0RecentReset) missile0RecentReset = false;
			else missile0ScanCounter = 7 + missile0ScanSpeed * 4; 
		}
		else if (missile0Counter == 12) {
			if (player0CloseCopy) missile0ScanCounter = 7 + missile0ScanSpeed * 4;
		}
		else if (missile0Counter == 28) {
			if (player0MediumCopy) missile0ScanCounter = 7 + missile0ScanSpeed * 4;
		}
		else if (missile0Counter == 60) {
			if (player0WideCopy) missile0ScanCounter = 7 + missile0ScanSpeed * 4;
		}
	}

	private void missile1ClockCounter() {
		if (++missile1Counter == 160) missile1Counter = 0;
		if (missile1ScanCounter >= 0) missile1ScanCounter -= missile1ScanSpeed;
		// Start scans 4 clocks before each copy. Scan is between 0 and 7, each pixel = 8 scan clocks
		if (missile1Counter == 156) {
			if (missile1RecentReset) missile1RecentReset = false;
			else missile1ScanCounter = 7 + missile1ScanSpeed * 4;
		}
		else if (missile1Counter == 12) {
			if (player1CloseCopy) missile1ScanCounter = 7 + missile1ScanSpeed * 4;
		}
		else if (missile1Counter == 28) {
			if (player1MediumCopy) missile1ScanCounter = 7 + missile1ScanSpeed * 4;
		}
		else if (missile1Counter == 60) {
			if (player1WideCopy) missile1ScanCounter = 7 + missile1ScanSpeed * 4;
		}
	}

	private void ballClockCounter() {
		if (++ballCounter == 160) ballCounter = 0;
		if (ballScanCounter >= 0) ballScanCounter -= ballScanSpeed;
		// The ball does not have copies and does not wait for the next scanline to start even if recently reset
		// Start scans 4 clocks before. Scan is between 0 and 7, each pixel = 8 scan clocks
		if (ballCounter == 156) ballScanCounter = 7 + ballScanSpeed * 4;
	}

	private void playfieldDelaySpriteChange(int part, int sprite) {
		observableChange();
		if (debug) debugPixel(DEBUG_PF_GR_COLOR);
		playfieldPerformDelayedSpriteChange(true);
		playfieldDelayedChangeClock = clock;
		playfieldDelayedChangePart = part;
		playfieldDelayedChangePattern = sprite;
	}

	private void playfieldPerformDelayedSpriteChange(boolean force) {
		// Only commits change if there is one and the delay has passed
		if (playfieldDelayedChangePart == -1) return;
		if (!force) {
			int dif = clock - playfieldDelayedChangeClock;
			if (dif == 0 || dif == 1) return;
		}
		observableChange();
		if 		(playfieldDelayedChangePart == 0) PF0 = playfieldDelayedChangePattern;
		else if	(playfieldDelayedChangePart == 1) PF1 = playfieldDelayedChangePattern;
		else if (playfieldDelayedChangePart == 2) PF2 = playfieldDelayedChangePattern;
		playfieldPatternInvalid = true;
		playfieldDelayedChangePart = -1;		// Marks the delayed change as nothing
	}

	private void playfieldUpdateCurrentPixel() {
		playfieldPerformDelayedSpriteChange(false);
		if (playfieldPatternInvalid) {
			playfieldPatternInvalid = false;
			// Shortcut if the Playfield is all clear
			if (PF0 == 0 && PF1 == 0 && PF2 == 0) {
				Arrays.fill(playfieldPattern, false);
				playfieldCurrentPixel = false;
				return;
			}
			int s, i;
			if (playfieldReflected) {
				s = 40; i = -1;
			} else {
				s = 19; i = 1;
			}
			playfieldPattern[0]  = playfieldPattern[s+=i] = (PF0 & 0x10) != 0;
			playfieldPattern[1]  = playfieldPattern[s+=i] = (PF0 & 0x20) != 0;
			playfieldPattern[2]  = playfieldPattern[s+=i] = (PF0 & 0x40) != 0;
			playfieldPattern[3]  = playfieldPattern[s+=i] = (PF0 & 0x80) != 0;
			playfieldPattern[4]  = playfieldPattern[s+=i] = (PF1 & 0x80) != 0;
			playfieldPattern[5]  = playfieldPattern[s+=i] = (PF1 & 0x40) != 0;
			playfieldPattern[6]  = playfieldPattern[s+=i] = (PF1 & 0x20) != 0;
			playfieldPattern[7]  = playfieldPattern[s+=i] = (PF1 & 0x10) != 0;
			playfieldPattern[8]  = playfieldPattern[s+=i] = (PF1 & 0x08) != 0;
			playfieldPattern[9]  = playfieldPattern[s+=i] = (PF1 & 0x04) != 0;
			playfieldPattern[10] = playfieldPattern[s+=i] = (PF1 & 0x02) != 0;
			playfieldPattern[11] = playfieldPattern[s+=i] = (PF1 & 0x01) != 0;
			playfieldPattern[12] = playfieldPattern[s+=i] = (PF2 & 0x01) != 0;
			playfieldPattern[13] = playfieldPattern[s+=i] = (PF2 & 0x02) != 0;
			playfieldPattern[14] = playfieldPattern[s+=i] = (PF2 & 0x04) != 0;
			playfieldPattern[15] = playfieldPattern[s+=i] = (PF2 & 0x08) != 0;
			playfieldPattern[16] = playfieldPattern[s+=i] = (PF2 & 0x10) != 0;
			playfieldPattern[17] = playfieldPattern[s+=i] = (PF2 & 0x20) != 0;
			playfieldPattern[18] = playfieldPattern[s+=i] = (PF2 & 0x40) != 0;
			playfieldPattern[19] = playfieldPattern[s+=i] = (PF2 & 0x80) != 0;
		}
		playfieldCurrentPixel = playfieldPattern[((clock - HBLANK_DURATION) >>> 2)];
	}

	private void playerDelaySpriteChange(int player, int sprite) {
		observableChange();
		if (debug) debugPixel(player == 0 ? DEBUG_P0_GR_COLOR : DEBUG_P1_GR_COLOR);
		if (playersDelayedSpriteChangesCount >= PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT) { 
			debugInfo(">>> Max player delayed changes reached: " + PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT); 
			return;
		}	
		playersDelayedSpriteChanges[playersDelayedSpriteChangesCount][0] = clock;
		playersDelayedSpriteChanges[playersDelayedSpriteChangesCount][1] = player;
		playersDelayedSpriteChanges[playersDelayedSpriteChangesCount][2] = sprite;
		playersDelayedSpriteChangesCount++;
	}

	private void playersPerformDelayedSpriteChanges() {
		if (playersDelayedSpriteChangesCount == 0 || playersDelayedSpriteChanges[0][0] == clock) return;
		for (int i = 0; i < playersDelayedSpriteChangesCount; i++) {
			int[] change = playersDelayedSpriteChanges[i];
			if (change[1] == 0) { 
				player0DelayedSprite = change[2];
				player1ActiveSprite = player1DelayedSprite; 
			} else { 
				player1DelayedSprite = change[2];
				player0ActiveSprite = player0DelayedSprite;
				ballEnabled = ballDelayedEnablement; 
			}
		}
		playersDelayedSpriteChangesCount = 0;
	}

	private void ballSetGraphic(int value) {
		observableChange();
		ballDelayedEnablement = (value & 0x02) != 0;
		if (!ballVerticalDelay) ballEnabled = ballDelayedEnablement;
	}

	private void player0SetShape(int shape) {
		observableChange();
		// Missile size
		int speed = shape & 0x30;
		if 		(speed == 0x00) speed = 8;		// Normal size = 8 = full speed = 1 pixel per clock
		else if	(speed == 0x10)	speed = 4;
		else if	(speed == 0x20) speed = 2;
		else if	(speed == 0x30)	speed = 1;
		if (missile0ScanSpeed != speed) {
			// if a copy is about to start, adjust for the new speed
			if (missile0ScanCounter > 7) missile0ScanCounter = 7 + (missile0ScanCounter - 7) / missile0ScanSpeed * speed;
			// if a copy is being scanned, kill the scan
			else if (missile0ScanCounter >= 0) missile0ScanCounter = -1;
			missile0ScanSpeed = speed;
		}
		// Player size and copies
		if ((shape & 0x07) == 0x05) {			// Double size = 1/2 speed
			speed = 2;
			player0CloseCopy = player0MediumCopy = player0WideCopy = false;	
		} else if ((shape & 0x07) == 0x07) {	// Quad size = 1/4 speed
			speed = 1;
			player0CloseCopy = player0MediumCopy = player0WideCopy = false;
		} else {
			speed = 4;							// Normal size = 4 = full speed = 1 pixel per clock
			player0CloseCopy =  (shape & 0x01) != 0;  
			player0MediumCopy = (shape & 0x02) != 0;  
			player0WideCopy =   (shape & 0x04) != 0;
		}
		if (player0ScanSpeed != speed) {
			// if a copy is about to start, adjust for the new speed
			if (player0ScanCounter > 31) player0ScanCounter = 31 + (player0ScanCounter - 31) / player0ScanSpeed * speed;
			// if a copy is being scanned, kill the scan
			else if (player0ScanCounter >= 0) player0ScanCounter = -1;
			player0ScanSpeed = speed;
		}
	}

	private void player1SetShape(int shape) {
		observableChange();
		// Missile size
		int speed = shape & 0x30;
		if 		(speed == 0x00) speed = 8;		// Normal size = 8 = full speed = 1 pixel per clock
		else if	(speed == 0x10)	speed = 4;
		else if	(speed == 0x20) speed = 2;
		else if	(speed == 0x30)	speed = 1;
		if (missile1ScanSpeed != speed) {
			// if a copy is about to start, adjust for the new speed
			if (missile1ScanCounter > 7) missile1ScanCounter = 7 + (missile1ScanCounter - 7) / missile1ScanSpeed * speed;
			// if a copy is being scanned, kill the scan
			else if (missile1ScanCounter >= 0) missile1ScanCounter = -1;
			missile1ScanSpeed = speed;
		}
		// Player size and copies
		if ((shape & 0x07) == 0x05) {			// Double size = 1/2 speed
			speed = 2;
			player1CloseCopy = player1MediumCopy = player1WideCopy = false;	
		} else if ((shape & 0x07) == 0x07) {	// Quad size = 1/4 speed
			speed = 1;
			player1CloseCopy = player1MediumCopy = player1WideCopy = false;
		} else {
			speed = 4;							// Normal size = 4 = full speed = 1 pixel per clock
			player1CloseCopy =  (shape & 0x01) != 0;  
			player1MediumCopy = (shape & 0x02) != 0;  
			player1WideCopy =   (shape & 0x04) != 0;
		}
		if (player1ScanSpeed != speed) {
			// if a copy is about to start, adjust to produce the same start position
			if (player1ScanCounter > 31) player1ScanCounter = 31 + (player1ScanCounter - 31) / player1ScanSpeed * speed;
			// if a copy is being scanned, kill the scan
			else if (player1ScanCounter >= 0) player1ScanCounter = -1;
			player1ScanSpeed = speed;
		}
	}

	private void playfieldAndBallSetShape(int shape) {
		observableChange();
		final boolean reflect = (shape & 0x01) != 0;
		if (playfieldReflected != reflect) {
			playfieldReflected = reflect;
			playfieldPatternInvalid = true;
		}
		playfieldScoreMode = (shape & 0x02) != 0;
		playfieldPriority = (shape & 0x04) != 0;
		int speed = shape & 0x30;
		if 		(speed == 0x00) speed = 8;		// Normal size = 8 = full speed = 1 pixel per clock
		else if	(speed == 0x10) speed = 4;
		else if	(speed == 0x20) speed = 2;
		else if	(speed == 0x30) speed = 1;
		if (ballScanSpeed != speed) {
			// if a copy is about to start, adjust for the new speed
			if (ballScanCounter > 7) ballScanCounter = 7 + (ballScanCounter - 7) / ballScanSpeed * speed;
			// if a copy is being scanned, kill the scan
			else if (ballScanCounter >= 0) ballScanCounter = -1;
			ballScanSpeed = speed;
		}
	}

	private void hitRESP0() {
		observableChangeExtended();
		if (debug) debugPixel(DEBUG_P0_RES_COLOR);

		// Hit in last pixel of HBLANK or after
		if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
			if (player0Counter != 155) player0RecentReset = true;				
			player0Counter = 155;
			return;
		}

		// Hit before last pixel of HBLANK
		int d = 0;									// No HMOVE, displacement = 0
		if (hMoveHitBlank) {						// With HMOVE
			if (clock >= HBLANK_DURATION)			// During extended HBLANK
				d = (HBLANK_DURATION - clock) + 8;
			else {
				d = (clock - hMoveHitClock - 4) >> 2;
				if (d > 8) d = 8;
			}
		}
		
		player0Counter = 157 - d;	
		player0RecentReset = player0Counter <= 155;
	}
	
	private void hitRESP1() {
		observableChangeExtended();
		if (debug) debugPixel(DEBUG_P1_RES_COLOR);

		// Hit in last pixel of HBLANK or after
		if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
			if (player1Counter != 155) player1RecentReset = true;				
			player1Counter = 155;
			return;
		}

		// Hit before last pixel of HBLANK
		int d = 0;									// No HMOVE, displacement = 0
		if (hMoveHitBlank) {						// With HMOVE
			if (clock >= HBLANK_DURATION)			// During extended HBLANK
				d = (HBLANK_DURATION - clock) + 8;
			else {
				d = (clock - hMoveHitClock - 4) >> 2;
				if (d > 8) d = 8;
			}
		}

		player1Counter = 157 - d;	
		player1RecentReset = player1Counter <= 155;
	}
	
	private void hitRESM0() {
		observableChangeExtended();
		if (debug) debugPixel(DEBUG_M0_COLOR);

		// Hit in last pixel of HBLANK or after
		if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
			if (missile0Counter != 155) missile0RecentReset = true;				
			missile0Counter = 155;
			return;
		}

		// Hit before last pixel of HBLANK
		int d = 0;									// No HMOVE, displacement = 0
		if (hMoveHitBlank) {						// With HMOVE
			if (clock >= HBLANK_DURATION)			// During extended HBLANK
				d = (HBLANK_DURATION - clock) + 8;
			else {
				d = (clock - hMoveHitClock - 4) >> 2;
				if (d > 8) d = 8;
			}
		}

		missile0Counter = 157 - d;	
		missile0RecentReset = missile0Counter <= 155;
	}
	
	private void hitRESM1() {
		observableChangeExtended();
		if (debug) debugPixel(DEBUG_M1_COLOR);

		// Hit in last pixel of HBLANK or after
		if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
			if (missile1Counter != 155) missile1RecentReset = true;				
			missile1Counter = 155;
			return;
		}

		// Hit before last pixel of HBLANK
		int d = 0;									// No HMOVE, displacement = 0
		if (hMoveHitBlank) {						// With HMOVE
			if (clock >= HBLANK_DURATION)			// During extended HBLANK
				d = (HBLANK_DURATION - clock) + 8;
			else {
				d = (clock - hMoveHitClock - 4) >> 2;
				if (d > 8) d = 8;
			}
		}

		missile1Counter = 157 - d;	
		missile1RecentReset = missile1Counter <= 155;
	}
	
	private void hitRESBL() {
		observableChange();
		if (debug) debugPixel(DEBUG_BL_COLOR);

		// Hit in last pixel of HBLANK or after
		if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
			ballCounter = 155;
			return;
		}

		// Hit before last pixel of HBLANK
		int d = 0;				// No HMOVE, displacement = 0
		if (hMoveHitBlank)		// With HMOVE
			if (clock >= HBLANK_DURATION)				// During extended HBLANK
				d = (HBLANK_DURATION - clock) + 8;
			else {
				d = (clock - hMoveHitClock - 4) >> 2;
				if (d > 8) d = 8;
			}

		ballCounter = 157 - d;	
	}
	
	private void hitHMOVE() {
		if (debug) debugPixel(DEBUG_HMOVE_COLOR);
		// Normal HMOVE
		if (clock < HBLANK_DURATION) {
			hMoveHitClock = clock;
			hMoveHitBlank = true;
			performHMOVE();
			return;
		}
		// Unsupported HMOVE
		if (clock < 219) {
			debugInfo("Unsupported HMOVE hit");
			return;
		}
		// Late HMOVE: Clocks [219-224] hide HMOVE blank next line, clocks [225, 0] produce normal behavior next line
		hMoveHitClock = 160 - clock;
		hMoveLateHit = true;
		hMoveLateHitBlank = clock >= 225;  
	}
	
	private void performHMOVE() {
		int add;
		boolean vis = false;
		add = (hMoveHitBlank ? HMP0 : HMP0 + 8); if (add != 0) { 
			vis = true;
			if (add > 0) for (int i = add; i > 0; i--) player0ClockCounter();
			else { 
				player0Counter += add; if (player0Counter < 0) player0Counter += 160; 
				if (player0ScanCounter >= 0) player0ScanCounter -= player0ScanSpeed * add; 
			}
		}
		add = (hMoveHitBlank ? HMP1 : HMP1 + 8); if (add != 0) {
			vis = true;
			if (add > 0) for (int i = add; i > 0; i--) player1ClockCounter();
			else { 
				player1Counter += add; if (player1Counter < 0) player1Counter += 160; 
				if (player1ScanCounter >= 0) player1ScanCounter -= player1ScanSpeed * add; 
			}
		}
		add = (hMoveHitBlank ? HMM0 : HMM0 + 8); if (add != 0) {
			vis = true;
			if (add > 0) for (int i = add; i > 0; i--) missile0ClockCounter();
			else { 
				missile0Counter += add; if (missile0Counter < 0) missile0Counter += 160; 
				if (missile0ScanCounter >= 0) missile0ScanCounter -= missile0ScanSpeed * add; 
			}
		}
		add = (hMoveHitBlank ? HMM1 : HMM1 + 8); if (add != 0) {
			vis = true;
			if (add > 0) for (int i = add; i > 0; i--) missile1ClockCounter();
			else { 
				missile1Counter += add; if (missile1Counter < 0) missile1Counter += 160;
				if (missile1ScanCounter >= 0) missile1ScanCounter -= missile1ScanSpeed * add; 
			}
		}
		add = (hMoveHitBlank ? HMBL : HMBL + 8); if (add != 0) {
			vis = true;
			if (add > 0) for (int i = add; i > 0; i--) ballClockCounter();
			else { 
				ballCounter += add; if (ballCounter < 0) ballCounter += 160;
				if (ballScanCounter >= 0) ballScanCounter -= ballScanSpeed * add;
			}
		}
		if (vis) observableChange();
	}	
	
	private void missile0SetResetToPlayer(int res) {
		observableChange();
		if (missile0ResetToPlayer = (res & 0x02) != 0) missile0Enabled = false;
	}

	private void missile1SetResetToPlayer(int res) {
		observableChange();
		if (missile1ResetToPlayer = (res & 0x02) != 0) missile1Enabled = false;
	}

	private void vBlankSet(int blank) {
		if (((blank & 0x02) != 0) != vBlankOn) {	// Start the delayed decode for vBlank state change
			vBlankDecodeActive = true;
			vBlankNewState = !vBlankOn;
		}
		if ((blank & 0x40) != 0) {
			controlsButtonsLatched = true;			// Enable Joystick Button latches
		} else {								
			controlsButtonsLatched = false;			// Disable latches and update registers with the current button state
			if (controlsJOY0ButtonPressed) INPT4 &= 0x7f; else INPT4 |= 0x80;
			if (controlsJOY1ButtonPressed) INPT5 &= 0x7f; else INPT5 |= 0x80;
		}
		if ((blank & 0x80) != 0) {					// Ground paddle capacitors
			paddleCapacitorsGrounded = true;
			paddle0CapacitorCharge = paddle1CapacitorCharge = 0;
			INPT0 &= 0x7f; INPT1 &= 0x7f; INPT2 &= 0x7f; INPT3 &= 0x7f;
		} else
			paddleCapacitorsGrounded = false;
	}

	private void vBlankClockDecode() {
		vBlankDecodeActive = false;
		vBlankOn = vBlankNewState;
		if (debug) debugPixel(DEBUG_VBLANK_COLOR);
		observableChange();
	}

	private void finishLine() {
	 	// Fills the extended HBLANK portion of the current line if needed
		if (hMoveHitBlank) {
			linePixels[HBLANK_DURATION] =
			linePixels[HBLANK_DURATION + 1] =
			linePixels[HBLANK_DURATION + 2] =
			linePixels[HBLANK_DURATION + 3] =
			linePixels[HBLANK_DURATION + 4] =
			linePixels[HBLANK_DURATION + 5] =
			linePixels[HBLANK_DURATION + 6] =
			linePixels[HBLANK_DURATION + 7] = hBlankColor;		// This is faster than Arrays.fill()
			hMoveHitBlank = false;
		}
		// Perform late HMOVE hit if needed
		if (hMoveLateHit) {
			hMoveLateHit = false;
			hMoveHitBlank = hMoveLateHitBlank;
			performHMOVE();
		}
		// Extend pixel computation to the entire next line if needed
		if (observableChangeExtended) {
			lastObservableChangeClock = 227;
			observableChangeExtended = false;
		}
		// Inject debugging information in the line if needed
		if (debugLevel >= 2) processDebugPixelsInLine();
	}
	
	private void observableChange() {
		lastObservableChangeClock = clock;
		if (repeatLastLine) repeatLastLine = false;	
	}

	private void observableChangeExtended() {
		observableChange();
		observableChangeExtended = true;	
	}

	private void debug(int level) {
		debugLevel = level > 4 ? 0 : level;
		debug = debugLevel != 0;
		videoOutput.showOSD(debug ? "Debug Level " + debugLevel : "Debug OFF", true);
		bus.cpu.debug = debug;
		bus.pia.debug = debug;
		if (debug) debugSetColors();
		else debugRestoreColors();
	}

	private void debugSetColors() {
		player0Color = DEBUG_P0_COLOR;
		player1Color = DEBUG_P1_COLOR;
		missile0Color = DEBUG_M0_COLOR;
		missile1Color = DEBUG_M1_COLOR;
		ballColor = DEBUG_BL_COLOR;
		playfieldColor = DEBUG_PF_COLOR;
		playfieldBackground = DEBUG_BK_COLOR;
		hBlankColor = debugLevel >= 1 ? DEBUG_HBLANK_COLOR : HBLANK_COLOR;
		vBlankColor = debugLevel >= 2 ? DEBUG_VBLANK_COLOR : VBLANK_COLOR;
	}

	private void debugRestoreColors() {
		hBlankColor = HBLANK_COLOR;
		vBlankColor = VBLANK_COLOR;
		playfieldBackground = palette[0];
		Arrays.fill(linePixels, hBlankColor);
		observableChange();
	}

	private void debugInfo(String str) {
		if (debug) System.out.printf("Line: %3d, Pixel: %3d, " + str + "\n", videoOutput.monitor().currentLine(), clock);
	}
	
	private void debugPixel(int color) {
		debugPixels[clock] = color;
	}
	
	private boolean debugPausedMoreFrames() {
		if (debugPauseMoreFrames <= 0) return false;
		debugPauseMoreFrames--;
		return true;
	}

	private void processDebugPixelsInLine() {
		Arrays.fill(linePixels, 0, HBLANK_DURATION, hBlankColor);
		if (debugLevel >= 4 && videoOutput.monitor().currentLine() % 10 == 0)
			for (int i = 0; i < LINE_WIDTH; i++) {
				if (debugPixels[i] != 0) continue;
				if (i < HBLANK_DURATION) {
					if (i % 6 == 0 || i == 66 || i == 63)
						debugPixels[i] = DEBUG_MARKS_COLOR;
				} else {
					if ((i - HBLANK_DURATION - 1) % 6 == 0)
						debugPixels[i] = DEBUG_MARKS_COLOR;
				}
			}
		if (debugLevel >= 3) 
			for (int i = 0; i < LINE_WIDTH; i++)
				if (debugPixels[i] != 0) {
					linePixels[i] = debugPixels[i];
					debugPixels[i] = 0;
				}
		observableChange();
	}
	
	private void paddlesChargeCapacitors() {
		if (INPT0 < 0x80 && ++paddle0CapacitorCharge >= paddle0Position) INPT0 |= 0x80;
		if (INPT1 < 0x80 && ++paddle1CapacitorCharge >= paddle1Position) INPT1 |= 0x80;
	}

	private void initLatchesAtPowerOn() {
		CXM0P = CXM1P = CXP0FB = CXP1FB = CXM0FB = CXM1FB = CXBLPF = CXPPMM = 0;
		INPT0 = INPT1 = INPT2 = INPT3 = 0;
		INPT4 = INPT5 = 0x80;
	}

	@Override
	public byte readByte(int address) {
		final int reg = address & READ_ADDRESS_MASK;

		if (reg == 0x00) return (byte) CXM0P;
		if (reg == 0x01) return (byte) CXM1P;
		if (reg == 0x02) return (byte) CXP0FB;
		if (reg == 0x03) return (byte) CXP1FB;
		if (reg == 0x04) return (byte) CXM0FB;
		if (reg == 0x05) return (byte) CXM1FB;
		if (reg == 0x06) return (byte) CXBLPF;
		if (reg == 0x07) return (byte) CXPPMM;
		if (reg == 0x08) return (byte) INPT0;
		if (reg == 0x09) return (byte) INPT1;
		if (reg == 0x0A) return (byte) INPT2;
		if (reg == 0x0B) return (byte) INPT3;
		if (reg == 0x0C) return (byte) INPT4;
		if (reg == 0x0D) return (byte) INPT5;

		// debugInfo(String.format("Invalid TIA read register address: %04x", address));
		return 0;
	}	

	@Override
	public void writeByte(int address, byte b) {
		final int i = b & 0xff;
		final int reg = address & WRITE_ADDRESS_MASK;
		
		if (reg == 0x1B) { /*GRP0   = i;*/ playerDelaySpriteChange(0, i); return; }
		if (reg == 0x1C) { /*GRP1   = i;*/ playerDelaySpriteChange(1, i); return; }
		if (reg == 0x02) { /*WSYNC  = i;*/ bus.cpu.RDY = false; if (debug) debugPixel(DEBUG_WSYNC_COLOR); return; } 	// <STROBE> Halts the CPU until the next HBLANK
		if (reg == 0x2A) { /*HMOVE  = i;*/ hitHMOVE();	return; }						   	
		if (reg == 0x0D) { if (PF0 != i || playfieldDelayedChangePart == 0) playfieldDelaySpriteChange(0, i); return; }
		if (reg == 0x0E) { if (PF1 != i || playfieldDelayedChangePart == 1) playfieldDelaySpriteChange(1, i); return; }
		if (reg == 0x0F) { if (PF2 != i || playfieldDelayedChangePart == 2) playfieldDelaySpriteChange(2, i); return; }
		if (reg == 0x06) { /*COLUP0 = i;*/ observableChange(); if (!debug) player0Color = missile0Color = palette[i]; return; }
		if (reg == 0x07) { /*COLUP1 = i;*/ observableChange(); if (!debug) player1Color = missile1Color = palette[i]; return; }
		if (reg == 0x08) { /*COLUPF = i;*/ observableChange(); if (!debug) playfieldColor = ballColor = palette[i]; return; }
		if (reg == 0x09) { /*COLUBK = i;*/ observableChange(); if (!debug) playfieldBackground = palette[i]; return; }
		if (reg == 0x1D) { /*ENAM0  = i;*/ observableChange(); missile0Enabled = (i & 0x02) != 0; return; }
		if (reg == 0x1E) { /*ENAM1  = i;*/ observableChange(); missile1Enabled = (i & 0x02) != 0; return; }
		if (reg == 0x14) { /*RESBL  = i;*/ hitRESBL(); return; }
		if (reg == 0x10) { /*RESP0  = i;*/ hitRESP0(); return; }
		if (reg == 0x11) { /*RESP1  = i;*/ hitRESP1(); return; }
		if (reg == 0x12) { /*RESM0  = i;*/ hitRESM0(); return; }
		if (reg == 0x13) { /*RESM1  = i;*/ hitRESM1(); return; }
		if (reg == 0x20) { HMP0   = (b >> 4); return; }
		if (reg == 0x21) { HMP1   = (b >> 4); return; }
		if (reg == 0x22) { HMM0   = (b >> 4); return; }
		if (reg == 0x23) { HMM1   = (b >> 4); return; }
		if (reg == 0x24) { HMBL   = (b >> 4); return; }
		if (reg == 0x2B) { /*HMCLR  = i;*/ HMP0 = HMP1 = HMM0 = HMM1 = HMBL = 0; return; }
		if (reg == 0x1F) { /*ENABL  = i;*/ ballSetGraphic(i); return; }
		if (reg == 0x04) { /*NUSIZ0 = i;*/ player0SetShape(i); return; }
		if (reg == 0x05) { /*NUSIZ1 = i;*/ player1SetShape(i); return; }
		if (reg == 0x0A) { /*CTRLPF = i;*/ playfieldAndBallSetShape(i); return; }
		if (reg == 0x0B) { /*REFP0  = i;*/ observableChange(); player0Reflected = (i & 0x08) != 0; return; }
		if (reg == 0x0C) { /*REFP1  = i;*/ observableChange(); player1Reflected = (i & 0x08) != 0; return; }
		if (reg == 0x25) { /*VDELP0 = i;*/ observableChange(); player0VerticalDelay = (i & 0x01) != 0; return; }
		if (reg == 0x26) { /*VDELP1 = i;*/ observableChange(); player1VerticalDelay = (i & 0x01) != 0; return; }
		if (reg == 0x27) { /*VDELBL = i;*/ observableChange(); ballVerticalDelay = (i & 0x01) != 0; return; }
		if (reg == 0x15) { AUDC0  = i; audioOutput.channel0().setControl(i & 0x0f); return; }
		if (reg == 0x16) { AUDC1  = i; audioOutput.channel1().setControl(i & 0x0f); return; }
		if (reg == 0x17) { AUDF0  = i; audioOutput.channel0().setDivider((i & 0x1f) + 1); return; }		// Bits 0-4, Divider from 1 to 32 )
		if (reg == 0x18) { AUDF1  = i; audioOutput.channel1().setDivider((i & 0x1f) + 1); return; }		// Bits 0-4, Divider from 1 to 32 )
		if (reg == 0x19) { AUDV0  = i; audioOutput.channel0().setVolume(i & 0x0f); return; }			// Bits 0-3, Volume from 0 to 15 )
		if (reg == 0x1A) { AUDV1  = i; audioOutput.channel1().setVolume(i & 0x0f); return; }			// Bits 0-3, Volume from 0 to 15 )
		if (reg == 0x28) { /*RESMP0 = i;*/ missile0SetResetToPlayer(i); return; }
		if (reg == 0x29) { /*RESMP1 = i;*/ missile1SetResetToPlayer(i); return; }
		if (reg == 0x01) { /*VBLANK = i;*/ vBlankSet(i); return; }
		if (reg == 0x00) { /*VSYNC  = i;*/ observableChange(); vSyncOn = (i & 0x02) != 0; if (debug) debugPixel(VSYNC_COLOR); return; }
		if (reg == 0x2C) { /*CXCLR  = i;*/ observableChange(); CXM0P = CXM1P = CXP0FB = CXP1FB = CXM0FB = CXM1FB = CXBLPF = CXPPMM = 0; return; }
		if (reg == 0x03) { /*RSYNC  = i;*/ /* clock = 0; */ return; }

		// debugInfo(String.format("Invalid TIA write register address: %04x value %d", address, b)); 
	}

	@Override
	public void controlStateChanged(ConsoleControls.Control control, boolean state) {
		switch (control) {
			case JOY0_BUTTON: 
				if (state) {
					controlsJOY0ButtonPressed = true;
					INPT4 &= 0x7f;
				} else {
					controlsJOY0ButtonPressed = false;
					if (!controlsButtonsLatched)			// Does not lift the button if Latched Mode is on
						INPT4 |= 0x80;
				}
				return;
			case JOY1_BUTTON:
				if (state) {
					controlsJOY1ButtonPressed = true;
					INPT5 &= 0x7f;
				} else {
					controlsJOY1ButtonPressed = false;
					if (!controlsButtonsLatched)			// Does not lift the button if Latched Mode is on
						INPT5 |= 0x80;
				}
				return;
		}
		// Toggles
		if (!state) return;
		switch (control) {
			case DEBUG:
				debug(debugLevel + 1); return;
			case NO_COLLISIONS:
				debugNoCollisions = !debugNoCollisions;
				videoOutput.showOSD(debugNoCollisions ? "Collisions OFF" : "Collisions ON", true);
				return;
			case PAUSE:
				debugPause = !debugPause; debugPauseMoreFrames = 0; 
				videoOutput.showOSD(debugPause ? "PAUSE" : "RESUME", true);
				return;
			case FRAME:
				debugPauseMoreFrames++; return;
			case TRACE:
				bus.cpu.trace = !bus.cpu.trace; return;
		}
	}

	@Override
	public void controlStateChanged(ConsoleControls.Control control, int position) {
		switch (control) {
			case PADDLE0_POSITION:
				paddle0Position = position; return;
			case PADDLE1_POSITION:
				paddle1Position = position; return;
		}
	}

	@Override
	public void controlsStateReport(Map<ConsoleControls.Control, Boolean> report) {
		//  No TIA controls visible outside by now
	}

	public TIAState saveState() {
		TIAState state = new TIAState();
		state.linePixels					   =  linePixels.clone();
		state.lastObservableChangeClock		   =  lastObservableChangeClock;
		state.observableChangeExtended		   =  observableChangeExtended;
		state.repeatLastLine 				   =  repeatLastLine;
		state.vSyncOn                     	   =  vSyncOn;                    
		state.vBlankOn                    	   =  vBlankOn;
		state.vBlankDecodeActive			   =  vBlankDecodeActive;
		state.vBlankNewState				   =  vBlankNewState;
		state.playfieldPattern            	   =  playfieldPattern.clone();
		state.playfieldPatternInvalid     	   =  playfieldPatternInvalid;    
		state.playfieldCurrentPixel       	   =  playfieldCurrentPixel;      
		state.playfieldColor              	   =  playfieldColor;             
		state.playfieldBackground         	   =  playfieldBackground;        
		state.playfieldReflected          	   =  playfieldReflected;         
		state.playfieldScoreMode          	   =  playfieldScoreMode;         
		state.playfieldPriority           	   =  playfieldPriority;          
		state.player0ActiveSprite         	   =  player0ActiveSprite;        
		state.player0DelayedSprite        	   =  player0DelayedSprite;
		state.player0Color                	   =  player0Color;               
		state.player0RecentReset     	  	   =  player0RecentReset;      
		state.player0Counter	          	   =  player0Counter;	         
		state.player0ScanCounter	      	   =  player0ScanCounter;	     
		state.player0ScanSpeed            	   =  player0ScanSpeed;           
		state.player0VerticalDelay        	   =  player0VerticalDelay;       
		state.player0CloseCopy            	   =  player0CloseCopy;           
		state.player0MediumCopy           	   =  player0MediumCopy;
		state.player0WideCopy             	   =  player0WideCopy;            
		state.player0Reflected            	   =  player0Reflected;
		state.player1ActiveSprite         	   =  player1ActiveSprite;        
		state.player1DelayedSprite        	   =  player1DelayedSprite;
		state.player1Color  	           	   =  player1Color;               
		state.player1RecentReset			   =  player1RecentReset;      
		state.player1Counter              	   =  player1Counter;             
		state.player1ScanCounter		  	   =  player1ScanCounter;						
		state.player1ScanSpeed			  	   =  player1ScanSpeed;						
		state.player1VerticalDelay        	   =  player1VerticalDelay;       
		state.player1CloseCopy            	   =  player1CloseCopy;           
		state.player1MediumCopy           	   =  player1MediumCopy;
		state.player1WideCopy             	   =  player1WideCopy;            
		state.player1Reflected            	   =  player1Reflected;
		state.missile0Enabled             	   =  missile0Enabled;            
		state.missile0Color               	   =  missile0Color;              
		state.missile0RecentReset   	   	   =  missile0RecentReset;     
		state.missile0Counter             	   =  missile0Counter;            
		state.missile0ScanCounter         	   =  missile0ScanCounter;        
		state.missile0ScanSpeed			  	   =  missile0ScanSpeed;						
		state.missile0ResetToPlayer		  	   =  missile0ResetToPlayer;					
		state.missile1Enabled             	   =  missile1Enabled;            
		state.missile1Color               	   =  missile1Color;              
		state.missile1RecentReset   	   	   =  missile1RecentReset;     
		state.missile1Counter             	   =  missile1Counter;            
		state.missile1ScanCounter         	   =  missile1ScanCounter;        
		state.missile1ScanSpeed			  	   =  missile1ScanSpeed;						
		state.missile1ResetToPlayer		  	   =  missile1ResetToPlayer;					
		state.ballEnabled                 	   =  ballEnabled;                
		state.ballDelayedEnablement       	   =  ballDelayedEnablement;      
		state.ballColor                   	   =  ballColor;                  
		state.ballCounter                 	   =  ballCounter;                
		state.ballScanCounter             	   =  ballScanCounter;            
		state.ballScanSpeed				  	   =  ballScanSpeed;						
		state.ballVerticalDelay           	   =  ballVerticalDelay;          
		state.playfieldDelayedChangeClock	   =  playfieldDelayedChangeClock;
		state.playfieldDelayedChangePart	   =  playfieldDelayedChangePart;
		state.playfieldDelayedChangePattern	   =  playfieldDelayedChangePattern;
		state.playersDelayedSpriteChanges      =  ArraysCopy.copy2D(playersDelayedSpriteChanges);
		state.playersDelayedSpriteChangesCount =  playersDelayedSpriteChangesCount;
		state.hMoveHitBlank					   =  hMoveHitBlank;
		state.hMoveHitClock					   =  hMoveHitClock;
		state.PF0						  	   =  PF0;	  
		state.PF1						  	   =  PF1;
		state.PF2						  	   =  PF2;  		
		state.AUDC0						  	   =  AUDC0;
		state.AUDC1						  	   =  AUDC1;
		state.AUDF0						  	   =  AUDF0;
		state.AUDF1						  	   =  AUDF1;
		state.AUDV0						  	   =  AUDV0;
		state.AUDV1						  	   =  AUDV1;
		state.HMP0						  	   =  HMP0;
		state.HMP1						  	   =  HMP1;  	
		state.HMM0						  	   =  HMM0;  	
		state.HMM1						  	   =  HMM1;  	
		state.HMBL						  	   =  HMBL;  	
		state.CXM0P 					  	   =  CXM0P;  
		state.CXM1P 					  	   =  CXM1P;  
		state.CXP0FB 					  	   =  CXP0FB;
		state.CXP1FB 					  	   =  CXP1FB;
		state.CXM0FB 					  	   =  CXM0FB;
		state.CXM1FB 					  	   =  CXM1FB;
		state.CXBLPF 					  	   =  CXBLPF;
		state.CXPPMM 					  	   =  CXPPMM;
		return state;
	}

	public void loadState(TIAState state) {
		linePixels						 =  state.linePixels;
		lastObservableChangeClock		 =	state.lastObservableChangeClock;
		observableChangeExtended		 =  state.observableChangeExtended;
		repeatLastLine 					 =	state.repeatLastLine;
		vSyncOn                     	 =  state.vSyncOn;                     
		vBlankOn                    	 =  state.vBlankOn;
		vBlankDecodeActive				 =  state.vBlankDecodeActive;
		vBlankNewState				 	 =  state.vBlankNewState;
		playfieldPattern            	 =  state.playfieldPattern;            
		playfieldPatternInvalid     	 =  state.playfieldPatternInvalid;     
		playfieldCurrentPixel       	 =  state.playfieldCurrentPixel;       
		playfieldColor              	 =  state.playfieldColor;              
		playfieldBackground         	 =  state.playfieldBackground;         
		playfieldReflected          	 =  state.playfieldReflected;          
		playfieldScoreMode          	 =  state.playfieldScoreMode;          
		playfieldPriority           	 =  state.playfieldPriority;           
		player0ActiveSprite         	 =  state.player0ActiveSprite;         
		player0DelayedSprite        	 =  state.player0DelayedSprite;
		player0Color                	 =  state.player0Color;                
		player0RecentReset       	 	 =  state.player0RecentReset;       
		player0Counter	            	 =  state.player0Counter;	          
		player0ScanCounter	        	 =  state.player0ScanCounter;	      
		player0ScanSpeed            	 =  state.player0ScanSpeed;            
		player0VerticalDelay        	 =  state.player0VerticalDelay;        
		player0CloseCopy            	 =  state.player0CloseCopy;            
		player0MediumCopy           	 =  state.player0MediumCopy;
		player0WideCopy             	 =  state.player0WideCopy;             
		player0Reflected            	 =  state.player0Reflected;
		player1ActiveSprite         	 =  state.player1ActiveSprite;         
		player1DelayedSprite        	 =  state.player1DelayedSprite;
		player1Color                	 =  state.player1Color;                
		player1RecentReset       		 =  state.player1RecentReset;       
		player1Counter              	 =  state.player1Counter;              
		player1ScanCounter				 =  state.player1ScanCounter;		 	
		player1ScanSpeed				 =  state.player1ScanSpeed;			 	
		player1VerticalDelay        	 =  state.player1VerticalDelay;        
		player1CloseCopy            	 =  state.player1CloseCopy;            
		player1MediumCopy           	 =  state.player1MediumCopy;
		player1WideCopy             	 =  state.player1WideCopy;             
		player1Reflected            	 =  state.player1Reflected;
		missile0Enabled             	 =  state.missile0Enabled;             
		missile0Color               	 =  state.missile0Color;               
		missile0RecentReset      	 	 =  state.missile0RecentReset;      
		missile0Counter             	 =  state.missile0Counter;             
		missile0ScanCounter         	 =  state.missile0ScanCounter;         
		missile0ScanSpeed				 =  state.missile0ScanSpeed;			 	
		missile0ResetToPlayer			 =  state.missile0ResetToPlayer;		 	
		missile1Enabled             	 =  state.missile1Enabled;             
		missile1Color               	 =  state.missile1Color;               
		missile1RecentReset      	 	 =  state.missile1RecentReset;      
		missile1Counter             	 =  state.missile1Counter;             
		missile1ScanCounter         	 =  state.missile1ScanCounter;         
		missile1ScanSpeed				 =  state.missile1ScanSpeed;			 	
		missile1ResetToPlayer			 =  state.missile1ResetToPlayer;		 	
		ballEnabled                 	 =  state.ballEnabled;                 
		ballDelayedEnablement       	 =  state.ballDelayedEnablement;       
		ballColor                   	 =  state.ballColor;                   
		ballCounter                 	 =  state.ballCounter;                 
		ballScanCounter             	 =  state.ballScanCounter;             
		ballScanSpeed					 =  state.ballScanSpeed;				 
		ballVerticalDelay           	 =  state.ballVerticalDelay;           
		playfieldDelayedChangeClock		 =  state.playfieldDelayedChangeClock;
		playfieldDelayedChangePart		 =  state.playfieldDelayedChangePart;
		playfieldDelayedChangePattern	 =  state.playfieldDelayedChangePattern;
		playersDelayedSpriteChanges      =  state.playersDelayedSpriteChanges;      
		playersDelayedSpriteChangesCount =  state.playersDelayedSpriteChangesCount;
		hMoveHitBlank					 =  state.hMoveHitBlank;
		hMoveHitClock					 =  state.hMoveHitClock;
		PF0								 =  state.PF0;
		PF1								 =  state.PF1;
		PF2								 =  state.PF2;
		AUDC0							 =  state.AUDC0; audioOutput.channel0().setControl(AUDC0 & 0x0f);		// Also update the Audio Generator
		AUDC1							 =  state.AUDC1; audioOutput.channel1().setControl(AUDC1 & 0x0f);
		AUDF0							 =  state.AUDF0; audioOutput.channel0().setDivider((AUDF0 & 0x1f) + 1); 	
		AUDF1							 =  state.AUDF1; audioOutput.channel1().setDivider((AUDF1 & 0x1f) + 1); 
		AUDV0							 =  state.AUDV0; audioOutput.channel0().setVolume(AUDV0 & 0x0f);
		AUDV1							 =  state.AUDV1; audioOutput.channel1().setVolume(AUDV1 & 0x0f);
		HMP0							 =  state.HMP0;
		HMP1							 =  state.HMP1;		
		HMM0							 =  state.HMM0;
		HMM1							 =  state.HMM1;		
		HMBL							 =  state.HMBL;
		CXM0P 							 =  state.CXM0P; 
		CXM1P 							 =  state.CXM1P; 
		CXP0FB							 =  state.CXP0FB;
		CXP1FB							 =  state.CXP1FB;
		CXM0FB							 =  state.CXM0FB;
		CXM1FB							 =  state.CXM1FB;
		CXBLPF							 =  state.CXBLPF;
		CXPPMM							 =  state.CXPPMM;
		if (debug) debugSetColors();						// IF debug is on, ensure debug colors are used
	}
	
	
	// Variables ----------------------------------------------
	
	private final VideoGenerator videoOutput;
	private final AudioMonoGenerator audioOutput;
	
	private int clock = 0;

	private BUS bus;

	private boolean powerOn = false;
	private final int debugPixels[] = new int[LINE_WIDTH];
	
	private int[] palette;
	private int vSyncColor = VSYNC_COLOR;
	private int vBlankColor = VBLANK_COLOR;
	private int hBlankColor = VBLANK_COLOR;

	private boolean debugPause = false;
	private int debugPauseMoreFrames = 0;
	
	private int[] framePattern = { 1 };
	private int   framePatternPosition = 0;
	

	// State Variables ----------------------------------------------

	private boolean debug = false;
	private int debugLevel = 0;
	private boolean debugNoCollisions = false;

	private int[] linePixels = new int[LINE_WIDTH];
	private int lastObservableChangeClock = -1;
	private boolean observableChangeExtended = false;
	private boolean repeatLastLine;

	private boolean vSyncOn = false;

	private boolean vBlankOn = false;
	private boolean vBlankDecodeActive = false;
	private boolean vBlankNewState;

	private boolean hMoveHitBlank = false;
	private int hMoveHitClock = -1;
	private boolean hMoveLateHit = false;
	private boolean hMoveLateHitBlank = false;
	
	private boolean[] playfieldPattern = new boolean[40];
	private boolean playfieldPatternInvalid = true;
	private boolean playfieldCurrentPixel = false;
	private int playfieldColor = 0xff000000;
	private int playfieldBackground = 0xff000000;
	private boolean playfieldReflected = false;
	private boolean playfieldScoreMode = false;
	private boolean playfieldPriority = false;

	private int playfieldDelayedChangeClock = -1;
	private int playfieldDelayedChangePart = -1;			// Supports only one delayed change at a time.
	private int playfieldDelayedChangePattern = -1;
	
	private int player0ActiveSprite = 0;
	private int player0DelayedSprite = 0;
	private int player0Color = 0xff000000;
	private boolean player0RecentReset = false;
	private int player0Counter = 0;							// Position!	
	private int player0ScanCounter = -1;					// 31 down to 0. Current scan position. Negative = scan not happening	
	private int player0ScanSpeed = 4;						// Decrement ScanCounter. 4 per clock = 1 pixel wide
	private boolean player0VerticalDelay = false;
	private boolean player0CloseCopy = false;
	private boolean player0MediumCopy = false;
	private boolean player0WideCopy = false;
	private boolean player0Reflected = false;
	
	private int player1ActiveSprite = 0;
	private int player1DelayedSprite = 0;
	private int player1Color = 0xff000000;
	private boolean player1RecentReset = false;
	private int player1Counter = 0;
	private int player1ScanCounter = -1;
	private int player1ScanSpeed = 4;
	private boolean player1VerticalDelay = false;
	private boolean player1CloseCopy = false;
	private boolean player1MediumCopy = false;
	private boolean player1WideCopy = false;
	private boolean player1Reflected = false;
	
	private boolean missile0Enabled = false;
	private int missile0Color = 0xff000000;
	private boolean missile0RecentReset = false;
	private int missile0Counter = 0;
	private int missile0ScanCounter = -1;
	private int missile0ScanSpeed = 8;						// 8 per clock = 1 pixel wide
	private boolean missile0ResetToPlayer = false;

	private boolean missile1Enabled = false;
	private int missile1Color = 0xff000000;
	private boolean missile1RecentReset = false;
	private int missile1Counter = 0;
	private int missile1ScanCounter = -1;
	private int missile1ScanSpeed = 8;
	private boolean missile1ResetToPlayer = false;
	
	private boolean ballEnabled = false;
	private boolean ballDelayedEnablement = false;
	private int ballColor = 0xff000000;
	private int ballCounter = 0;
	private int ballScanCounter = -1;
	private int ballScanSpeed = 8;							// 8 per clock = 1 pixel wide			
	private boolean ballVerticalDelay = false;

	private int[][] playersDelayedSpriteChanges = new int[PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT][3];
	private int playersDelayedSpriteChangesCount = 0;

	private boolean controlsButtonsLatched = false;
	private boolean controlsJOY0ButtonPressed = false;
	private boolean controlsJOY1ButtonPressed = false;

	private boolean paddleCapacitorsGrounded = false;
	private int paddle0Position = -1;			// 380 = Left, 190 = Middle, 0 = Right. -1 = disconnected, won't charge POTs
	private int paddle0CapacitorCharge = 0;
	private int paddle1Position = -1;
	private int paddle1CapacitorCharge = 0;

	// Read registers -------------------------------------------

	private int CXM0P; 		// collision M0-P1, M0-P0 (Bit 7,6)
	private int CXM1P; 		// collision M1-P0, M1-P1
	private int CXP0FB;		// collision P0-PF, P0-BL
	private int CXP1FB;		// collision P1-PF, P1-BL
	private int CXM0FB;		// collision M0-PF, M0-BL
	private int CXM1FB;		// collision M1-PF, M1-BL
	private int CXBLPF;		// collision BL-PF, unused
	private int CXPPMM;		// collision P0-P1, M0-M1
	private int INPT0; 		// Paddle0 Left pot port
	private int INPT1; 		// Paddle0 Right pot port
	private int INPT2; 		// Paddle1 Left pot port
	private int INPT3; 		// Paddle1 Right pot port
	private int INPT4; 		// input (Joy0 button)
	private int INPT5; 		// input (Joy1 button)

	
	// Write registers -------------------------------------------

//	private int VSYNC;		// ......1.  vertical sync set-clear
//	private int VBLANK;		// 11....1.  vertical blank set-clear
//	private int WSYNC;		// <strobe>  wait for leading edge of horizontal blank
//	private int RSYNC;		// <strobe>  reset horizontal sync counter
//	private int NUSIZ0;		// ..111111  number-size player-missile 0
//	private int NUSIZ1;		// ..111111  number-size player-missile 1
//	private int COLUP0;		// 1111111.  color-lum player 0 and missile 0
//	private int COLUP1;		// 1111111.  color-lum player 1 and missile 1
//	private int COLUPF;		// 1111111.  color-lum playfield and ball
//	private int COLUBK;		// 1111111.  color-lum background
//	private int CTRLPF;		// ..11.111  control playfield ball size & collisions
//	private int REFP0;		// ....1...  reflect player 0
//	private int REFP1;		// ....1...  reflect player 1
	private int PF0;		// 1111....  playfield register byte 0
	private int PF1;		// 11111111  playfield register byte 1
	private int PF2;		// 11111111  playfield register byte 2
//	private int RESP0;		// <strobe>  reset player 0
//	private int RESP1;		// <strobe>  reset player 1
//	private int RESM0;		// <strobe>  reset missile 0
//	private int RESM1;		// <strobe>  reset missile 1
//	private int RESBL;		// <strobe>  reset ball
	private int AUDC0;		// ....1111  audio control 0
	private int AUDC1;		// ....1111  audio control 1
	private int AUDF0;		// ...11111  audio frequency 0
	private int AUDF1;		// ...11111  audio frequency 1
	private int AUDV0;		// ....1111  audio volume 0
	private int AUDV1;		// ....1111  audio volume 1
//	private int GRP0;		// 11111111  graphics player 0
//	private int GRP1;		// 11111111  graphics player 1
//	private int ENAM0;		// ......1.  graphics (enable) missile 0
//	private int ENAM1;		// ......1.  graphics (enable) missile 1
//	private int ENABL;		// ......1.  graphics (enable) ball
	private int HMP0;		// 1111....  horizontal motion player 0
	private int HMP1;		// 1111....  horizontal motion player 1
	private int HMM0;		// 1111....  horizontal motion missile 0
	private int HMM1;		// 1111....  horizontal motion missile 1
	private int HMBL;		// 1111....  horizontal motion ball
//	private int VDELP0;		// .......1  vertical delay player 0
//	private int VDELP1;		// .......1  vertical delay player 1
//	private int VDELBL;		// .......1  vertical delay ball
//	private int RESMP0;		// ......1.  reset missile 0 to player 0
//	private int RESMP1;		// ......1.  reset missile 1 to player 1
//	private int HMOVE;		// <strobe>  apply horizontal motion
//	private int HMCLR;		// <strobe>  clear horizontal motion registers
//	private int CXCLR;		// <strobe>  clear collision latches


	// Constants --------------------------------------------------
	
	public static final int CHIP_MASK = 0x1080;
	public static final int CHIP_SELECT = 0x0000;
	
	private static final int READ_ADDRESS_MASK  = 0x000f;
	private static final int WRITE_ADDRESS_MASK = 0x003f;
	
	private static final int VBLANK_COLOR = 0x00000000;		// Full transparency needed for CRT emulation modes
	private static final int HBLANK_COLOR = 0xff000000;
	private static final int VSYNC_COLOR = 0xffdddddd;

	private static final int HBLANK_DURATION = 68;
	private static final int LINE_WIDTH = 228;

	private static final int DEBUG_MARKS_COLOR  = 0xff202020;
	private static final int DEBUG_HBLANK_COLOR = 0xff444444;
	private static final int DEBUG_VBLANK_COLOR = 0xff2a2a2a;
	
	private static final int DEBUG_WSYNC_COLOR  = 0xff880088;
	private static final int DEBUG_HMOVE_COLOR  = 0xffffffff;

	private static final int DEBUG_P0_COLOR     = 0xff0000ff;
	private static final int DEBUG_P0_RES_COLOR = 0xff2222bb;
	private static final int DEBUG_P0_GR_COLOR  = 0xff111177;
	private static final int DEBUG_P1_COLOR     = 0xffff0000;
	private static final int DEBUG_P1_RES_COLOR = 0xffbb2222;
	private static final int DEBUG_P1_GR_COLOR  = 0xff771111;
	private static final int DEBUG_M0_COLOR     = 0xff6666ff;
	private static final int DEBUG_M1_COLOR     = 0xffff6666;

	private static final int DEBUG_PF_COLOR     = 0xff448844;
	private static final int DEBUG_PF_GR_COLOR  = 0xff33dd33;
	private static final int DEBUG_BK_COLOR     = 0xff334433;
	private static final int DEBUG_BL_COLOR     = 0xffffff00;

	@SuppressWarnings("unused")
	private static final int DEBUG_SP_COLOR  	= 0xff00ffff;
	@SuppressWarnings("unused")
	private static final int DEBUG_SP_COLOR2 	= 0xffff00ff;

	private static final int PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT = 50;  // Supports a maximum of player GR changes before any is drawn
	
	private static final double FORCED_CLOCK = Parameters.TIA_FORCED_CLOCK;	//  TIA Real Clock = NTSC clock = 3584160 or 3579545 Hz


	// Used to save/load states
	public static class TIAState implements Serializable {
		int[] linePixels;
		int lastObservableChangeClock;
		boolean observableChangeExtended;
		boolean repeatLastLine;
		boolean vSyncOn;
		boolean vBlankOn;
		boolean vBlankDecodeActive;
		boolean vBlankNewState;
		boolean[] playfieldPattern;
		boolean playfieldPatternInvalid;
		boolean playfieldCurrentPixel;
		int playfieldColor;
		int playfieldBackground;
		boolean playfieldReflected;
		boolean playfieldScoreMode;
		boolean playfieldPriority;
		int player0ActiveSprite;
		int player0DelayedSprite;
		int player0Color;
		boolean player0RecentReset;
		int player0Counter;	
		int player0ScanCounter;	
		int player0ScanSpeed;
		boolean player0VerticalDelay;
		boolean player0CloseCopy;
		boolean player0MediumCopy;
		boolean player0WideCopy;
		boolean player0Reflected;
		int player1ActiveSprite;
		int player1DelayedSprite;
		int player1Color;
		boolean player1RecentReset;
		int player1Counter;
		int player1ScanCounter;						
		int player1ScanSpeed;						
		boolean player1VerticalDelay;
		boolean player1CloseCopy;
		boolean player1MediumCopy;
		boolean player1WideCopy;
		boolean player1Reflected;
		boolean missile0Enabled;
		int missile0Color;
		boolean missile0RecentReset;
		int missile0Counter;
		int missile0ScanCounter;
		int missile0ScanSpeed;						
		boolean missile0ResetToPlayer;					
		boolean missile1Enabled;
		int missile1Color;
		boolean missile1RecentReset;
		int missile1Counter;
		int missile1ScanCounter;
		int missile1ScanSpeed;						
		boolean missile1ResetToPlayer;					
		boolean ballEnabled;
		boolean ballDelayedEnablement;
		int ballColor;
		int ballCounter;
		int ballScanCounter;
		int ballScanSpeed;						
		boolean ballVerticalDelay;
		int playfieldDelayedChangeClock;
		int playfieldDelayedChangePart;
		int playfieldDelayedChangePattern;
		int[][] playersDelayedSpriteChanges;
		int playersDelayedSpriteChangesCount;
		boolean hMoveHitBlank;
		int hMoveHitClock;
		int PF0;
		int PF1;
		int PF2;
		int AUDC0;
		int AUDC1;
		int AUDF0;		
		int AUDF1;
		int AUDV0;
		int AUDV1;
		int HMP0;
		int HMP1;		
		int HMM0;
		int HMM1;		
		int HMBL;
		int CXM0P; 
		int CXM1P; 
		int CXP0FB;
		int CXP1FB;
		int CXM0FB;
		int CXM1FB;
		int CXBLPF;
		int CXPPMM;

		public static final long serialVersionUID = 3L;
	}

}
