
package uk.co.wingpath.io;

import java.io.*;
import java.util.*;

import uk.co.wingpath.util.*;
import uk.co.wingpath.event.*;

/**
* This class implements connections over serial interfaces such as RS232.
* <p>The methods in this class are thread-safe, and guarantee atomicity of
* reads & writes.
*/
public class SerialConnection
    implements Connection
{
    private final String portName;
    private final Reporter reporter;
    private String rtsControl = "high";
    private int speed = 9600;
    private int databits = 8;
    private int stopbits = 1;
    private String parity = "none";
    private final Object readLock = new Object ();
    private final Object writeLock = new Object ();
    private String name = null;

    // The following are declared volatile to avoid having to lock in their
    // respective access methods. Locking 'readLock' could take up to 200ms.
    private volatile int fd;
    private volatile long timeLastRead;
    private volatile long timeLastWrite;

    private native int open (String name)
        throws IOException;
    private native void close (int fd);
    private native int read (int fd, byte [] data, int offset, int len)
        throws IOException;
    private native void write (int fd, byte [] data, int offset, int len)
        throws IOException;
    private native void drain (int fd);
    private native void flush (int fd);
    private native void setTimeout (int fd, int timeout);
    private native void setSpeed (int fd, int speed)
        throws IOException;
    private native void setEightDataBits (int fd, boolean eight)
        throws IOException;
    private native void setTwoStopBits (int fd, boolean two)
        throws IOException;
    private native void setParityEnable (int fd, boolean enable)
        throws IOException;
    private native void setOddParity (int fd, boolean odd)
        throws IOException;
    private native void setRtsControl (int fd, int mode)
        throws IOException;
    private native int getSpeed (int fd);
    private native boolean getEightDataBits (int fd);
    private native boolean getTwoStopBits (int fd);
    private native boolean getParityEnable (int fd);
    private native boolean getOddParity (int fd);
    private native void setRts (int fd, boolean on)
        throws IOException;
    private native void setDtr (int fd, boolean on)
        throws IOException;
    private native boolean getCts (int fd)
        throws IOException;
    private native boolean getDsr (int fd)
        throws IOException;
    private native static void checkLoaded ();
    public native static void setAttributes (String name, int set, int clear)
        throws IOException;

    public static final int ATTRIBUTE_HIDDEN = 0x0001;

    /**
    * Constructs a {@code SerialConnection} using the specified port name.
    * <p>The port must be opened using the {@link SerialConnection#open open}
    * method before reading or writing.
    * @param portName name of serial port to be used for the connection.
    * @param reporter where to report opening/closing of connection.
    */
    public SerialConnection (String portName, Reporter reporter)
    {
        if (reporter == null)
            throw new NullPointerException ("reporter must not be null");
        this.portName = portName;
        this.reporter = reporter;
        fd = -1;
        timeLastRead = timeLastWrite = System.nanoTime ();
    }

    /**
    * Constructs a {@code SerialConnection} using the specified port name.
    * <p>The port must be opened using the {@link SerialConnection#open open}
    * method before reading or writing.
    * @param portName name of serial port to be used for the connection.
    */
    public SerialConnection (String portName)
    {
        this (portName, new DummyReporter ());
    }

    /**
    * Opens the port.
    * @throws IOException if the port cannot be opened.
    */
    @Override
    public void open ()
        throws IOException
    {
        synchronized (readLock)
        {
            synchronized (writeLock)
            {
                if (fd >= 0)
                {
                    close (fd);
                    fd = -1;
                }
                try
                {
                    fd = open (portName);
                }
                catch (IOException e)
                {
                    IOException ioe = new HIOException ("I102",
                        "Can't open serial port " + portName);
                    ioe.initCause (e);
                    throw ioe;
                }
                try
                {
                    setSerialPortParams ();
                    setRtsControl ();
                }
                catch (IOException e)
                {
                    close (fd);
                    fd = -1;
                    throw e;
                }
                timeLastRead = timeLastWrite = System.nanoTime ();
            }
        }
        reporter.info (null, "Opened serial connection %s", getName ());
    }

    @Override
    public boolean isOpen ()
    {
        return fd >= 0;
    }

    @Override
    public long getTimeLastRead ()
    {
        return timeLastRead;
    }

    @Override
    public long getTimeLastWrite ()
    {
        return timeLastWrite;
    }

    private void setSerialPortParams ()
        throws HIOException
    {
        try
        {
            setSpeed (fd, speed);
        }
        catch (IOException ex)
        {
            HIOException ioe = new HIOException ("I103",
                "Can't set serial speed");
            ioe.initCause (ex);
            throw ioe;
        }
        try
        {
            setEightDataBits (fd, databits == 8);
        }
        catch (IOException ex)
        {
            HIOException ioe = new HIOException ("I104",
                "Can't set serial data bits");
            ioe.initCause (ex);
            throw ioe;
        }
        try
        {
            setTwoStopBits (fd, stopbits == 2);
        }
        catch (IOException ex)
        {
            HIOException ioe = new HIOException ("I105",
                "Can't set serial stop bits");
            ioe.initCause (ex);
            throw ioe;
        }
        try
        {
            setParityEnable (fd, !parity.equals ("none"));
            setOddParity (fd, parity.equals ("odd"));
        }
        catch (IOException ex)
        {
            HIOException ioe = new HIOException ("I106",
                "Can't set serial parity");
            ioe.initCause (ex);
            throw ioe;
        }
    }

    /**
    * Sets the port parameters to the specified values.
    * @param speed speed in bits/second. Must be one of the speeds supported
    * by the port.
    * @param databits number of data bits per character. Must be 7 or 8.
    * @param stopbits number of stop bits per character. Must be 1 or 2.
    * @param parity parity to be used. Must be "odd", "even" or "none".
    * @throws IOException if the parameters could not be set.
    */
    public void setSerialPortParams (int speed, int databits,
            int stopbits, String parity)
        throws HIOException
    {
        synchronized (readLock)
        {
            synchronized (writeLock)
            {
                this.speed = speed;
                this.databits = databits;
                this.stopbits = stopbits;
                this.parity = parity;
                if (fd >= 0)
                    setSerialPortParams ();
            }
        }
    }

    /**
    * Gets the value of the CTS line.
    * @return whether the CTS line is set.
    */
    boolean getCts ()
        throws IOException
    {
        try
        {
            return getCts (fd);
        }
        catch (IOException e)
        {
            return false;
        }
    }

    /**
    * Gets the value of the DSR line.
    * @return whether the DSR line is set.
    */
    boolean getDsr ()
    {
        try
        {
            return getDsr (fd);
        }
        catch (IOException e)
        {
            return false;
        }
    }

    /**
    * Sets the DTR line.
    * @param on whether to set or clear the line.
    */
    public void setDtr (boolean on)
    {
        try
        {
            setDtr (fd, on);
        }
        catch (IOException e)
        {
        }
    }

    private void setRtsControl ()
        throws HIOException
    {
        try
        {
            if (rtsControl.equals ("low"))
                setRtsControl (fd, 0);
            else if (rtsControl.equals ("high"))
                setRtsControl (fd, 1);
            else if (rtsControl.equals ("flow"))
                setRtsControl (fd, 2);
            else if (rtsControl.equals ("rs485"))
                setRtsControl (fd, 3);
        }
        catch (Exception e)
        {
            // Only report exception if user is actually trying to use
            // RTS for flow-control or RS485-control.

            if (rtsControl.equals ("flow") || rtsControl.equals ("rs485"))
            {
                HIOException ioe = new HIOException ("I107",
                    "Can't set serial RTS control");
                ioe.initCause (e);
                throw ioe;
            }
        }
    }

    /**
    * Sets the use of the RTS line.
    * @param rtsc how to use the RTS line. Must be one of:
    * <ul>
    * <li>"flow" - use for software flow control.</li>
    * <li>"rs485" - use for controlling an RS232/RS485 converter.</li>
    * <li>"high" - set and leave high.</li>
    * <li>"low" - set and leave low.</li>
    * </ul>
    * @throws IOException if the RTS usage could not be set. No exception
    * is thrown if {@code rtsc} is "high" or "low" - failure to set the line
    * is ignored.
    */
    public void setRtsControl (String rtsc)
        throws HIOException
    {
        synchronized (readLock)
        {
            synchronized (writeLock)
            {
                rtsControl = rtsc;
                if (fd >= 0)
                    setRtsControl ();
            }
        }
    }

    private void throwConnectionClosed ()
        throws HEOFException
    {
        throw new HEOFException ("I100",
            "Connection " + getName () + " closed");
    }

    @Override
    public void write (byte [] data, int offset, int len)
        throws IOException, InterruptedException
    {
        synchronized (writeLock)
        {
            if (fd < 0)
                throwConnectionClosed ();
            try
            {
                write (fd, data, offset, len);
                timeLastWrite = System.nanoTime ();
            }
            catch (IOException e)
            {
                close (fd);
                fd = -1;
                throw e;
            }
        }
    }

    @Override
    public int read (byte [] data, int offset, int len,
            int timeout, boolean first)
        throws IOException, InterruptedException
    {
        if (len == 0)
            return 0;

        synchronized (readLock)
        {
            if (fd < 0)
                throwConnectionClosed ();
            setTimeout (fd, timeout);
            try
            {
                int n = read (fd, data, offset, len);
                if (n == 0)
                    throw new HInterruptedIOException ("I120", "Timed out");
                timeLastRead = System.nanoTime ();
                return n;
            }
            catch (HInterruptedIOException e)
            {
                throw e;
            }
            catch (RecoverableIOException e)
            {
                throw e;
            }
            catch (IOException e)
            {
                reporter.error (Exceptions.getHelpId (e),
                    "%s: %s",
                    getName (),
                    Exceptions.getMessage (e));
                close (fd);
                fd = -1;
                throw e;
            }
        }
    }

    @Override
    public void drain ()
    {
        synchronized (writeLock)
        {
            if (fd >= 0)
                drain (fd);
        }
    }

    @Override
    public void flush ()
    {
        synchronized (writeLock)
        {
            if (fd >= 0)
                flush (fd);
        }
    }

    @Override
    public void close ()
    {
        synchronized (readLock)
        {
            synchronized (writeLock)
            {
                if (fd < 0)
                    return;
                close (fd);
                fd = -1;
                reporter.info (null, "Closed serial connection %s", getName ());
            }
        }
    }

    /**
    * Gets the serial port names known to the comms package.
    * @return serial port names.
    */
    public native static String [] getPortNames ();

    /**
    * Try to load a native serial library.
    * @param name name of library to be loaded.
    * @return true if the library was loaded successfully, false otherwise.
    */
    private static boolean tryLoad (String name)
    {
        try
        {
// System.out.println ("trying to load serial library " + name);
            System.loadLibrary (name);
// System.out.println ("loaded library " + name + " - checking linkage");
            checkLoaded ();
// System.out.println ("loaded serial library " + name + " OK");
            return true;
        }
        catch (UnsatisfiedLinkError e)
        {
// System.out.println ("failed to load serial library " + name);
// e.printStackTrace ();
            return false;
        }
    }

    /**
    * Tests whether the native serial library can be loaded.
    * If the native library cannot be loaded, attempts to call other methods
    * in this class will cause an UnsatisfiedLinkError.
    * Note that even if the library can be loaded, there may not be any
    * serial ports - call {@link #getPortNames} to check for this.
    * @return true if the native serial library can be loaded, false
    * otherwise.
    */
    public static boolean isAvailable ()
    {
        try
        {
            checkLoaded ();
// System.out.println ("serial library already loaded");
        }
        catch (UnsatisfiedLinkError e)
        {
            if (Installer.isJvm32 ())
            {
                if (tryLoad ("jserial32"))
                    return true;
                if (tryLoad ("jserial32a"))
                    return true;
            }
            else if (Installer.isJvm64 ())
            {
                if (tryLoad ("jserial64"))
                    return true;
            }
            else
            {
                if (tryLoad ("jserial64"))
                    return true;
                if (tryLoad ("jserial32"))
                    return true;
                if (tryLoad ("jserial32a"))
                    return true;
            }
            return false;
        }

        return true;
    }

    /**
    * Sets/clears the Windows "hidden" atrribute of a file.
    * <p>This has nothing to do with serial interfaces! It was put here to
    * avoid having a separate native library for just one simple method.
    * @param name name of file.
    * @param hidden whether to set or clear the "hidden" attribute.
    */
    public static void setHidden (String name, boolean hidden)
        throws IOException
    {
        if (!isAvailable ())
            return;
        if (hidden)
        {
            SerialConnection.setAttributes (name,
                SerialConnection.ATTRIBUTE_HIDDEN, 0);
        }
        else
        {
            SerialConnection.setAttributes (name,
                0, SerialConnection.ATTRIBUTE_HIDDEN);
        }
    }

    @Override
    public String getName ()
    {
        if (name != null && !name.equals (""))
            return name;
        return portName;
    }

    @Override
    public void setName (String name)
    {
        this.name = name;
    }
}

