
package uk.co.wingpath.registration;

import java.math.BigInteger;
import java.io.*;
import java.util.*;
import java.util.List;
import java.text.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.jar.*;
import java.nio.channels.*;
import java.lang.management.*;
import uk.co.wingpath.io.SerialConnection;


/**
* This class provides dialogs that (in conjunction with the Wingpath website)
* enable a user to register a product, and methods for checking that a
* product has been registered.
*/
public class Registration
{
    // DateFormat for tracing times

    private static DateFormat df = DateFormat.getDateTimeInstance ();

    // Timer for periodic registration checking.

    private static java.util.Timer checkTimer = null;

    // Product major & minor version numbers.

    private static int majorVersion;
    private static int minorVersion;

    // Lock used to prevent more than instance of the product running.

    private static FileLock regFileLock = null;
    private static long lockFileLastModified = 0;

    static synchronized void startChecker (final int product,
        final int majorVersion)
    {
        if (checkTimer != null)
            return;
// System.out.println ("Registration.startChecker");
        // Schedule periodic checking for expiry of evaluation or
        // deregistration.
        checkTimer = new java.util.Timer (true);
        checkTimer.schedule (
            new TimerTask ()
            {
                public void run ()
                {
// System.out.println ("checkTimer");
                    Details details = loadRegistration (product,
                        majorVersion, true);

                    // Check that lock file has not been deleted.

                    if (regFileLock != null)
                    {
                        File lockFile = getLockFile (product, majorVersion);
// System.out.println ("check lock " + lockFile + " " + lockFileLastModified + " " + lockFile.lastModified ());
                        if (!lockFile.exists () ||
                            lockFile.lastModified () != lockFileLastModified)
                        {
                            System.exit (1);
                        }
                    }
                    // Check for registration expiry.

                    switch (details.status)
                    {
                    case Details.EXPIRED:
                    case Details.EXPIREDF:
                    case Details.DEREGISTERED:
                    case Details.CORRUPT:
                        System.exit (1);
                        break;

                    case Details.EVAL:
                        // Keep times in funny file up to date.
                        writeFunnyFile (details);
                        break;
                    }
                }
            },
            15 * 1000, 15 * 1000);
    }

    static String formatTime (long t)
    {
        return df.format (new Date (t * 1000));
    }

    private static int getObscureInt (byte [] data, int off)
    {
        return
            ((data [off + 3] ^ 0x4a) & 0xff) |
            (((data [off + 1] ^ 0xb2) & 0xff) << 8) |
            (((data [off + 0] ^ 0x31) & 0xff) << 16) |
            (((data [off + 2] ^ 0x78) & 0xff) << 24);
    }

    private static void putObscureInt (byte [] data, int off, int n)
    {
        data [off + 3] = (byte) (n ^ 0x4a);
        data [off + 1] = (byte) ((n >> 8) ^ 0xb2);
        data [off + 0] = (byte) ((n >> 16) ^ 0x31);
        data [off + 2] = (byte) ((n >> 24) ^ 0x78);
    }

    private Registration ()
    {
    }

    // Get directory that used to be used for registration files.

    private static File getOldRegDir ()
    {
        return new File (System.getProperty ("user.home"));
    }

    // Get directory to be used for registration files.

    private static String [] regDirNames =
    {
        "AppData\\Local",
        "Local Settings\\Application Data",
        "Application Data",
    };

    private static File getRegDir ()
    {
        String osName = System.getProperty ("os.name");
        File homeDir = new File (System.getProperty ("user.home"));
        File regDir = homeDir;
        if (osName.indexOf ("Windows") != -1)
        {
            for (String dirName : regDirNames)
            {
                File dir = new File (homeDir, dirName);
                if (dir.exists ())
                {
                    regDir = dir;
                    break;
                }
// System.out.println ("   " + dir + " not found");
            }
        }
// System.out.println ("regDir: " + regDir);
        if (!regDir.canWrite ())
        {
            JOptionPane.showMessageDialog (null,
                "Cannot register or check registration.\n" + 
                "You do not have permission to write to\n" +
                "the directory " + regDir,
                "Error", JOptionPane.ERROR_MESSAGE);
            System.exit (9);
        }
        return regDir;
    }

    // Get name of registration file.

    private static String getRegFileName (int product, int majorVersion)
    {
        return ".wingpath" +
                ((majorVersion - 1) * Crypt.MAX_PRODUCT + product) + ".reg";
    }

    // Get lock file.

    private static File getLockFile (int product, int majorVersion)
    {
        String lockFileName = ".wingpath" +
            ((majorVersion - 1) * Crypt.MAX_PRODUCT + product) + ".lck";
        File lockFile = new File (getRegDir (), lockFileName);
        return lockFile;
    }

    // Get name of funny file.

    private static String getFunnyFileName (int product, int majorVersion)
    {
        int n = ((majorVersion - 1) * Crypt.MAX_PRODUCT + product) * 309 +
                    21973;
        String osName = System.getProperty ("os.name");
        if (osName.indexOf ("Windows") != -1)
        {
            return "Nfa" + n + "." + "dat";
        }
        else
        {
            return ".nfa" + n;
        }
    }

    private synchronized static Details loadRegistration (int product,
        int majorVersion, boolean checkTime)
    {
        Details d = readRegFile (product, majorVersion);
        Details fd = readFunnyFile (product, majorVersion,
            d.key == null ? Crypt.CHAR_VERSION_CURRENT : d.key.keyVersion);
        if (d.status == Details.CORRUPT ||
            fd.status == Details.CORRUPT ||
            d.status == Details.NEW && fd.status != Details.NEW ||
            d.status != Details.NEW && fd.status == Details.NEW ||
            d.key != null && !d.key.equals (fd.key))
        {
            // Corrupted/inconsistent registration data.
            // Assume that it has been copied or tampered with, and don't
            // allow the user to do anything except enter a valid full
            // registration key.

// System.out.println ("Corruption:");
// System.out.println ("  d.status " + d.status);
// System.out.println ("  fd.status " + fd.status);
// if (d.key != null)
// System.out.println ("  d.key.equals (fd.key) " + d.key.equals (fd.key));
            d.buildExpiredKey (fd, Details.CORRUPT);
        }
        else if (d.status == Details.NEW)
        {
        }
        else if (d.status == Details.EXPIRED ||
            d.status == Details.EXPIREDF ||
            d.status == Details.DEREGISTERED)
        {
            d.buildExpiredKey (d, d.status);
        }
        else if (checkTime && d.status == Details.EVAL && fd.timeLast != 0)
        {
            int timeNow = (int) (System.currentTimeMillis () / 1000);
            if (timeNow >= fd.timeLast)
            {
                fd.timeLeft -= timeNow - fd.timeLast;
                fd.timeLast = timeNow;
// System.out.println ("loadRegistration:");
// System.out.println ("timeLast " + fd.timeLast + " (" + formatTime (fd.timeLast) + ")");
// System.out.println ("timeLeft " + fd.timeLeft + " (" + ((double) fd.timeLeft / (60 * 60)) + " hours)");
                if (fd.timeLeft <= 0)
                {
                    d.buildExpiredKey (d, Details.EXPIREDF);
                    fd.timeLeft = 0;
                }
            }
            else
            {
                // System clock has been put back.
                fd.timeLast = timeNow;
            }
        }
        d.timeLast = fd.timeLast;
        d.timeLeft = fd.timeLeft;
        if (d.key == null)
            d.random = fd.random;
        return d;
    }

    private static String getExceptionMessage (Throwable ex)
    {
        String msg = ex.getMessage ();
        if (msg == null) 
            msg = "";
        Throwable c = ex.getCause ();
        if (c != null && c.getMessage () != null)
            msg = msg + ": " + c.getMessage (); 
        return msg;
    }

    // Read registration file and put information read from file into
    // a Details object.
    // If the file exists, but the user has not yet registered, the key
    // field should be an empty string. The user and company fields may be
    // empty, or they may contain values entered by the user.
    // If the user has registered, the key field should be a key that
    // is consistent with: the user and company fields, the product code, and
    // the file modification time.
    // Obviously, if the user has tampered with the file, it could contain
    // anything.

    private synchronized static Details readRegFile (int product,
        int majorVersion)
    {
// System.out.println ("readRegFile: product " + product + " majorVersion " + majorVersion);
        Details d = new Details (product, majorVersion);
        String regFileName = getRegFileName (product, majorVersion);
// System.out.println ("regFileName: " + regFileName);
        File regFile = new File (getRegDir (), regFileName);
        if (!regFile.exists ())
        {
            regFile = new File (getOldRegDir (), regFileName);
            d.writeRequired = true;
        }
        if (!regFile.exists ())
        {
// System.out.println ("readRegFile: doesn't exist: " + regFile);
            return d;
        }
        BufferedReader in = null;

        try
        {
            long mt = regFile.lastModified () / 1000;
            d.modTime = (int) (mt % Crypt.MAX_MODTIME);
            d.modTime1 = (int) ((mt - 60 * 60) % Crypt.MAX_MODTIME);
            d.modTime2 = (int) ((mt + 60 * 60) % Crypt.MAX_MODTIME);
// System.out.println ("readRegFile: modTime " + d.modTime + " " + d.modTime1 + " " + d.modTime2);
            in = new BufferedReader (new FileReader (regFile));
            String line;

            while ((line = in.readLine ()) != null)
            {
                if (line.equals (""))
                    continue;
                if (line.charAt (0) != '#')
                {
                    // String [] s = line.split ("\t");

                    int n = 1;

                    for (int i = 0 ; i < line.length () ; i++)
                    {
                        if (line.charAt (i) == '\t')
                            n++;
                    }
// System.out.println ("readRegFile: n " + n);
                    String [] s = new String [n];
                    int lasti = 0;
                    int j = 0;
                    for (int i = 0 ; i < line.length () ; i++)
                    {
                        if (line.charAt (i) == '\t')
                        {
                            s [j++] = line.substring (lasti, i);
                            lasti = i + 1;
                        }
                    }
                    s [j] = line.substring (lasti);

                    if (s.length >= 3)
                    {
                        if (!s [0].equals (""))
                            d.key = Key.create (s [0]);
                        d.user = s [1];
                        d.company = s [2];
                        if (s.length >= 5)
                        {
                            d.newUser = s [3];
                            d.newCompany = s [4];
                        }
                        else
                        {
                            // Old format registration file

                            d.newUser = d.user;
                            d.newCompany = d.company;
                        }
// System.out.println ("readRegFile:" + s [0] + ":" + d.user + ":" +
// d.company + ":" + d.newUser + ":" + d.newCompany);
                        if (d.key == null)
                        {
// System.out.println ("readRegFile: new");
                            d.status = Details.NEW;
                        }
                        else if (d.key.product != d.product)
                        {
// System.out.println ("readRegFile: Corrupt 1");
                            d.status = Details.CORRUPT;
                        }
                        else if (d.key.majorVersion != d.majorVersion)
                        {
// System.out.println ("readRegFile: Corrupt 1v");
                            d.status = Details.CORRUPT;
                        }
                        else if (d.key.hash != Crypt.hash (d.user + d.company))
                        {
// System.out.println ("readRegFile: Corrupt 2");
                            d.status = Details.CORRUPT;
                        }
                        else if (!d.key.hasValidExpiry ())
                        {
// System.out.println ("readRegFile: Corrupt 3");
                            d.status = Details.CORRUPT;
                        }
                        else if (Math.abs (d.key.modTime - d.modTime) <= 2 ||
                            Math.abs (d.key.modTime - d.modTime1) <= 2 ||
                            Math.abs (d.key.modTime - d.modTime2) <= 2)
                        {
                            d.setStatus ();
                            d.modTime = d.key.modTime;
                            d.random = d.key.random;
                        }
                        else
                        {
// System.out.println ("readRegFile: Corrupt 4");
// System.out.println ("  key.modTime " + d.key.modTime);
                            d.status = Details.CORRUPT;
                        }
                        return d;
                    }
                }
            }
        }
        catch (IOException e)
        {
            JOptionPane.showMessageDialog (null,
                "Cannot read registration file:\n" +
                getExceptionMessage (e),
                "Registration error", JOptionPane.ERROR_MESSAGE);
            System.exit (9);
        }
        finally
        {
            try
            {
                if (in != null)
                    in.close ();
            }
            catch (IOException e)
            {
            }
        }

// System.out.println ("readRegFile: bad format");
        d.status = Details.CORRUPT;
        return d;
    }

    private synchronized static Details readFunnyFile (int product,
        int majorVersion, char version)
    {
        Details d = new Details (product, majorVersion);
        String funnyFileName = getFunnyFileName (product, majorVersion);
        File funnyFile = new File (getRegDir (), funnyFileName);
        if (!funnyFile.exists ())
        {
            funnyFile = new File (getOldRegDir (), funnyFileName);
            d.writeRequired = true;
        }
        if (!funnyFile.exists ())
        {
// System.out.println ("readFunnyFile: doesn't exist");
            return d;
        }
        FileInputStream in = null;

        try
        {
            in = new FileInputStream (funnyFile);
            byte [] buf = new byte [100];
            int len = in.read (buf);
            if (len < 0)
            {
                d.status = Details.CORRUPT;
                return d;
            }
            long mt = funnyFile.lastModified () / (60 * 1000);
            d.modTime = (int) (mt % Crypt.MAX_MODTIME);
            d.modTime1 = (int) ((mt + 60) % Crypt.MAX_MODTIME);
            d.modTime2 = (int) ((mt - 60) % Crypt.MAX_MODTIME);
// System.out.println ("readFunnyFile: modTime " + d.modTime + " " + d.modTime1 + " " + d.modTime2);

            byte [] keyData;

            if (len >= 16)
            {
// System.out.println ("timeLast " + d.timeLast + " (" + formatTime (d.timeLast) + ")");
// System.out.println ("timeLeft " + d.timeLeft + " (" + ((double) d.timeLeft / (60 * 60)) + " hours)");
                d.timeLast = getObscureInt (buf, 0);
                d.timeLeft = getObscureInt (buf, 4);

                keyData = new byte [len - 8];

                for (int i = 8 ; i < len ; i++)
                    keyData [i - 8] = buf [i];
            }
            else
            {
                // Old style funny file, without times.
                // This is very unlikely to happen during an evaluation
                // (the user must have started the evaluation with an old
                // version of the product, and has now downloaded a new
                // version), so we don't need to worry too much about
                // checking times - just rely on the expiry date in the key.
// System.out.println ("funny file without times");

                keyData = new byte [len];

                for (int i = 0 ; i < len ; i++)
                    keyData [i] = buf [i];
            }

            d.key = Key.create (keyData, version);
            d.user = "";
            d.company = "";
            d.newUser = "";
            d.newCompany = "";
            if (d.key == null)
            {
// System.out.println ("readFunnyFile: new");
                d.status = Details.NEW;
            }
            else if (d.key.product != d.product ||
                d.key.majorVersion != d.majorVersion ||
                !d.key.hasValidExpiry ())
            {
// System.out.println ("readFunnyFile: Corrupt");
                d.status = Details.CORRUPT;
            }
            else if (d.key.modTime == d.modTime ||
                d.key.modTime == d.modTime1 ||
                d.key.modTime == d.modTime2)
            {
                d.setStatus ();
                d.modTime = d.key.modTime;
                d.random = d.key.random;
            }
            else
            {
// System.out.println ("readFunnyFile: Corrupt");
                d.status = Details.CORRUPT;
            }
            return d;
        }
        catch (IOException e)
        {
            // Don't tell user we can't read file!
// System.out.println ("exception: " + e.getMessage ());
// e.printStackTrace ();
        }
        finally
        {
            try
            {
                if (in != null)
                    in.close ();
            }
            catch (IOException e)
            {
            }
        }

// System.out.println ("readFunnyFile: can't read");
        d.status = Details.CORRUPT;
        return d;
    }

    synchronized static void saveRegistration (Details d)
    {
        writeRegFile (d);
        writeFunnyFile (d);
        d.writeRequired = false;
    }

    // Write "funny" file. This file is used to store a second copy of
    // the registration key, so that if the user deletes the registration
    // file we should still be able to detect that they have registered
    // the product and determine whether their evaluation period has expired.
    // The file is given a meaningless name and an old modification time, so
    // that it will be less conspicuous. The registration key is encoded in
    // binary - again to make it less recognisable.
    // The registration information in the funny file is incomplete (key only,
    // no user or company name).

    private synchronized static void writeFunnyFile (Details d)
    {
        if (d.key == null)
            d.buildNewKey ();

        FileOutputStream out = null;

        try
        {
            String funnyFileName = getFunnyFileName (d.product, d.majorVersion);
            File funnyFile = new File (getRegDir (), funnyFileName);
            File oldFunnyFile = new File (getOldRegDir (), funnyFileName);
            if (!oldFunnyFile.exists ())
                oldFunnyFile = funnyFile;

            // If funny file exists, preserve its modification time,
            // otherwise set it to approximately 3 months ago.

            long t;
            if (oldFunnyFile.exists ())
            {
                t = oldFunnyFile.lastModified ();
// System.out.println ("writeFunnyFile: exists");
            }
            else
            {
// System.out.println ("writeFunnyFile: does not exist: d.modTime " + d.modTime);
                t = System.currentTimeMillis () -
                    ((100L * 24 + 7) * 60 + 13) * 60 * 1000;
            }
            t /= Crypt.MAX_MODTIME * 60 * 1000;
            t = ((t * Crypt.MAX_MODTIME + d.modTime) * 60 + 17) * 1000;
            if (!funnyFile.equals (oldFunnyFile))
                oldFunnyFile.delete ();
            if (funnyFile.exists ())
                SerialConnection.setHidden (funnyFile.getPath (), false);
            out = new FileOutputStream (funnyFile);
// System.out.println ("timeLast " + d.timeLast + " (" + formatTime (d.timeLast) + ")");
// System.out.println ("timeLeft " + d.timeLeft + " (" + ((double) d.timeLeft / (60 * 60)) + " hours)");
            byte [] timeData = new byte [8];
            putObscureInt (timeData, 0, d.timeLast);
            putObscureInt (timeData, 4, d.timeLeft);
            out.write (timeData);
            byte [] buf = d.key.encodeBytes ();
            out.write (buf);
            out.close ();
            funnyFile.setLastModified (t);
            SerialConnection.setHidden (funnyFile.getPath (), true);
        }
        catch (IOException e)
        {
            // Don't tell the user if we can't write the file!
// System.out.println ("exception: " + e.getMessage ());
// e.printStackTrace ();
        }
        finally
        {
            try
            {
                if (out != null)
                   out.close ();
            }
            catch (IOException e)
            {
            }
        }
    }

    private synchronized static void writeRegFile (Details d)
    {
        try
        {
            String regFileName = getRegFileName (d.product, d.majorVersion);
            File regFile = new File (getRegDir (), regFileName);
            File oldRegFile = new File (getOldRegDir (), regFileName);
            if (!regFile.equals (oldRegFile))
                oldRegFile.delete ();
            if (regFile.exists ())
                SerialConnection.setHidden (regFile.getPath (), false);
            PrintWriter out = new PrintWriter (new FileOutputStream (regFile));
/*
            out.println ("### DO NOT MODIFY OR DELETE THIS FILE ###");
            out.println ("### If you do, your Wingpath software ###");
            out.println ("### will no longer be registered.     ###");
*/
            out.println ((d.key == null ? "" : d.key.encodeStr ()) +
                "\t" + d.user + "\t" + d.company +
                "\t" + d.newUser + "\t" + d.newCompany);
            out.close ();
            long t = regFile.lastModified ();
            t -= t % (Crypt.MAX_MODTIME * 1000);
            t += d.modTime * 1000;
            regFile.setLastModified (t);
            SerialConnection.setHidden (regFile.getPath (), true);
        }
        catch (IOException e)
        {
            JOptionPane.showMessageDialog (null,
                "Cannot write registration file:\n" +
                getExceptionMessage (e),
                "Registration error", JOptionPane.ERROR_MESSAGE);
            System.exit (9);
        }
    }

    // Looks for a full registration of a previous version of the product.
    // Returns the version if found, otherwise 0.

    private static Details findPreviousDetails (int product, int majorVersion)
    {
        for (int version = majorVersion - 1 ; version >= 1 ; version--)
        {
            Details d = loadRegistration (product, version, false);
            if (d.status == Details.FULL)
            {
// System.out.println ("Found full registration for version " + version);
                return d;
            }
        }

        return null;
    }

    /**
    * This interface should be implemented to find out whether the product
    * has been registered, and whether the registration is full or
    * evaluation. Instances of this interface are passed to the
    * {@link #showDialog showDialog} and {@link #check check} methods.
    */
    public interface Callback
    {
        /**
        * This method is called by the {@link #showDialog showDialog} and
        * {@link #check check} methods to inform the caller that the product
        * has been registered, and indicate whether the registration is full or
        * evaluation.
        * <p>This method will not be called if the product has not been
        * registered.
        * <p>The method is called from the AWT Event Dispatch Thread.
        * @param full {@code true} if the product has been fully registered.
        */
        void registered (boolean full);
    }

    private static void checkProductArgs (int product, String productVersion)
    {
        if (product < 1 || product >= Crypt.MAX_PRODUCT)
        {
            throw new IllegalArgumentException ("product " + product +
                " out of range 1.." + (Crypt.MAX_PRODUCT - 1));
        }
        double v = Double.parseDouble (productVersion);
        majorVersion = (int) v;
        minorVersion = (int) (v * 100.0 + 0.1) % 100;
        if (majorVersion < 1 || majorVersion >= Crypt.MAX_MAJOR_VERSION)
        {
            throw new IllegalArgumentException ("majorVersion " +
                majorVersion + " out of range 1.." +
                (Crypt.MAX_MAJOR_VERSION - 1));
        }
    }

    /**
    * Checks whether the specified product is registered.
    * <p>If the product is registered for evaluation only,
    * it shows a dialog reminding the user that the evaluation period will
    * end soon, inviting them to purchase the product, and enabling them
    * to enter a full registration key.
    * <p>If the product is not registered at all, it shows a dialog that
    * allows the user to enter a registration key (full or evaluation).
    * <p>If the product is fully registered, no dialog is shown.
    * <p>This method would normally be called before any other frame/dialog is
    * displayed.
    * <p>The dialog displayed by this method is not modal, and the method will
    * return before the user has closed the dialog.
    * Therefore, the call of this method should be the
    * last thing done from {@code main}, and the rest of the program should
    * be run from the callback {@link Callback#registered registered} method.
    * <p>This method (or rather the dialog created by it) will call
    * {@link System#exit System.exit} if the product is not registered after the
    * dialog has been closed.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param productVersion version number of the product, as a decimal string
    * with two decimal places (e.g. "2.13").
    * @param productName name of the product, for display in the dialog
    * (e.g. "Modsak", "ModSlaveSim").
    * @param callback callback to be informed of registration status.
    */
    public static void check (final int product, final String productVersion,
        final String productName, final Callback callback)
    {
        checkProductArgs (product, productVersion);
        if (!EventQueue.isDispatchThread ())
        {
            EventQueue.invokeLater (
                new Runnable ()
                {
                    public void run ()
                    {
                        check (product, productVersion, productName, callback);
                    }
                });
            return;
        }
        assert EventQueue.isDispatchThread ();

        Details details = loadRegistration (product, majorVersion, true);
// System.out.println ("check " + details.status);
        Details previousDetails = findPreviousDetails (product, majorVersion);
        details.minorVersion = minorVersion;
        Gui gui = new Gui (productName, details, previousDetails, callback);

        switch (details.status)
        {
        case Details.NEW:
            gui.show ("eval1");
            break;

        case Details.EVAL:
            // Update times in funny file.

            writeFunnyFile (details);

            // Remind user that their evaluation period will
            // expire in n days, and give them an opportunity
            // to purchase.

            gui.show ("remind");
            break;

        case Details.EXPIRED:
        case Details.EXPIREDF:
        case Details.DEREGISTERED:
        case Details.CORRUPT:
            gui.show (previousDetails != null ? "upgradev" : "full1");
            break;

        case Details.FULL:
// System.out.println ("registered true");
            if (details.writeRequired)
                saveRegistration (details);
            Registration.startChecker (product, majorVersion);
            callback.registered (true);
            break;

        default:
            // Shouldn't happen

            System.exit (9);
            break;
        }
    }

    /**
    * Shows a dialog that enables the user enter a full registration key.
    * <p>The dialog displayed by this method is not modal, and the method will
    * return before the user has closed the dialog.
    * <p>This method should only be called when the user already has an
    * evaluation registration key or a restricted full registration key -
    * i.e. this is the method to call from the Help->Register/Upgrade menu item.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param productVersion version number of the product, as a decimal string
    * with two decimal places (e.g. "2.13").
    * @param productName name of the product, for display in the dialog
    * (e.g. "Modsak", "ModSlaveSim").
    * @param callback callback to be informed of registration status when
    * the dialog is closed.
    */
    public static void showDialog (final int product,
        final String productVersion,
        final String productName, final Callback callback)
    {
// System.out.println ("showDialog " + product + " " + productVersion + " " + productName);
        checkProductArgs (product, productVersion);
// System.out.println ("   majorVersion " + majorVersion);
        if (!EventQueue.isDispatchThread ())
        {
            EventQueue.invokeLater (
                new Runnable ()
                {
                    public void run ()
                    {
                        showDialog (product, productVersion, productName,
                            callback);
                    }
                });
            return;
        }
        assert EventQueue.isDispatchThread ();
        Details details = loadRegistration (product, majorVersion, true);
// System.out.println ("status " + details.status);
        if (details.status == Details.FULL)
        {
            if (details.key.licenceLevel == 0)
            {
                // Unrestricted full registration.
                // Shouldn't really happen - we are only supposed to be called
                // when the user is evaluating the product or has a restricted
                // registration.

                callback.registered (true);
                return;
            }
        }
        else if (details.status != Details.EVAL)
        {
            // We have allowed the program to start, but now we don't have
            // valid registration details, probably because the user has
            // tampered with the registration file and/or "funny" file, or the
            // key has expired while the program was running.

            System.exit (9);
        }

        Details previousDetails = findPreviousDetails (product, majorVersion);
        details.minorVersion = minorVersion;
        Gui gui = new Gui (productName, details, previousDetails, callback);
        gui.show (details.status == Details.FULL ? "upgradel" :
            previousDetails != null ? "upgradev" : "full1");
    }

    /**
    * Checks whether the specified product is registered.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product
    * @return {@code true} if the product is registered, either fully or
    * for evaluation.
    */
    public static boolean isRegistered (int product, int majorVersion)
    {
        Details details = Registration.loadRegistration (product,
            majorVersion, false);
        boolean registered =
             details.status == Details.FULL || details.status == Details.EVAL;
        if (registered)
            startChecker (product, majorVersion);
        return registered;
    }

    /**
    * Checks whether the specified product is fully registered.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product
    * @return {@code true} if the product is fully registered.
    */
    public static boolean isFullyRegistered (int product, int majorVersion)
    {
        Details details = Registration.loadRegistration (product,
            majorVersion, false);
        boolean fullyRegistered = details.status == Details.FULL;
        if (fullyRegistered)
            startChecker (product, majorVersion);
        return fullyRegistered;
    }

    /**
    * Returns the licence level for the specified product.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product
    * @return the licence level if the product is fully registered,
    * 0 otherwise.
    */
    public static int getLicenceLevel (int product, int majorVersion)
    {
        Details details = Registration.loadRegistration (product,
            majorVersion, false);
        if (details.status == Details.FULL)
            return details.key.licenceLevel;
        return 0;
    }

    /**
    * Returns the registration log ID.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product.
    * @return the log ID.
    */
    public static int getLogId (int product, int majorVersion)
    {
        Details details = Registration.loadRegistration (product,
            majorVersion, false);
        return details.key.logId;
    }

    /**
    * Checks whether the specified product has been deregistered.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product
    * @return {@code true} if the product has been deregistered.
    */
    public static boolean isDeregistered (int product, int majorVersion)
    {
        Details details = Registration.loadRegistration (product,
            majorVersion, false);
        return details.status == Details.DEREGISTERED;
    }

    /**
    * Deregisters the specified product.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product
    * @return "transfer key" that the user can email to us.
    */
    public static String deregister (int product, int majorVersion)
    {
// System.out.println ("deregister: product " + product + ", version " + productVersion);
        Details details = loadRegistration (product, majorVersion, false);
        details.buildOldKey ();
        saveRegistration (details);
        return details.key.getTransferKey ();
    }

    /**
    * Gets registration details.
    * <p>Information about the registration is returned as 5 or 6 strings
    * to be displayed one-per-line in the Help->About dialog.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product.
    * @return strings describing registration details.
    */
    public static String [] description (int product, int majorVersion)
    {
        Details details = loadRegistration (product, majorVersion, true);
        Key key = details.key;
        if (!(details.status == Details.EVAL || details.status == Details.FULL))
            System.exit (9);
        String [] a = new String [details.status == Details.EVAL ? 6 : 5];
        int i = 0;
        a [i++] = "Registration details:";
        String licenceType = "Evaluation";
        if (details.status == Details.FULL)
        {
            licenceType = "Full";
            if (key.licenceLevel != 0)
                licenceType += "-" + key.licenceLevel;
        }
        a [i++] = "   Licence type: " + licenceType;
        a [i++] = "   Registration key: " + key.encodeStr ();
        a [i++] = "   User: " + details.user;
        a [i++] = "   Company: " + details.company;
        if (details.status == Details.EVAL)
        {
            a [i++] = "Registration will expire " +
                (details.daysLeft () <= 1 ?
                    "today" :
                    "in " + details.daysLeft () + " days");
        }
        assert i == a.length;
        return a;
    }

    /**
    * Attempts to acquire an exclusive lock related to the registration file.
    * This is typically used to prevent more than one instance of the
    * program running simultaneously.
    * The lock will be released automatically when the program exits,
    * or it can be released using the {@link #unlock unlock} method.
    * @param product identification code for the product
    * (e.g. 23 for Modsak, 27 for ModSlaveSim).
    * @param majorVersion major version number of the product
    * @return {@code true} if the lock was acquired, {@code false} otherwise
    * (presumably because another instance of the program holds the lock).
    */
    public static boolean tryLock (int product, int majorVersion)
    {
        try
        {
            // Java FileLock is implemented on Linux using 'fcntl' - this
            // has two non-obvious consequences:
            //   1. The lock file has to be open for writing to get an exclusive
            //   lock.
            //   2. The registration file cannot be used as a lock file: the
            //   lock is released if the process "closes any file descriptor
            //   referring to a file on which locks are held" (quote from
            //   'fcntl' man page).
            // We must therefore use a dedicated lock file, which means the
            // user could defeat the locking by removing the lock file!
            // We handle this by setting the lock file modification time when
            // we lock it, and then periodically checking whether the lock
            // file has been deleted or modified (e.g. by deleting & re-creating
            // it). This period checking is done by the checkTimer started by
            // startChecker.
            // Note that deletion of the lock file is not a problem under
            // Windows - the file cannot be deleted while it is open.
            File lockFile = getLockFile (product, majorVersion);
            RandomAccessFile raf = new RandomAccessFile (lockFile, "rw");
// System.out.println ("tryLock " + lockFile);
            regFileLock = raf.getChannel ().tryLock ();
            if (regFileLock != null)
            {
                // Write the user and JVM names to the lock file.
                // We currently don't use this data, but it might help confuse
                // anyone that is trying to defeat the locking.
                String user = System.getProperty ("user.name");
                String pid = ManagementFactory.getRuntimeMXBean ().getName ();
// System.out.println ("user " + user + " " + pid);
                raf.writeUTF (user + " " + pid);
                lockFileLastModified = lockFile.lastModified ();
// System.out.println ("   locked");
                return true;
            }
        }
        catch (IOException e)
        {
        }

        unlock ();
        return false;
    }

    /**
    * Releases a lock obtained by {@link #tryLock tryLock}.
    */
    public static void unlock ()
    {
        if (regFileLock != null)
        {
            try
            {
                regFileLock.channel ().close ();
            }
            catch (IOException e)
            {
            }
            regFileLock = null;
        }
    }

    public static void main (String [] args)
    {
        System.out.println ("oldCryptMult " + Crypt.oldCryptMult);
        System.out.println ("oldCryptMod " + Crypt.oldCryptMod);
        System.out.println ("oldCryptMultInv " + Crypt.oldCryptMultInv);

        System.out.println ("cryptMultC " + Crypt.cryptMultC);
        System.out.println ("cryptModC " + Crypt.cryptModC);
        System.out.println ("cryptMultInvC " + Crypt.cryptMultInvC);

        // Check available space in registration key.

        BigInteger n = BigInteger.valueOf (1);
        n = n.multiply (Crypt.maxLicenceLevel);     // 100
        n = n.multiply (Crypt.maxEvalPeriod);       // 50
        n = n.multiply (Crypt.maxLogId);            // 100000
        n = n.multiply (Crypt.maxManual);           // 2
        n = n.multiply (Crypt.maxMajorVersion);     // 10
        n = n.multiply (Crypt.maxProduct);          // 50
        n = n.multiply (Crypt.maxModTime);          // 67
        n = n.multiply (Crypt.maxHash);             // 0x10000
        n = n.multiply (Crypt.maxDate);             // 20000
        System.out.println ("max key value " + n +
            " = cryptModC / " + Crypt.cryptModC.divide (n) +
            " = " + n.divide (Crypt.cryptModC) + " / cryptMod");
 
        // Check available space in product ID.

        n = BigInteger.valueOf (1);
        n = n.multiply (Crypt.maxLicenceLevel);     // 100
        n = n.multiply (Crypt.maxDate);             // 20000
        n = n.multiply (Crypt.maxHasSerial);        // 3
        n = n.multiply (Crypt.maxRandom);           // 100
        n = n.multiply (Crypt.maxLogId);            // 100000
        n = n.multiply (Crypt.maxMajorVersion);     // 10
        n = n.multiply (BigInteger.valueOf (Crypt.MAX_MAGIC));
        n = n.multiply (Crypt.maxStatus);           // 10
        n = n.multiply (Crypt.maxLogId);            // 100000
        n = n.multiply (Crypt.maxVendor);           // 10
        n = n.multiply (Crypt.maxOS);               // 20
        n = n.multiply (Crypt.max64Bit);            // 3
        n = n.multiply (Crypt.maxJavaVersion);      // 10
        n = n.multiply (Crypt.maxHash);             // 0x10000
        n = n.multiply (Crypt.maxModTime);          // 67
        n = n.multiply (Crypt.maxMajorVersion);     // 10
        n = n.multiply (Crypt.maxMinorVersion);     // 100
        n = n.multiply (Crypt.maxProduct);          // 50
        System.out.println ("max product id " + n +
            " = cryptMod / " + Crypt.cryptModC.divide (n) + 
            " = " + n.divide (Crypt.cryptModC) + " / cryptMod");

        // Check available space in transfer key.

        n = BigInteger.valueOf (1);
        n = n.multiply (Crypt.maxLicenceLevel);     // 100
        n = n.multiply (Crypt.maxHash);
        n = n.multiply (Crypt.maxModTime);
        n = n.multiply (Crypt.maxLogId);
        n = n.multiply (Crypt.maxDate);
        n = n.multiply (Crypt.maxMajorVersion);
        n = n.multiply (Crypt.maxProduct);
        System.out.println ("max transfer key " + n +
            " = cryptMod / " + Crypt.cryptModA.divide (n) + 
            " = " + n.divide (Crypt.cryptModA) + " / cryptMod");

        System.exit (0);
    }
}

