
package uk.co.wingpath.registration;

import java.math.BigInteger;

class Key
{
    public final int product;       // Product code (e.g. 23 for Modsak)
    public final int majorVersion;   // Product major version (1..9)
    public final int modTime;       // Low-order modification time
    public final int hash;          // Hash code from user & company names
    public final int expiry;        // Expiry date / registration type
    public final int evalPeriod;    // Evaluation period
    public final int logId;         // Registration log ID
    public final int manual;        // 1 if key was manually generated
    public final int random;        // Random number from Product ID
    public final int licenceLevel;  // Licence level. 0 for unrestricted.
    public final char keyVersion;   // Key version

    Key (int product, int majorVersion, int modTime, int hash, int expiry,
        int logId, int manual, int random, int evalPeriod, int licenceLevel,
        char keyVersion)
    {
        assert product >= 1 && product < Crypt.MAX_PRODUCT : product;
        assert majorVersion >= 1 && majorVersion < Crypt.MAX_MAJOR_VERSION :
            majorVersion;
        this.product = product;
        this.majorVersion = majorVersion;
        this.modTime = modTime;
        this.hash = hash;
        this.expiry = expiry;
        this.logId = logId;
        this.manual = manual;
        this.random = random;
        this.evalPeriod = evalPeriod;
        this.licenceLevel = licenceLevel;
        this.keyVersion = keyVersion;
// System.out.println ("new Key: expiry " + expiry + " hash " + hash +
// " modTime " + modTime + " product " + product +
// " majorVersion " + majorVersion + " logId " + logId + " manual " + manual +
// " random " + random + " evalPeriod " + evalPeriod + " licenceLevel " +
// licenceLevel + " keyVersion " + keyVersion);
    }

    // Construct a key from a new-style decrypted key number

    public static Key create (BigInteger n, char version)
    {
        int random = 0;
        if (version >= Crypt.CHAR_VERSION_B)
        {
            random = n.mod (Crypt.maxRandom).intValue ();
            n = n.divide (Crypt.maxRandom);
        }
        int expiry = n.mod (Crypt.maxDate).intValue ();
        if (version >= Crypt.CHAR_VERSION_F && expiry > Crypt.MAX_EXPIRY)
            expiry += Crypt.DATE_BASE;
        n = n.divide (Crypt.maxDate);
        int hash = n.mod (Crypt.maxHash).intValue ();
        n = n.divide (Crypt.maxHash);
        int modTime = n.mod (Crypt.maxModTime).intValue ();
        n = n.divide (Crypt.maxModTime);
        int product = n.mod (Crypt.maxProduct).intValue ();
        n = n.divide (Crypt.maxProduct);
        int magic = n.mod (BigInteger.valueOf (Crypt.MAX_MAGIC)).intValue ();
        n = n.divide (BigInteger.valueOf (Crypt.MAX_MAGIC));
        int majorVersion = n.mod (Crypt.maxMajorVersion).intValue ();
        n = n.divide (Crypt.maxMajorVersion);
        int manual = n.mod (Crypt.maxManual).intValue ();
        n = n.divide (Crypt.maxManual);
        int logId = n.mod (Crypt.maxLogId).intValue ();
        if (version >= Crypt.CHAR_VERSION_E)
        {
            logId += Crypt.MAX_LOGID;
        }
        n = n.divide (Crypt.maxLogId);
        int evalPeriod = 0;
        if (version >= Crypt.CHAR_VERSION_C)
        {
            evalPeriod = n.mod (Crypt.maxEvalPeriod).intValue ();
            n = n.divide (Crypt.maxEvalPeriod);
        }
        int licenceLevel = 0;
        if (version >= Crypt.CHAR_VERSION_D)
        {
            licenceLevel = n.mod (Crypt.maxLicenceLevel).intValue ();
            n = n.divide (Crypt.maxLicenceLevel);
        }
        boolean valid = magic == Crypt.MAGIC_KEY && n.intValue () == 0;
// System.out.println ("new Key (new-style): expiry " + expiry + " hash " + hash +
// " modTime " + modTime + " product " + product +
// " majorVersion " + majorVersion + " manual " + manual + " logId " + logId +
// " remainder " + n + " magic " + magic);
        if (valid)
        {
            return new Key (product, majorVersion, modTime, hash,
                expiry, logId, manual, random, evalPeriod, licenceLevel,
                version);
        }
        else
        {
            return null;
        }
    }

    // Construct a key from an old-style decrypted key number

    public static Key createOld (BigInteger n)
    {
        int expiry = n.mod (Crypt.maxDate).intValue ();
        n = n.divide (Crypt.maxDate);
        int hash = n.mod (Crypt.maxHash).intValue ();
        n = n.divide (Crypt.maxHash);
        int modTime = n.mod (Crypt.maxModTime).intValue ();
        n = n.divide (Crypt.maxModTime);
        int product = n.mod (Crypt.maxProduct).intValue ();
        n = n.divide (Crypt.maxProduct);
        int majorVersion = n.mod (Crypt.maxMajorVersion).intValue () + 1;
        n = n.divide (Crypt.maxMajorVersion);
        int manual = n.mod (Crypt.maxManual).intValue ();
        n = n.divide (Crypt.maxManual);
        int logId = n.mod (Crypt.oldMaxLogId).intValue ();
        n = n.divide (Crypt.oldMaxLogId);
        int version = n.mod (Crypt.maxRegVersion).intValue ();
        n = n.divide (Crypt.maxRegVersion);
// System.out.println ("new Key (old-style): expiry " + expiry + " hash " + hash +
// " modTime " + modTime + " product " + product +
// " majorVersion " + majorVersion + " manual " + manual + " logId " + logId +
// " version " + version + " remainder " + n.intValue ());
        if (n.intValue () == 0 && product > 0)
        {
            return new Key (product, majorVersion, modTime, hash,
                expiry, logId, manual, 0, 0, 0,
                Crypt.CHAR_VERSION_A);
        }
        else
        {
            return null;
        }
    }

    // construct a registration Key object by decoding a registration
    // key string

    public static Key create (String str)
    {
        if (str.length () > 3 &&
            str.charAt (0) >= 'a' &&
            str.charAt (0) <= Crypt.CHAR_VERSION_CURRENT &&
            str.charAt (1) == Crypt.CHAR_KEY)
        {
            // New-style hex registration key.
            BigInteger n = Crypt.decrypt (str.substring (2), str.charAt (0));
            if (n == null)
            {
// System.out.println ("new Key: invalid");
                return null;
            }
            else
            {
                return create (n, str.charAt (0));
            }
        }
        else
        {
            // Old-style decimal registration key.
            BigInteger n = Crypt.oldDecrypt (str);
            return createOld (n);
        }
    }

    // encode key as a number

    BigInteger encode ()
    {
        assert product >= 1 && product < Crypt.MAX_PRODUCT : product;
        assert majorVersion >= 1 && majorVersion < Crypt.MAX_MAJOR_VERSION :
            majorVersion;
        BigInteger n = BigInteger.ZERO;
        n = n.multiply (Crypt.maxLicenceLevel);
        n = n.add (BigInteger.valueOf (licenceLevel));
        n = n.multiply (Crypt.maxEvalPeriod);
        n = n.add (BigInteger.valueOf (evalPeriod));
        n = n.multiply (Crypt.maxLogId);
        n = n.add (BigInteger.valueOf (logId % Crypt.MAX_LOGID));
        n = n.multiply (Crypt.maxManual);
        n = n.add (BigInteger.valueOf (manual));
        n = n.multiply (Crypt.maxMajorVersion);
        n = n.add (BigInteger.valueOf (majorVersion));
        n = n.multiply (BigInteger.valueOf (Crypt.MAX_MAGIC));
        n = n.add (BigInteger.valueOf (Crypt.MAGIC_KEY));
        n = n.multiply (Crypt.maxProduct);
        n = n.add (BigInteger.valueOf (product));
        n = n.multiply (Crypt.maxModTime);
        n = n.add (BigInteger.valueOf (modTime));
        n = n.multiply (Crypt.maxHash);
        n = n.add (BigInteger.valueOf (hash));
        n = n.multiply (Crypt.maxDate);
        int xp = expiry;
        if (keyVersion >= Crypt.CHAR_VERSION_F && expiry > Crypt.MAX_EXPIRY)
            xp -= Crypt.DATE_BASE;
        n = n.add (BigInteger.valueOf (xp));
        n = n.multiply (Crypt.maxRandom);
        n = n.add (BigInteger.valueOf (random));
        return n;
    }

    // encode key as string

    String encodeStr ()
    {
        BigInteger n = encode ();
        return "" + keyVersion + Crypt.CHAR_KEY + Crypt.encrypt (n, keyVersion);
    }

    // Encode as a byte array (for use in "funny" file).
    // Key is given a double dose of "encryption", and converted to
    // 2's complement form in a byte array.

    byte [] encodeBytes ()
    {
        BigInteger n = encode ();
        n = n.multiply (Crypt.cryptMultC);
        n = n.mod (Crypt.cryptModC);
        n = n.multiply (Crypt.cryptMultC);
        n = n.mod (Crypt.cryptModC);
        n = Crypt.addCheck (n);
        byte [] data = n.toByteArray ();
        return data;
    }

    // Construct a registration Key object by decoding a registration
    // key byte array (from a "funny" file).

    public static Key create (byte [] data, char version)
    {
        // Try de-crypting as new-style key.
        BigInteger n = new BigInteger (data);
        n = Crypt.removeCheck (n);
        if (n != null)
        {
// System.out.println ("assuming new-style funny key");
            // Hopefully a new-style key, but it's possible that an old-style
            // key will appear to have the check on the end.
            // If that happens, we will have to send the user a new
            // registration key.
            BigInteger m = n.multiply (Crypt.cryptMultInvC);
            m = m.mod (Crypt.cryptModC);
            m = m.multiply (Crypt.cryptMultInvC);
            m = m.mod (Crypt.cryptModC);
            Key k = create (m, version);
            return k;
        }
        else
        {
            // Presumably an old-style key
// System.out.println ("assuming old-style funny key");
            n = new BigInteger (data);
            n = n.multiply (Crypt.oldCryptMultInv);
            n = n.mod (Crypt.oldCryptMod);
            n = n.multiply (Crypt.oldCryptMultInv);
            n = n.mod (Crypt.oldCryptMod);
            n = n.divide (Crypt.maxRand);
            return createOld (n);
        }
    }

    // Construct a tranfer key from the registration details

    String getTransferKey ()
    {
        assert product >= 1 && product < Crypt.MAX_PRODUCT : product;
        assert majorVersion >= 1 && majorVersion < Crypt.MAX_MAJOR_VERSION :
            majorVersion;
        BigInteger n = BigInteger.ZERO;
        if (keyVersion >= Crypt.CHAR_VERSION_D)
        {
            n = n.multiply (Crypt.maxLicenceLevel);
            n = n.add (BigInteger.valueOf (licenceLevel));
        }
        n = n.multiply (Crypt.maxHash);
        n = n.add (BigInteger.valueOf (hash));
        n = n.multiply (Crypt.maxModTime);
        n = n.add (BigInteger.valueOf (modTime));
        n = n.multiply (BigInteger.valueOf (Crypt.MAX_MAGIC));
        n = n.add (BigInteger.valueOf (Crypt.MAGIC_TRANSFER));
        n = n.multiply (Crypt.maxLogId);
        n = n.add (BigInteger.valueOf (logId % Crypt.MAX_LOGID));
        n = n.multiply (Crypt.maxDate);
        n = n.add (BigInteger.valueOf (expiry));
        n = n.multiply (Crypt.maxMajorVersion);
        n = n.add (BigInteger.valueOf (majorVersion));
        n = n.multiply (Crypt.maxProduct);
        n = n.add (BigInteger.valueOf (product));
        String str = "" + keyVersion + Crypt.CHAR_TRANSFER +
            Crypt.encrypt (n, keyVersion);
        return str;
    }

    // check whether registration key is valid by comparing the
    // product code/version, hash code, and modTime encoded in it with
    // product code/version expected for this program, and the hash code and
    // modTime from the registration file.

    boolean isValid (Details d, String user, String company)
    {
        if (product != d.product)
        {
// System.out.println ("isValid: bad product " + product + " " + d.product);
            return false;
        }
        if (majorVersion != d.majorVersion)
        {
// System.out.println ("isValid: bad product version " + majorVersion + " " + d.majorVersion);
            return false;
        }
        if (hash != Crypt.hash (user + company))
        {
// System.out.println ("isValid: bad hash");
            return false;
        }
        if (random != d.random)
        {
// System.out.println ("isValid: bad random " + random + " " + d.random);
            return false;
        }
        if (Math.abs (modTime - d.modTime) <= 2 ||
            Math.abs (modTime - d.modTime1) <= 2 ||
            Math.abs (modTime - d.modTime2) <= 2)
        {
            return true;
        }
// System.out.println ("isValid: bad modTime " + modTime + ": " + d.modTime +
// " " + d.modTime1 + " " + d.modTime2);
        return false;
    }

    boolean hasExpired ()
    {
        switch (expiry)
        {
        case Crypt.FULL_LICENCE:
        case Crypt.NEW_LICENCE:
            return false;

        case Crypt.EXPIRED_LICENCE:
        case Crypt.EXPIREDF_LICENCE:
        case Crypt.CORRUPT_LICENCE:
        case Crypt.OLD_LICENCE:
            return true;

        default:
            return daysLeft () < 0;
        }
    }

    boolean isNewLicence ()
    {
        return expiry == Crypt.NEW_LICENCE;
    }

    boolean isFullLicence ()
    {
        return expiry == Crypt.FULL_LICENCE;
    }

    boolean isOldLicence ()
    {
        return expiry == Crypt.OLD_LICENCE;
    }

    boolean isManual ()
    {
        return manual != 0;
    }

    int daysLeft ()
    {
        int now = (int) (System.currentTimeMillis () /
            (1000 * 60 * 60 * 24));
        return expiry - now + 1;
    }

    boolean hasValidExpiry ()
    {
        switch (expiry)
        {
        case Crypt.FULL_LICENCE:
        case Crypt.NEW_LICENCE:
        case Crypt.EXPIRED_LICENCE:
        case Crypt.EXPIREDF_LICENCE:
        case Crypt.CORRUPT_LICENCE:
        case Crypt.OLD_LICENCE:
            return true;

        default:
            int daysLeft = daysLeft ();
            return daysLeft < Crypt.MAX_EVAL_PERIOD && daysLeft > -2 * 365;
        }
    }

    // Check whether system date looks sensible relative to the key date.
    // Assume that key is used on the day it was created, or on the following
    // day.
    boolean checkDate ()
    {
        switch (expiry)
        {
        case Crypt.FULL_LICENCE:
        case Crypt.NEW_LICENCE:
        case Crypt.EXPIRED_LICENCE:
        case Crypt.EXPIREDF_LICENCE:
        case Crypt.CORRUPT_LICENCE:
        case Crypt.OLD_LICENCE:
            return true;
        }

        int today = (int) (System.currentTimeMillis () /
            (1000 * 60 * 60 * 24));
// System.out.println ("today " + Registration.formatTime (today * 24 * 60 * 60));
// System.out.println ("expiry " + Registration.formatTime (expiry * 24 * 60 * 60));
        int keyDate = expiry;
        if (evalPeriod != 0)
        {
            keyDate -= (evalPeriod - 1);
// System.out.println ("keyDate " + Registration.formatTime (keyDate * 24 * 60 * 60));
            // Key was generated on keyDate.
            return today >= keyDate && today <= keyDate + 1;
        }
        else
        {
            keyDate -= 3;
// System.out.println ("keyDate3 " + Registration.formatTime (keyDate * 24 * 60 * 60));
            // Key was generated between keyDate-2 and keyDate.
            return today >= keyDate - 2 && today <= keyDate + 1;
        }
    }

    public boolean equals (Object o)
    {
        if (!(o instanceof Key))
            return false;
        Key k = (Key) o;
        return
            k.product == product &&
            k.majorVersion == majorVersion &&
            k.modTime == modTime &&
            k.hash == hash &&
            k.expiry == expiry &&
            k.logId == logId;
    }
}

