/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */


import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;

import com.fazecast.jSerialComm.*;


public class SerialConsole {

    // The lowest possible baudrate that would still be supported on most platforms which is not the magic baudrate of 1200 bps
    private static final int LOWEST_BAUDRATE_DEF = 2400;
    private static final int LOWEST_BAUDRATE_ALT =  600;

    private static       int LOWEST_BAUDRATE_SEL =    0;

    // Reset pulse time
    private static final int RESET_PULSE_TIME =  250;

    // Communication-related variables
    private static SerialPort     ttyPort     = null;
    private static InputStream    ttyReader   = null;
    private static OutputStream   ttyWriter   = null;

    private static Socket         tcpSocket   = null;
    private static InputStream    tcpReader   = null;
    private static BufferedWriter tcpWriter   = null;

    // GUI-related variables
    private static JFrame         mainWindow  = null;
    private static JTextArea      txtOutput   = null;
    private static JButton        btnCopy     = null;
    private static JButton        btnClear    = null;
    private static JTextField     txtInput    = null;
    private static JList          lstLineEnd  = null;
    private static JButton        btnReset    = null;

    private static boolean        initialized = false;

    // Initialize all
    private static void initialize(boolean tty, String target, int config)
    {
        // Initialize the reader and writer
        try {
            if(tty) {
                // Select the lowest baudrate
                LOWEST_BAUDRATE_SEL = LOWEST_BAUDRATE_DEF;
                if(LOWEST_BAUDRATE_SEL == config) LOWEST_BAUDRATE_SEL = LOWEST_BAUDRATE_ALT;
                // Disable hangup signal (Linux only)
                // stty -F <device> cs8 <baudrate> ignbrk noflsh -brkint -crtscts -echo -echoctl -echoe -echok -echoke -hupcl -icanon -icrnl -iexten -imaxbel -isig -ixon -onlcr -opost
                final String osName = System.getProperty("os.name");
                if( osName.contains("nux") || osName.contains("nix") ) {
                    final Process process = Runtime.getRuntime().exec( String.format("stty -F %s -hupcl", target) );
                    process.waitFor();
                    if(process.exitValue() != 0) {
                        final BufferedReader stdError = new BufferedReader(
                            new InputStreamReader( process.getErrorStream() )
                        );
                        throw new Exception( stdError.readLine() );
                    }
                }
                // Open the port and set some settings
                ttyPort = SerialPort.getCommPort(target);
                ttyPort.setFlowControl​(SerialPort.FLOW_CONTROL_DISABLED);
                ttyPort.setBaudRate​   (LOWEST_BAUDRATE_SEL);
                ttyPort.setNumDataBits​(8);
                ttyPort.setParity​     (SerialPort.NO_PARITY);
                ttyPort.setNumStopBits​(SerialPort.ONE_STOP_BIT);
                ttyPort.openPort      ();
                ttyPort.setDTR        ();
                ttyPort.setRTS        ();
                // Get the stream
                ttyReader = ttyPort.getInputStream​();
                ttyWriter = ttyPort.getOutputStream​();
                // Set the baudrate to the intended baudrate
                ttyPort.setBaudRate(config);
                TimeUnit.MILLISECONDS.sleep(RESET_PULSE_TIME);
                ttyPort.setBaudRate​(LOWEST_BAUDRATE_SEL);
                TimeUnit.MILLISECONDS.sleep(RESET_PULSE_TIME);
                ttyPort.setBaudRate(config);
            }
            else {
                // Open the socket
                tcpSocket = new Socket(target, config);
                // Get the stream
                tcpReader = tcpSocket.getInputStream();
                tcpWriter = new BufferedWriter( new OutputStreamWriter( tcpSocket.getOutputStream() ) );
            }
        }
        catch(Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }

        // Create the GUI - main window
        final String sconfig = String.format("%d", config);

        if(ttyPort != null)
            mainWindow = new JFrame( "TTY Serial Console - " + target + ":" + sconfig + "/8N1" );
        else
            mainWindow = new JFrame( "TCP Serial Console - " + target + ":" + sconfig          );

        mainWindow.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        mainWindow.setLayout( new BorderLayout() );
        mainWindow.getRootPane().setBorder( BorderFactory.createEmptyBorder(10, 10, 10, 10) );

        // Create the GUI - output area
        txtOutput = new JTextArea(20, 100);
        txtOutput.setEditable(false);
        txtOutput.setFont( new Font(Font.MONOSPACED, Font.PLAIN, 12) );
        txtOutput.setBorder( new CompoundBorder( txtOutput.getBorder(), new EmptyBorder(5, 5, 5, 5) ) );

        final DefaultCaret caret = (DefaultCaret) txtOutput.getCaret();
        caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);

        final JScrollPane scrollPane = new JScrollPane(txtOutput);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        mainWindow.add(scrollPane, BorderLayout.CENTER);

        // Create the GUI - input area
        final JPanel panelInMain = new JPanel();
        panelInMain.setLayout( new BorderLayout(5, 0) );
        panelInMain.setBorder( BorderFactory.createEmptyBorder(10, 0, 0, 0) );

            final JPanel panelLeft = new JPanel();
            panelLeft.setLayout( new BorderLayout() );

                final JPanel panelLeftSub = new JPanel();
                panelLeftSub.setLayout( new GridLayout(3, 1, 0, 7) );

                    btnCopy = new JButton("Copy Output");
                    btnCopy.setBorder( new CompoundBorder( btnCopy.getBorder(), new EmptyBorder(2, 2, 2, 2) ) );

                    btnClear = new JButton("Clear Output");
                    btnClear.setBorder( new CompoundBorder( btnClear.getBorder(), new EmptyBorder(2, 2, 2, 2) ) );

                    btnReset = new JButton("Try Reset MCU");
                    btnReset.setBorder( new CompoundBorder( btnReset.getBorder(), new EmptyBorder(2, 2, 2, 2) ) );
                    if(!tty) btnReset.setEnabled(false);

                panelLeftSub.add(btnCopy);
                panelLeftSub.add(btnClear);
                panelLeftSub.add(btnReset);

            panelLeft.add(panelLeftSub, BorderLayout.PAGE_START);
            panelLeft.add(new JPanel(), BorderLayout.CENTER);

            final JPanel panelMiddle = new JPanel();
            panelMiddle.setLayout( new BorderLayout() );

                txtInput = new JTextField();
                txtInput.setFont( new Font(Font.MONOSPACED, Font.PLAIN, 12) );
                txtInput.setBorder( new CompoundBorder( txtInput.getBorder(), new EmptyBorder(5, 5, 5, 5) ) );

            panelMiddle.add(txtInput, BorderLayout.PAGE_START);

            final JPanel panelRight = new JPanel();
            panelRight.setLayout( new BorderLayout() );

                final String[] lineEndStr  = { "[none]", "LF", "CR", "CRLF", "LFCR" };
                final char[]   lineEndChr1 = { 0,        '\n', '\r', '\r',   '\n'   };
                final char[]   lineEndChr2 = { 0,        0,    0,    '\n',   '\r'   };
                lstLineEnd = new JList<String>(lineEndStr);
                lstLineEnd.setBorder( new CompoundBorder( new EtchedBorder(EtchedBorder.LOWERED), new EmptyBorder(3, 3, 3, 3) ) );
                lstLineEnd.setSelectedIndex(0);

            panelRight.add(lstLineEnd, BorderLayout.CENTER);

        panelInMain.add(panelLeft, BorderLayout.LINE_START);
        panelInMain.add(panelMiddle, BorderLayout.CENTER);
        panelInMain.add(panelRight, BorderLayout.LINE_END);

        mainWindow.add(panelInMain, BorderLayout.PAGE_END);

        // Add event listener to the copy button
        btnCopy.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Copy to clipboard
                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
                    new StringSelection( txtOutput.getText() ),
                    null
                );
                // Set focus to the input text field
                txtInput.requestFocus();
            }
        } );

        // Add event listener to the clear button
        btnClear.addActionListener( new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Clear the text field
                txtOutput.setText("");
                // Set focus to the input text field
                txtInput.requestFocus();
            }
        } );

        // Add event listener to the input text field
        txtInput.addKeyListener( new KeyAdapter() {
            public void keyPressed(KeyEvent keyEvent) {
                // Check if it is the enter key
                if(keyEvent.getKeyCode() != KeyEvent.VK_ENTER) return;
                // Get the line ending character(s)
                final char leChar1 = lineEndChr1[ lstLineEnd.getSelectedIndex() ];
                final char leChar2 = lineEndChr2[ lstLineEnd.getSelectedIndex() ];
                // Write data
                try {
                    // TTY
                    if(ttyPort != null) {
                        ttyWriter.write( txtInput.getText().getBytes() );
                        if(leChar1 != 0) ttyWriter.write(leChar1);
                        if(leChar2 != 0) ttyWriter.write(leChar2);
                        ttyWriter.flush();
                    }
                    // TCP
                    else {
                        tcpWriter.write( txtInput.getText() );
                        if(leChar1 != 0) tcpWriter.write(leChar1);
                        if(leChar2 != 0) tcpWriter.write(leChar2);
                        tcpWriter.flush();
                    }
                }
                catch(Exception e) {
                    e.printStackTrace();
                    System.exit(-1);
                }
                // Clear the text field
                txtInput.setText("");
            }
        } );

        // Add event listener to the list box
        lstLineEnd.addListSelectionListener( new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                // Set focus to the input text field
                txtInput.requestFocus();
            }
        } );

        // Add event listener to the reset button
        if(btnReset != null) {
            btnReset.addActionListener( new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    // Reset the MCU using thread so the GUI does not froze
                    ( new Thread() {
                        public void run() {
                            ttyPort.clearDTR();
                            try {
                                TimeUnit.MILLISECONDS.sleep(RESET_PULSE_TIME);
                            }
                            catch(Exception ignored) {
                            }
                            ttyPort.setDTR();
                            // Restore the  baudrate
                            ttyPort.setBaudRate​(LOWEST_BAUDRATE_SEL);
                            ttyPort.setBaudRate​(config);
                        }
                    } ).start();
                    // Set focus to the input text field
                    txtInput.requestFocus();

                }
            } );
        }

        // Add event listener to the main window
        mainWindow.addWindowFocusListener( new WindowFocusListener() {
            private long lastTime = 0;
            public void windowGainedFocus(WindowEvent e) {
                lastTime = System.currentTimeMillis();
            }
            public void windowLostFocus(WindowEvent e) {
                final long newTime = System.currentTimeMillis();
                if(newTime - lastTime > 250) {
                    mainWindow.setAlwaysOnTop(false);
                    mainWindow.removeWindowFocusListener(this);
                }
                lastTime = newTime;
            }
        } );

        // Make the window to stay above all other windows
        mainWindow.setAlwaysOnTop(true);

        // Show the window
        mainWindow.setSize(600, 425);
        mainWindow.setMinimumSize( new Dimension(600, 425) );
        mainWindow.setVisible(true);

        // Set focus to the input text field
        txtInput.requestFocus();

        // Set flag to indicate that application initialization has been completed
        initialized = true;
    }

    // Java main()
    public static void main(String args[]) throws Exception
    {
        // Check the number of arguments
        if( args.length != 3 || ( !args[0].equals("tty") && !args[0].equals("tcp") ) ) {
            System.out.println("\n");
            System.out.println("Usage: SerialConsole tty <device> <baudrate>");
            System.out.println("       SerialConsole tcp <hostname/ip> <port>");
            System.out.println("\n");
            return;
        }

        // Get the arguments
        final boolean tty    = args[0].equals("tty");
        final String  target = args[1];                   // TTY : device   ; TCP : hostname/IP
        final int     config = Integer.parseInt(args[2]); // TTY : baudrate ; TCP : port

        // Initialize the application
        SwingUtilities.invokeLater( new Runnable() {
            public void run() {
                initialize(tty, target, config);
            }
        } );

        // Initialize the reader thread
        ( new Thread() {
            public void run() {
                for(;;) {
                    // Yield
                    yield();
                    // Skip if the application is not fully initialized yet
                    if(!initialized) continue;
                    // Read data
                    try {
                        // TTY
                        if(ttyPort != null) {
                            while( ttyReader.available() > 0 ) {
                                final int c = ttyReader.read();
                                if(c < 0) break;
                                if(c != 0) txtOutput.append( String.valueOf( (char) c ) );
                            }
                        }
                        // TCP
                        else {
                            while( tcpReader.available() > 0 ) {
                                final int c = tcpReader.read();
                                if(c < 0) break;
                                if(c != 0) txtOutput.append( String.valueOf( (char) c ) );
                            }
                        }
                    }
                    catch(Exception e) {
                        e.printStackTrace();
                        System.exit(-1);
                    }
                }
            }
        } ).start();
    }

} // class
