/* 2010-03-10
 */
package com.cwasa.javacv;


import java.io.IOException;

import java.net.Socket;

import java.awt.image.BufferedImage;

import java.lang.reflect.InvocationTargetException;

import java.util.Date;


import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Font;
import java.awt.RenderingHints;
import java.awt.BorderLayout;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.Box;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JScrollPane;
import javax.swing.JButton;
import javax.swing.WindowConstants;
import javax.swing.BorderFactory;


import com.cwasa.javacv.Logging;
import com.cwasa.javacv.ClientForTesting;
import javax.swing.JCheckBox;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;


import static com.cwasa.javacv.JAVideoServiceThread.VideoRequestHandler;


public class ServiceApp {

	public static final String				VM_TAG = "JAVideoServiceApp";
	public static final String				VERSION_TAG = "2022-06-26";
	public static final String				SUFF = ".mp4";

	// Negative value means "no limit".
	private static final int				JOB_LIMIT = -1;

/** Writes the given message to the log. */
	private static void log(String msg)		{ System.out.println(msg); }
/** Writes the standard prefix tag and then the given message to the log. */
	private static void logt(String msg)	{ log(VM_TAG+msg); }
/** Conditionally writes the given message to the log. */
	private static void logc(String msg)	{ if (DO_LOGGING) { log(msg); } }

	// Only 3BYTE_BGR works with Xuggle.
	public static final int					IMG_TYPE =
											BufferedImage.TYPE_3BYTE_BGR;
											//BufferedImage.TYPE_4BYTE_ABGR_PRE;
											//BufferedImage.TYPE_4BYTE_ABGR;
											//BufferedImage.TYPE_INT_ARGB;

	private static final int				TIMEOUT = -1;	// seconds

	public static boolean					DO_LOGGING = false;
	public static boolean					DO_VIDEO = true;
	public static boolean					DO_FRAMES = false;
	public static boolean					DO_RUN_TEST_CLIENT = false;
	public static String					TEST_PATH;


	public static void main(String[] args)
	throws IOException, InterruptedException, InvocationTargetException {

		processArgs(args);

		ServiceApp videoapp = new ServiceApp();
		videoapp.startServiceApp();

		//if (DO_RUN_TEST_CLIENT) { videoapp.doRunTestClient(); }

		if (0 < TIMEOUT) {
			try { Thread.sleep(TIMEOUT * 1000); }
			catch (InterruptedException ix) { Thread.currentThread().interrupt(); }
			videoapp.stopServiceApp();
		}
	}

	private static void processArgs(String[] args) {

		boolean log = false;
		boolean test = false;
		String tpath = null;

		if (args != null) {
			for (String arg : args) {
				if (arg.startsWith("-")) {
					if ("-log".startsWith(arg)) { log = true; }
					else if ("-test".startsWith(arg)) { test = true; }
					else { logt(": unknown argument setting: "+arg); }
				}
				else {
					test = true;
					tpath = arg;
					if (!arg.endsWith(SUFF)) {
						tpath = arg+SUFF;
						logt(": test path adjusted: "+tpath);
					}
				}
			}
		}

		if (tpath == null) { tpath = "test-video"+SUFF; }

		//DO_LOGGING = log;
		//DO_RUN_TEST_CLIENT = test;
		TEST_PATH = tpath;
	}

	/*########  Instance data and methods.  ########*/

	private final Logging					LOG =
	new Logging() {
		public void log(String msg) {
			ServiceApp.this.txtLog.append(msg+"\r\n");
			ServiceApp.this.txtLog.setCaretPosition(
				ServiceApp.this.txtLog.getText().length());
		}
		public void logt(String msg)	{ this.log(VM_TAG+msg); }
		public void logc(String msg)	{ if (DO_LOGGING) { this.log(msg); } }
	};

	private final VideoRequestHandler		VIDEO_REQUEST_HANDLER =
	this.makeVideoRequestHandler(this.LOG);

	private VideoRequestHandler makeVideoRequestHandler(final Logging LOG) {

		return new VideoRequestHandler() {
			private final String PFX = "VideoRequestHandler.processRequest()";
			private final String VJH = " VideoJobHandler:";

			private void log(String msg) { LOG.log(PFX+msg); }

			private void logvjhfail(String reason) { this.log(VJH+" "+reason); }

			public synchronized void processRequest(Socket ccsckt) {


				this.log(" starts at "+(new Date())+".");
				try {
					VideoJobHandler mjh = new VideoJobHandler(ccsckt, LOG);
				}
				catch (InterruptedException ix) {
					this.logvjhfail(ix.toString());
					for (StackTraceElement ste : ix.getStackTrace()) {
						LOG.log(ste.toString());
					}
					//ix.printStackTrace();
					Thread.currentThread().interrupt();
				}
				catch (IOException iox) {
					this.logvjhfail(iox.toString());
					for (StackTraceElement ste : iox.getStackTrace()) {
						LOG.log(ste.toString());
					}
					//iox.printStackTrace();
				}

				this.log(" done.");
				ServiceApp.this.notifyJobDone();
			}
		};
	}

	private JAVideoServiceCheckThread		checkThread;
	private JAVideoServiceThread			serviceThread;

	private int								nJobs;


	public ServiceApp()
	throws InterruptedException, InvocationTargetException {

		final Runnable RUN_SET_UP_GUI = new Runnable() {
			public void run() { ServiceApp.this.makeGUI(); }
		};
		EventQueue.invokeAndWait(RUN_SET_UP_GUI);

		try {
			this.nJobs = 0;

			this.checkThread = new JAVideoServiceCheckThread(this.LOG);
			this.serviceThread =
				new JAVideoServiceThread(
					this.VIDEO_REQUEST_HANDLER, this.LOG);
		}
		catch (IOException iox) {
			//System.out.println("Failed to create MovieServiceThread:"+iox);
			this.LOG.log("Failed to create MovieServiceThread:"+iox);
		}
	}

	public Logging getLogging()			{ return this.LOG; }

	public void startServiceApp() {

		//logt(" starts.");
		if (serviceThread != null) {
                    this.LOG.logt(" starts at "+(new Date())+".");
                    this.serviceThread.start();
                    this.checkThread.start();
                } else {
                    this.LOG.logt(" failed to start at "+(new Date())+".");
                }
	}

	public void stopServiceApp() {

		try {
			this.checkThread.shutDown();
			this.serviceThread.shutDown();
			//logt(" stop signal sent.");
			this.LOG.logt(" stop signal sent.");
			System.out.println(VM_TAG+" stop signal sent.");
		}
		//catch (IOException iox) { logt(" stopServiceApp(): "+iox); }
		catch (IOException iox) { this.LOG.logt(" stopServiceApp(): "+iox); }
	}

	private void notifyJobDone() {

		++ this.nJobs;
		if (JOB_LIMIT == this.nJobs) { this.stopServiceApp(); }
	}

	private void doShutDown() {

            if (serviceThread !=  null) {
                this.LOG.logt(" shutting down.");
		this.stopServiceApp();
            }
	}

	private boolean							doneOneQuit = false;
	private void doQuit() {

		if (this.doneOneQuit) {
			System.out.println("Extra doQuit().");
		}
		else {
			this.doneOneQuit = true;
			System.out.println("Quitting.");
			try { Thread.sleep(80); }
			catch (InterruptedException ix) { Thread.currentThread().interrupt(); }
			System.exit(0);
		}
	}

	public void doRunTestClient() {
		//final int W = 320, H = 160, FPS = 25, N = 128;//200;
		final int W = 419, H = 482, FPS = 50, N = 81;
		ClientForTesting.startTest(N, W, H, FPS, TEST_PATH, this.getLogging());
	}

/*################  Support for GUI set up  ################*/

	private JFrame		frame;
	private JPanel		content;
	private JTextArea	txtLog;
	private JScrollPane	scrollLog;

	public static final RenderingHints	AA_HINTS =
		new RenderingHints(
			RenderingHints.KEY_TEXT_ANTIALIASING,
			RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

	protected Font getLogFont() {

		String NICE_MONOSPACED = "Lucida Console";
		String[] ffnames =
			GraphicsEnvironment.
				getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
		final int NF = ffnames.length;
		int i = 0, ii = NF;
		while (i != ii) {
			if (ffnames[i].equals(NICE_MONOSPACED)) { ii = i; } else { ++i; }
		}
		String fname = (ii != NF ? NICE_MONOSPACED : Font.MONOSPACED);

		return new Font(fname, Font.PLAIN, 12);
	}

	protected void makeGUI() {

		this.makeFrameContent();

		final String TITLE = "JASigning Video Generation Server ("+VERSION_TAG+")";
		this.frame = new JFrame(TITLE);
		this.frame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent wevt) {
				ServiceApp.this.doShutDown();
			}
			public void windowClosed(WindowEvent wevt) {
				ServiceApp.this.doQuit();
			}
		});
		this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		this.frame.setContentPane(content);
		this.frame.pack();

		this.frame.setVisible(true);
	}

	protected void makeFrameContent() {

		this.content = new JPanel(new BorderLayout());
		this.content.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));

		this.txtLog = new JTextArea(24, 80) {
			protected void paintComponent(Graphics g) {
				((Graphics2D)g).setRenderingHints(AA_HINTS);
				super.paintComponent(g);
			}
		};
		this.txtLog.setFont(this.getLogFont());
		this.txtLog.setMargin(new Insets(4, 4, 4, 4));

		this.scrollLog = new JScrollPane(this.txtLog);

		final JCheckBox CHKVID = new JCheckBox("Video", true);
		CHKVID.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent cevt) {
				DO_VIDEO = CHKVID.isSelected();
			}
		});

		final JCheckBox CHKFRM = new JCheckBox("Frames", false);
		CHKFRM.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent cevt) {
				DO_FRAMES = CHKFRM.isSelected();
			}
		});

		final JCheckBox CHKLOG = new JCheckBox("Log", false);
		CHKLOG.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent cevt) {
				DO_LOGGING = CHKLOG.isSelected();
			}
		});

		JButton bttntest = new JButton("Test");
		bttntest.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent aevt) {
				ServiceApp.this.doRunTestClient();
			}
		});

		JButton bttnquit = new JButton("Quit");
		bttnquit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent aevt) {
				ServiceApp.this.doShutDown();
				ServiceApp.this.doQuit();
			}
		});

		Box boxctrl = Box.createVerticalBox();
		boxctrl.setBorder(BorderFactory.createEtchedBorder());
		boxctrl.add(Box.createRigidArea(new Dimension(0, 4)));
		boxctrl.add(CHKVID);
		boxctrl.add(Box.createRigidArea(new Dimension(0, 4)));
		boxctrl.add(CHKFRM);
		boxctrl.add(Box.createRigidArea(new Dimension(0, 4)));
		boxctrl.add(CHKLOG);
		boxctrl.add(Box.createRigidArea(new Dimension(0, 4)));
		boxctrl.add(bttntest);
		boxctrl.add(Box.createRigidArea(new Dimension(0, 4)));
		boxctrl.add(bttnquit);
		boxctrl.add(Box.createRigidArea(new Dimension(0, 4)));
		boxctrl.add(Box.createVerticalGlue());

		Box boxhpad = Box.createHorizontalBox();
		boxhpad.add(Box.createRigidArea(new Dimension(4, 0)));
		boxhpad.add(boxctrl);

		content.add(this.scrollLog, BorderLayout.CENTER);
		content.add(boxhpad, BorderLayout.EAST);
	}
}
