/* ticktock.java
 * Copyright (C) 1996 by William Giel
 *
 * E-mail: rvdi@usa.nai.net
 * WWW: http://www.nai.net/~rvdi/home.htm
 *
 *********************************************************************************
 * Other code units:    inputBox.java       // Generic OK-Cancel dialog box
 *                      Julian.java         // Julian calendar conversions, etc.
 *                      Scroller.java       // Scrolling, read-only text display
 *                      socketThread.java   // Thread for socket connection
 *                      timeThread.java     // Thread to display time
 *                      ttConst.java        // Stringtable-like list of literals
 *                      Configuration.java  // Handles file i/o of users' defaults
 *						Format.java			// Formatted output of doubles
 *********************************************************************************
 * Version 1.03b - 25 Feb 1996
 * Changes in Version 1.03b - Expanded internal documentation.
 *********************************************************************************
 * Abstract
 * --------
 * "ticktock" is a  Java beta application that lets users set their computer
 * clocks with the master clock at the U.S. Naval Observatory, using its 'daytime'
 * TCP/Port 13 time service.
 *
 * This technique yields results accurate to plus or minus one second. While
 * not as accurate as systems implementing Network Time Protocol (NTP), or
 * Simplified Network Time Protocol (SNTP), this should be sufficient for most
 * personal computer users. In fact, most PC clocks are not even this accurate.
 *
 * This program is (hopefully) an interesting and useful demonstration of
 * user-interface techniques, concurrent multithreading, and socket programming.
 *
 * Notwithstanding the above, users requiring extremely accurate system time
 * should install software certified to provide accuracy and precision that
 * is consistent with their needs. The U.S. Naval Observatory web site maintains
 * links to a variety of public domain, freeware, shareware and commercial programs
 * for precision timekeeping (http://www.usno.navy.mil)
 *
 ********************************************************************************* 
 * Permission to use, copy, modify, and distribute this software and its
 * documentation without fee for NON-COMMERCIAL purposes is hereby granted.
 * 
 * THE AUTHOR MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
 * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. THE AUTHOR SHALL NOT BE LIABLE
 * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 *********************************************************************************/

import java.awt.*;
import java.lang.*;
import java.util.*;
import java.net.*;
import java.io.*;

///////////////////////////////
//Bring in our support classes
///////////////////////////////
import Julian;
import timeThread;
import Scroller;
import ttConst;
import SocketThread;
import inputBox;
import Configuration;


public class ticktock extends Frame
{

    //////////////
    //Default size
    //////////////
    static final int H_SIZE = 450;
    static final int V_SIZE = 450;

    ////////////////////////
    //Main window origin
    ////////////////////////
    int baseX,
        baseY;


    /////////////////////////////////////////
    //Dialog control 'handles' for reference
    //purposes
    /////////////////////////////////////////
    Scroller scroller               =   null;
    TextField serverTimeField       =   null;
    TextField systemTimeField       =   null;
    TextField correctionField       =   null;
    TextField timezoneField         =   null;
    List serverList                 =   null;

    ///////////////////////////////////////////
    // Threads for running clocks & connection
    ///////////////////////////////////////////
    timeThread sysTimeThread        =   null;
    timeThread serverTimeThread     =   null;
    SocketThread socketThread       =   null;

    ////////////////////////////////////////////
    // These next three methods help eliminate
    // redundant code when setting up our display
    // layout.
    ////////////////////////////////////////////
    protected Button makebutton(String name,
                                GridBagLayout gridbag,
                                GridBagConstraints c)
    {
        Button button = new Button(name);
        gridbag.setConstraints(button, c);
        add(button);
        return button;
    }

    protected Label makeLabel(  String text,
                                GridBagLayout gridbag,
                                GridBagConstraints c)
    {
        Label label = new Label(text);
        gridbag.setConstraints(label, c);
        add(label);
        return label;
    }
        
    protected TextField makeTextField(  String value,
                                        GridBagLayout gridbag,
                                        GridBagConstraints c,
                                        int len,
                                        boolean readwrite)
    {
        TextField text = new TextField(value,len);
        text.setEditable(readwrite);
        gridbag.setConstraints(text, c);
        add(text);
        return text;
    }

    /////////////////////////////////////////
    // Initiallizer (or constructor)
    /////////////////////////////////////////
    public ticktock(String title)
    {
        super(title);
        resize(H_SIZE, V_SIZE);

        setFont(new Font(ttConst.FONTSTRING,Font.BOLD,ttConst.FONTHEIGHT));

        ///////////////////////////////////////////
        // Set up the UI using GridBagLayout
        ///////////////////////////////////////////
        GridBagLayout gridbag=new GridBagLayout();
        GridBagConstraints c=new GridBagConstraints();
        setLayout(gridbag);
 
        c.insets=new Insets(3,3,3,3);
        c.weightx = 1.0;
        c.weighty = 1.0;            
        c.gridwidth=2;     
        c.fill=GridBagConstraints.NONE;
        c.anchor=GridBagConstraints.NORTHEAST;
        makeLabel(ttConst.SERVER,gridbag,c);
        c.fill=GridBagConstraints.NONE;
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.anchor=GridBagConstraints.NORTHWEST;
        serverTimeField=makeTextField("",gridbag,c,28,false);
     		
        c.gridwidth=2;
        c.fill=GridBagConstraints.NONE;
        c.anchor=GridBagConstraints.NORTHEAST;
        makeLabel(ttConst.SYSTEM,gridbag,c);
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.anchor=GridBagConstraints.NORTHWEST;
        systemTimeField=makeTextField("",gridbag,c,28,false);

        c.gridwidth=2;
        c.fill=GridBagConstraints.NONE;
        c.anchor=GridBagConstraints.NORTHEAST;
        makeLabel(ttConst.OFFSET,gridbag,c);
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.anchor=GridBagConstraints.NORTHWEST;
        timezoneField=makeTextField("",gridbag,c,10,false);

        c.gridwidth=2;
        c.fill=GridBagConstraints.NONE;
        c.anchor=GridBagConstraints.NORTHEAST;
        makeLabel(ttConst.CORRECTION,gridbag,c);
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.anchor=GridBagConstraints.NORTHWEST;
        correctionField=makeTextField("",gridbag,c,10,false);
        
        c.weightx=0;
        c.fill=GridBagConstraints.NONE;
        c.insets=new Insets(0,5,0,5);
        c.anchor=GridBagConstraints.SOUTHWEST;
        makeLabel(ttConst.SERVERNAME,gridbag,c);
        
        c.weightx=0;
        c.fill=GridBagConstraints.BOTH;
        serverList= new List(2,false);
        for(int i=0;ttConst.SERVERLIST[i]!=null;i++)
            serverList.addItem(ttConst.SERVERLIST[i]);
        serverList.select(0);
        serverList.makeVisible(0);
        
        c.weightx=0;
        gridbag.setConstraints(serverList, c);
        add(serverList);
      		
        c.fill=GridBagConstraints.BOTH;
        c.insets=new Insets(5,5,5,5);
        scroller = new Scroller(ttConst.VERSION,5,50);
        gridbag.setConstraints(scroller, c);
        add(scroller);

        c.insets=new Insets(5,5,5,5);
        c.gridwidth=4;
        c.weightx = 1.0;
        c.weighty = 1.0;
        c.fill=GridBagConstraints.HORIZONTAL;        	
        makebutton(ttConst.CONNECT, gridbag, c);
        makebutton(ttConst.SET, gridbag, c);
        makebutton(ttConst.RESET, gridbag, c);
        c.gridwidth = GridBagConstraints.REMAINDER;        
        makebutton(ttConst.QUIT, gridbag, c);

        ///////////////////////////////////////////
        //Get screen dimensions
        ///////////////////////////////////////////
        Dimension screenSize = getToolkit().getScreenSize();

        //////////////////////////////////////
        //Position window in center of display
        //////////////////////////////////////
        baseX = (screenSize.width - H_SIZE)/2;
        baseY = (screenSize.height - V_SIZE)/2;
        move(baseX,baseY);

        ///////////////////////////////////////////////////////
        // Fire-up our local system clock
        ///////////////////////////////////////////////////////
        sysTimeThread=new timeThread(systemTimeField,true);
        timezoneField.setText(sysTimeThread.getTimezoneString());
        sysTimeThread.start();        

        //////////////////////
        // Do it!
        //////////////////////
        show();
    }

	/////////////////////////////////////////
    // This is an application, thus we need a
    // public main method. All that needs to
    // be done here is to create an instance
    // of our ticktock class.
    /////////////////////////////////////////
    public static void main(String args[])
    {
        new ticktock("TickTock");
    }

	//////////////////////////////////////////////
    //Here we handle any buttons that are pressed
    //////////////////////////////////////////////
    public synchronized boolean action(Event evt, Object arg)
    {
        double correction = 0;
        String test=arg.toString();

        //////////////////////////////////////
        // Handle the QUIT button by stooping
        // any threads( I test for null, but
        // this could be done with isAlive()
        // also ), clean up and exit.
        //////////////////////////////////////
        if(ttConst.QUIT == test){
            if(socketThread!= null){
                serverTimeThread=socketThread.getTimeThread();
                socketThread.stopSocket();
                socketThread=null;
            }            
            if(serverTimeThread!=null){
                serverTimeThread.stop();
                serverTimeThread=null;
            }
            if(sysTimeThread!=null){
                sysTimeThread.stop();
                sysTimeThread=null;
            }
            
            dispose();
            System.exit(0);
        }

        ///////////////////////////////////////////////////////
        // Handle the CONNECT button. Kill the socketThread and
        // serverTimeThread (if alive.) Then create a new
        // socketThread.
        ///////////////////////////////////////////////////////
        else if(ttConst.CONNECT == test){
            if(socketThread!= null){
                serverTimeThread=socketThread.getTimeThread();
                socketThread.stopSocket();
                socketThread=null;
            }            
            if(serverTimeThread!=null){
                serverTimeThread.stop();
                serverTimeThread=null;
            }

			/////////////////////////////////////
            //Clear the corresponding text fields
            /////////////////////////////////////
            serverTimeField.setText("");
            correctionField.setText("");
 
            socketThread = new SocketThread(    serverList.getSelectedItem(),
                                                serverTimeField,
                                                correctionField,
                                                scroller );
            socketThread.start();
            return true;
        }

        //////////////////////////////
        // Handle the SET TIME button
        //////////////////////////////
        else if(ttConst.SET == test){

        	///////////////////////////////////////////
        	// Can't set the time/date if we don't have
        	// any evidence of a valid connection
        	///////////////////////////////////////////
            if(null == socketThread){
                scroller.newLine();
                scroller.appendText(ttConst.NO_CONNECT);
                return true;
            }
            
            ///////////////////////////////////////////
            // We might have an incomplete set of data,
            // so display WAITING message and boogie.
            ///////////////////////////////////////////
            else if (!socketThread.isConnected()){
                scroller.newLine();
                scroller.appendText(ttConst.WAITING);
                return true;
            }

			////////////////////////////////////////
            // If we have data to correct our clock
			// get the correction (in seconds) and
            // a handle to the serverTimeThread,
            // then kill the socket thread.
            ////////////////////////////////////////
            correction=socketThread.getCorrection();
            serverTimeThread=socketThread.getTimeThread();
            socketThread.stopSocket();
            socketThread=null;

            /////////////////////////////////////////////
            // Get the system command prompts and correct
            // the time/date.
            /////////////////////////////////////////////
            sysCommandDialog confirm = new sysCommandDialog(    this,
                                                                ttConst.CONFIRM,
                                                                ttConst.PROMPT,
                                                                ttConst.DFLT_TIME,
                                                                ttConst.DFLT_DATE,
                                                                correction,
                                                                scroller,
                                                                H_SIZE-10,
                                                                (H_SIZE * 10)/30 );
            confirm.show();
            
            ///////////////////////////////////////////////////////
            // After executing the dialog, clear the serverTimeField
            // and correctionField.
            // If dialogs were modal these next lines would
            // not be executed until dialog returned.
            // (This is a known bug in Java 1.0)
            ///////////////////////////////////////////////
            if(serverTimeThread!=null){
                serverTimeThread.stop();
                serverTimeThread=null;
            }            
            serverTimeField.setText("");            
            correctionField.setText("");
            correction = 0;
            return true;            
        }

        ///////////////////////////////
        // Handle the RESET button
        ///////////////////////////////
        else if(ttConst.RESET == test){

        	//////////////////////////
        	// Display Reset Message
        	//////////////////////////
            scroller.newLine();
            scroller.appendText(ttConst.USER_RESET);

            ///////////////////////////////////////
            // Kill socketThread & serverTimeThreads
            // and set to null.
            ///////////////////////////////////////
            if(socketThread!= null){
                serverTimeThread=socketThread.getTimeThread();
                socketThread.stopSocket();
                socketThread=null;
            }            
            if(serverTimeThread!=null){
                serverTimeThread.stop();
                serverTimeThread=null;
            }

            /////////////////////////////////
            //Clear textFields (except local)
            //Clear correction value.
            /////////////////////////////////
            serverTimeField.setText("");
            correctionField.setText("");
            correction = 0;
            return true;
        }        
        return false;
    }

	/////////////////////////////////////////////////
    //Handle built-in events from window
    /////////////////////////////////////////////////
    public synchronized boolean handleEvent(Event e)
    {
        if (e.id == Event.WINDOW_DESTROY){
            if(socketThread!= null){
                serverTimeThread=socketThread.getTimeThread();
                socketThread.stopSocket();
                socketThread=null;
            }              
            if(serverTimeThread!=null){
                serverTimeThread.stop();
                serverTimeThread=null;
            }
            if(sysTimeThread!=null){
              sysTimeThread.stop();
                sysTimeThread=null;
            }

            dispose(); 
            System.exit(0);
        }
        return super.handleEvent(e);
    }
}

/////////////////////////////////////////////////
// Create a descendant of our abstract class
// inputBox. We'll pass the correction to this
// object, let the user accept or modify the
// system commands for a particular platform,
// and then execute the commands from the
// sysCommandDialog.action(...) method.
/////////////////////////////////////////////////
class sysCommandDialog extends inputBox
{
    
    double correction;
    Scroller scroller;
    Configuration cfg;
   
    
    public sysCommandDialog(    Frame parent,
                                String title,
                                String prompt,
                                String dflt,
                                String dflt2,
                                double corr,
                                Scroller s,
                                int width,
                                int height)
    {
        super(parent,title,prompt,dflt,dflt2,width,height);
        correction = corr;
        scroller=s;

        cfg=new Configuration(ttConst.DEFAULTS);

        ////////////////////////////////////////
        // Check configuration file... if only
        // a single line, i.e. UNIX systems,
        // then the second field to ""
        ////////////////////////////////////////
        if(cfg.getConfig()){
        	textfield1.setText(cfg.getCommand1());
        	if(null != cfg.getCommand2())
	        	textfield2.setText(cfg.getCommand2());
	        else
	        	textfield2.setText("");
        }
  	
    }

    boolean doCommand(String command)
    {
		try{
            	Process p=Runtime.getRuntime().exec(command);
               
   	        	///////////////////////////////////////////////////
       	    	//In case the system sends something back to us...
           		///////////////////////////////////////////////////
           		DataInputStream dis = new DataInputStream(
                   		 new BufferedInputStream(p.getInputStream()));
               	String s = null;
            	while ((s = dis.readLine()) != null) {
   	            	scroller.newLine();
       	        	scroller.appendText(s);
           		}
       	}catch(Exception e){
                               	scroller.newLine();
                               	scroller.appendText("Exception: " + e);
            	            	return false;
            	           }
        /////////////////////////////////
        // Echo command to scroller, if
        // successful.
        /////////////////////////////////
        scroller.newLine();
        scroller.appendText(command);
        return true;
    }

    public boolean action(Event evt, Object arg)
    {
		boolean unixCommand=false;
    	
        ///////////////////////////////////////////
        // Close the dialog and do nothing else
        // on CANCEL
        ///////////////////////////////////////////
        if(arg.equals(inputBox.CANCEL))
        {
            dispose();
            return true;
        }
        
        //////////////////////////////////////////
        // We'll correct the clock here (finally!)
        //////////////////////////////////////////
        if(arg.equals(inputBox.OK)){
     
            
			//////////////////////////////////////
			// Retrieve our command templates.
			//////////////////////////////////////
			String command1 = textfield1.getText();
			String command2 = textfield2.getText();

			////////////////////////////////////////
			// Get the positions of hh, mm & ss and
			// mm, dd, and yy in the system command
			// templates.
			////////////////////////////////////////
            int hIndex=command1.indexOf("hh");
            int mIndex=command1.lastIndexOf("mm");
            int sIndex=command1.indexOf("ss");

            /////////////////////////////////////////
            // If the second command field is empty
            // we're on a single command system.
            /////////////////////////////////////////
            if(0 == command2.trim().length()){
            	unixCommand=true;
				command2=command1;
            }

            int monthIndex=command2.lastIndexOf("MM");
            int dayIndex=command2.indexOf("DD");
            int yearIndex=command2.indexOf("YY");            

            /////////////////////////////////////////
            // They've all got to be there, else we
            // have invalid syntax.
            /////////////////////////////////////////
            if(-1==hIndex || -1==mIndex || -1==sIndex){
                scroller.newLine();
                scroller.appendText(ttConst.INVALID_TIME_FORMAT);
                return false;
            }

            if(-1==monthIndex || -1==dayIndex || -1==yearIndex){
                scroller.newLine();
                scroller.appendText(ttConst.INVALID_DATE_FORMAT);
                return false;
            }

            ///////////////////////////////////////////////////////
            // Get the current time and date. Apply the correction,
            // substitute corrected values in command templates and
            // execute system commands.
            ///////////////////////////////////////////////////////
			Date now=new Date();            
            
            now.setTime(now.getTime()+(long)(correction + .5) * 1000L);
			String hours=Integer.toString(now.getHours());
			if(1 == hours.length())hours="0" + hours;
			String minutes=Integer.toString(now.getMinutes());
			if(1 == minutes.length())minutes="0" + minutes;			
			String seconds=Integer.toString(now.getSeconds());
			if(1 == seconds.length())seconds="0" + seconds;

			StringBuffer temp= new StringBuffer(command1);
			
			temp.setCharAt(hIndex,hours.charAt(0));
			temp.setCharAt(hIndex+1,hours.charAt(1));

			temp.setCharAt(mIndex,minutes.charAt(0));
			temp.setCharAt(mIndex+1,minutes.charAt(1));
			
			temp.setCharAt(sIndex,seconds.charAt(0));
			temp.setCharAt(sIndex+1,seconds.charAt(1));

			///////////////////////////////////////////
			// If we're using two commands, i.e. DOS or
			// Win95, execute the time command now
			///////////////////////////////////////////
			if(false == unixCommand){
				if(!ttConst.DEBUG){

				    if(!doCommand(temp.toString())){
                        dispose();
				        return false;
				    }

				}
            	//////////////////////////////////////
            	// Set the StringBuffer to the second
            	// command.
            	//////////////////////////////////////
            	temp=new StringBuffer(command2);
			}

			///////////////////////////////////////////////
			// If this is a new command, we're starting a
			// fresh StringBuffer objecr... if not, we're
			// inserting our date fields, MM, DD and YY.
			///////////////////////////////////////////////
			String month=Integer.toString(now.getMonth()+1);
			if(1 == month.length())month="0" + month;
			String day=Integer.toString(now.getDate());
			if(1 == day.length())day="0" + day;			
			String year=Integer.toString(now.getYear());
			if(1 == year.length())year="0" + year;

			temp.setCharAt(monthIndex,month.charAt(0));
			temp.setCharAt(monthIndex+1,month.charAt(1));

			temp.setCharAt(dayIndex,day.charAt(0));
			temp.setCharAt(dayIndex+1,day.charAt(1));
			
			temp.setCharAt(yearIndex,year.charAt(0));
			temp.setCharAt(yearIndex+1,year.charAt(1));

			if(!ttConst.DEBUG){
			    
 				    if(!doCommand(temp.toString())){
 				        dispose();
 				        return false;
 				    }
 				    
			}

            ///////////////////////////////////
            // Save these templates as defaults
            // for next time.
            ///////////////////////////////////
            cfg.setConfig(command1.trim(),(unixCommand)? null : command2.trim());

            dispose();
            return true;
        }
        
        return false;
    }    
}

