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

package org.javatari.atari.console;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.javatari.atari.board.BUS;
import org.javatari.atari.cartridge.Cartridge;
import org.javatari.atari.cartridge.CartridgeDatabase;
import org.javatari.atari.cartridge.CartridgeFormatOption;
import org.javatari.atari.cartridge.CartridgeInsertionListener;
import org.javatari.atari.cartridge.CartridgeSocket;
import org.javatari.atari.cartridge.ROMFormatUnsupportedException;
import org.javatari.atari.cartridge.formats.CartridgeSavestate;
import org.javatari.atari.console.savestate.ConsoleState;
import org.javatari.atari.console.savestate.SaveStateMedia;
import org.javatari.atari.console.savestate.SaveStateSocket;
import org.javatari.atari.controls.ConsoleControls;
import org.javatari.atari.controls.ConsoleControls.Control;
import org.javatari.atari.controls.ConsoleControlsInput;
import org.javatari.atari.controls.ConsoleControlsSocket;
import org.javatari.atari.pia.PIA;
import org.javatari.atari.pia.RAM;
import org.javatari.atari.tia.TIA;
import org.javatari.general.av.audio.AudioSignal;
import org.javatari.general.av.video.VideoSignal;
import org.javatari.general.av.video.VideoStandard;
import org.javatari.general.board.Clock;
import org.javatari.general.m6502.M6502;
import org.javatari.parameters.Parameters;


public class Console {

	public Console() {
		mainComponentsCreate();
		socketsCreate();
		mainClockCreate();
		videoStandardAuto();
	}

	public VideoSignal videoOutput() {
		return tia.videoOutput();
	}

	public AudioSignal audioOutput() {
		return tia.audioOutput();
	}

	public ConsoleControlsSocket controlsSocket() {
		return controlsSocket;
	}
	
	public CartridgeSocket cartridgeSocket() { 
		return cartridgeSocket;
	}
	
	public SaveStateSocket saveStateSocket() { 
		return saveStateSocket;
	}
	
	public void powerOn() {
		if (powerOn) powerOff();
		cpu.powerOn();
		bus.powerOn();
		powerOn = true;
		controlsSocket.controlsStatesRedefined();
		go();
		videoStandardAutoDetectionStart();
	}

	public void powerOff() {
		pause();
		bus.powerOff();
		cpu.powerOff();
		powerOn = false;
		controlsSocket.controlsStatesRedefined();
	}

	public void extendedPowerOff() {
		powerOff();
	}
	
	public void destroy() {
		extendedPowerOff();
		mainClockDestroy();
	}
	
	public void showOSD(String message, boolean overlap) {
		tia.videoOutput().showOSD(message, overlap);
	}
	
	public VideoStandard videoStandard() {
		return videoStandard;
	}
		
	public void videoStandard(VideoStandard videoStandard) {
		if (videoStandard != this.videoStandard) {
			this.videoStandard = videoStandard;
			tia.videoStandard(this.videoStandard);
			mainClockAdjustToNormal();
		}
		showOSD((videoStandardAuto ? "AUTO: " : "") + videoStandard.toString(), false);
	}

	public void go() {
		mainClock.go();
	}
	
	public void pause() {
		mainClock.pause();
	}
	
	// For debug purposes
	public Clock mainClock() {
		return mainClock;
	}

	protected Cartridge cartridge() {
		return bus.cartridge;
	}

	protected void cartridge(Cartridge cartridge) {
		controlsSocket.removeForwardedInput(cartridge());
		bus.cartridge(cartridge);
		cartridgeSocket.cartridgeInserted(cartridge);
		if (cartridge != null) {
			controlsSocket.addForwardedInput(cartridge);
			saveStateSocket.connectCartridge(cartridge);
		}
	}

	protected void videoStandardAuto() {
		videoStandardAuto = true;
		if (powerOn) videoStandardAutoDetectionStart();
		else videoStandard(VideoStandard.NTSC);
	}

	protected void videoStandardAutoDetectionStart() {
		if (!videoStandardAuto || videoStandardAutoDetectionInProgress) return;
		// If no Cartridge present, use NTSC
		if (bus.cartridge == null) {
			videoStandard(VideoStandard.NTSC);
			return;
		}
		// Otherwise use the VideoStandard detected by the monitor
		if (tia.videoOutput().monitor() == null) return;
		videoStandardAutoDetectionInProgress = true;
		tia.videoOutput().monitor().videoStandardDetectionStart();
		// TODO This thread is a source of indeterminism. Potential problem in multiplayer sync
		new Thread("Console VideoStd Detection") { public void run() {
			VideoStandard std;
			int tries = 0;
			do {
				try { Thread.sleep(20); } catch (InterruptedException e) {};
				std = tia.videoOutput().monitor().videoStandardDetected();
			} while (std == null && ++tries < 1500/20);
			if (std != null) videoStandard(std);
			else showOSD("AUTO: FAILED", false);
			videoStandardAutoDetectionInProgress = false;
		}}.start();
	}

	protected void videoStandardForced(VideoStandard forcedVideoStandard) {
		videoStandardAuto = false;
		videoStandard(forcedVideoStandard);
	}

	protected void mainComponentsCreate() {
		cpu = new M6502();
		tia = new TIA();
		pia = new PIA();
		ram = new RAM();
		bus = new BUS(cpu, tia, pia, ram);
	}

	protected void mainClockCreate() {
		mainClock = new Clock("Console(TIA)", tia, 0);
	}

	protected void mainClockAdjustToNormal() {
		double fps = tia.desiredClockForVideoStandard();
		mainClock.speed(fps);
		tia.audioOutput().fps(fps);
	}

	protected void mainClockAdjustToAlternate() {
		double fps = tia.desiredClockForVideoStandard() * ALTERNATE_CLOCK_FACTOR;
		mainClock.speed(fps);
		tia.audioOutput().fps(fps);
	}

	protected void mainClockDestroy() {
		mainClock.terminate();
	}
	
	protected void socketsCreate() {
		controlsSocket = new ConsoleControlsSocket();
		controlsSocket.addForwardedInput(new ConsoleControlsInputAdapter());
		controlsSocket.addForwardedInput(tia);
		controlsSocket.addForwardedInput(pia);
		cartridgeSocket = new CartridgeSocketAdapter();
		saveStateSocket = new SaveStateSocketAdapter();
	}

	protected void loadState(ConsoleState state) {
		tia.loadState(state.tiaState);
		pia.loadState(state.piaState);
		ram.loadState(state.ramState);
		cpu.loadState(state.cpuState);
		cartridge(state.cartridge);
		videoStandard(state.videoStandard);
		controlsSocket.controlsStatesRedefined();
	}

	protected ConsoleState saveState() {
		return new ConsoleState(
			tia.saveState(),
			pia.saveState(),
			ram.saveState(),
			cpu.saveState(),
			cartridge() != null ? cartridge().saveState() : null,
			videoStandard()
		);
	}

	protected void cycleCartridgeFormat() {
		if (cartridge() == null) {
			showOSD("NO CARTRIDGE INSERTED!", true);
			return;
		}
		ArrayList<CartridgeFormatOption> options;
		try {
			options = CartridgeDatabase.getFormatOptions(cartridge().rom());
		} catch (ROMFormatUnsupportedException e) {
			return;
		}
		CartridgeFormatOption currOption = null;
		for (CartridgeFormatOption option : options)
			if (option.format.equals(cartridge().format())) currOption = option;
		int pos = options.indexOf(currOption) + 1;		// cycle through options
		if (pos >= options.size()) pos = 0;
		CartridgeFormatOption newOption = options.get(pos);
		Cartridge newCart = newOption.format.createCartridge(cartridge().rom());
		cartridgeSocket().insert(newCart, true);
		showOSD(newOption.format.toString(), true);
	}
		
	protected void powerFry() {
		ram.powerFry();
	}

	private ConsoleState pauseAndSaveState() {
		pause();
		ConsoleState state = saveState();
		go();
		return state;
	}

	protected void pauseAndLoadState(ConsoleState state) {
		if (powerOn) {
			pause();
			loadState(state);
			go();
		} else {
			powerOn();
			pauseAndLoadState(state);
		}
	}


	public boolean powerOn = false;

	protected BUS bus;
	protected M6502 cpu;
	protected TIA tia;
	protected PIA pia;
	protected RAM ram;
	protected VideoStandard videoStandard;

	protected boolean videoStandardAuto = true;
	private boolean videoStandardAutoDetectionInProgress = false;
	
	protected ConsoleControlsSocket controlsSocket;
	protected CartridgeSocketAdapter cartridgeSocket;
	protected SaveStateSocketAdapter saveStateSocket;

	protected Clock mainClock;
	
	public static final float ALTERNATE_CLOCK_FACTOR = Parameters.CONSOLE_ALTERNATE_CLOCK_FACTOR;

	
	protected class ConsoleControlsInputAdapter implements ConsoleControlsInput {
		public ConsoleControlsInputAdapter() {
		}
		@Override
		public void controlStateChanged(Control control, boolean state) {
			// Normal state controls
			if (control == Control.FAST_SPEED) {
				if (state) {
					showOSD("FAST FORWARD", true);
					mainClockAdjustToAlternate();
				} else {
					showOSD(null, true);
					mainClockAdjustToNormal();
				}
				return;
			} 
			// Toggles
			if (!state) return;
			switch (control) {
				case POWER:
					if (powerOn) powerOff();
					else powerOn();
					break;
				case POWER_FRY:
					powerFry();
					break;
				case SAVE_STATE_0: case SAVE_STATE_1: case SAVE_STATE_2: case SAVE_STATE_3: case SAVE_STATE_4: case SAVE_STATE_5: 
				case SAVE_STATE_6: case SAVE_STATE_7: case SAVE_STATE_8: case SAVE_STATE_9: case SAVE_STATE_10: case SAVE_STATE_11: case SAVE_STATE_12:
					saveStateSocket.saveState(control.slot);
					break;
				case LOAD_STATE_0: case LOAD_STATE_1: case LOAD_STATE_2: case LOAD_STATE_3: case LOAD_STATE_4: case LOAD_STATE_5: 
				case LOAD_STATE_6: case LOAD_STATE_7: case LOAD_STATE_8: case LOAD_STATE_9: case LOAD_STATE_10: case LOAD_STATE_11: case LOAD_STATE_12:
					saveStateSocket.loadState(control.slot);
					break;
				case VIDEO_STANDARD:
					showOSD(null, true);	// Prepares for the upcoming "AUTO" OSD to always show
					if (videoStandardAuto) videoStandardForced(VideoStandard.NTSC);
					else if (videoStandard() == VideoStandard.NTSC) videoStandardForced(VideoStandard.PAL); 
						else videoStandardAuto();
					break;
				case CARTRIDGE_FORMAT:
					cycleCartridgeFormat();
			}
		}
		@Override
		public void controlStateChanged(ConsoleControls.Control control, int position) {
			// No positional controls here
		}
		@Override
		public void controlsStateReport(Map<ConsoleControls.Control, Boolean> report) {
			//  Only Power Control is visible from outside
			report.put(Control.POWER, powerOn);
		}
	}	
	
	protected class CartridgeSocketAdapter implements CartridgeSocket {
		@Override
		public void insert(Cartridge cartridge, boolean autoPower) {
			// Special case for Savestates
			if (cartridge != null && cartridge instanceof CartridgeSavestate) {
				insertSavestateCartridge((CartridgeSavestate) cartridge);
				return;
			}
			// Normal case
			if (autoPower && powerOn) powerOff();
			cartridge(cartridge); 
			if (autoPower && !powerOn) powerOn();
		}
		@Override
		public Cartridge inserted() {
			return cartridge();
		}
		@Override
		public void cartridgeInserted(Cartridge cartridge) {
			for (CartridgeInsertionListener listener : insertionListeners)
				listener.cartridgeInserted(cartridge);
		}
		@Override
		public void addInsertionListener(CartridgeInsertionListener listener) {
			if (!insertionListeners.contains(listener)) { 
				insertionListeners.add(listener);
				listener.cartridgeInserted(inserted());		// Fire a insertion event
			}
		}
		@Override
		public void removeInsertionListener(CartridgeInsertionListener listener) {
			insertionListeners.remove(listener);
		}
		protected void insertSavestateCartridge(CartridgeSavestate cartridge) {
			ConsoleState state = cartridge.getConsoleState();
			if (state != null) {
				pauseAndLoadState(state);
				showOSD("Savestate Cartridge loaded", true);
			}
		}
		private List<CartridgeInsertionListener> insertionListeners = new ArrayList<CartridgeInsertionListener>();
	}	
	
	protected class SaveStateSocketAdapter implements SaveStateSocket {
		@Override
		public void connectMedia(SaveStateMedia media) {
			this.media = media;	
		}
		@Override
		public SaveStateMedia media() {
			return media;
		}
		@Override
		public void externalStateChange() {
			// Nothing
		}
		public void connectCartridge(Cartridge cartridge) {
			cartridge.connectSaveStateSocket(this);
		}
		@Override
		public void saveStateFile() {
			if (!powerOn || media == null) return;
			ConsoleState state = pauseAndSaveState();
			if (media.saveStateFile(state))
				showOSD("State file saved", true);
			else 
				showOSD("State file save failed", true);
		}
		public void saveState(int slot) {
			if (!powerOn || media == null) return;
			ConsoleState state = pauseAndSaveState();
			if (media.saveState(slot, state))
				showOSD("State " + slot + " saved", true);
			else 
				showOSD("State " + slot + " save failed", true);
		}
		public void loadState(int slot) {
			if (media == null) return;
			ConsoleState state = media.loadState(slot);
			if (state == null) {
				showOSD("State " + slot + " load failed", true);
				return;
			}
			pauseAndLoadState(state);
			showOSD("State " + slot + " loaded", true);
		}
		private SaveStateMedia media;
	}	

}
