/**
 * Author: Chris Wilson dbareader@yepher.com
 * Bug fixes by: David Sontag webmaster@web-page-services.com
 * Last-edited June 5, 2005. Works with Palm Desktop 4.1
 *
 * Purpose: Example code for accessing the Palm datebook.dat file
 *
 * Inteteded use: Make it easy for a jsp to generate web base calendars
 * from Palms .dba file through an abstract interface.
 *
 * References Used:
 * http://www.geocities.com/Heartland/Acres/3216/palmrecs.htm
 *
 * Decode Document:
 * http://www.geocities.com/Heartland/Acres/3216/datebook_dat.htm
 *
 * Refernce implementation:
 * Scott Leighton's dbtest.pl (December 12, 1999) where decode document was
 * not clear
 *
 * Permission granted to freely distribute provided that no
 * money changes hands, otherwise contact me: dbareader@yepher.com.
 * Use this program and code contained here in at your own risk.
 * It may not perform as intended and may cause corruption of data.
 * If you do find bugs or make improvements please send me a copy ;)
 *
 * This program has only been tested on Linux/Intel systems using datebook
 * files produced from Windows/Intel. I expect there should be no problems
 * running the app on a Windos machine though.
 *
 * TODO:
 *     # Create abstract access interface
 *     # Create access interface
 *     # Fix StreamUtil read int/short
 *
**/
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;

/**
 * This class is immutable so I expect it is thread safe but I have not tested
 * to make sure.
 */
public final class DbaReader
{
    private boolean m_verbose = false;
    private final String m_dbName;
    private DataInputStream m_data;
    private InputStream m_inputStream;
    private Data m_db;

    public DbaReader(String dbFile)
    {
        if (dbFile == null)
            throw new NullPointerException("DB File was null");

        m_dbName = dbFile;
    }

    public void setVerbose(boolean val)
    {
        m_verbose = val;
    }

    public void decode()
        throws IOException
    {
        // Only parse DB once
        if (m_db != null)
            return;

        if (m_data == null || m_inputStream == null)
        {
            m_inputStream = new FileInputStream(m_dbName);
            m_data = new DataInputStream(m_inputStream);
        }

        m_db = new Data(m_data, m_verbose);

        // Clean up we don't need the file anymore we only parse it once
        m_data.close();
        m_data = null;
        m_inputStream = null;
    }

    public void finalize()
    {
        try
        {
            if (m_data != null)
            {
                m_data.close();
                m_data = null;
            }
        }
        catch(Exception e)
        {
        }
    }

    public static final void main(String[] args)
        throws IOException
    {
        if (args.length != 1)
        {
            System.out.println("Usage: java dbaReader [.dba file]");
            System.exit(-1);
        }

        DbaReader dba = new DbaReader(args[0]);
        dba.setVerbose(true);
        dba.decode();
    }
}

// ---------------------------------------------------------------------------
// Data class that contains all datebook entries
// ---------------------------------------------------------------------------
final class Data
{
    private boolean m_verbose = false;
    private static final int DATEBOOKVERSION = 0x44420100;
    private final int m_version;
    private final String m_origName;
    private final String m_showHeader;
    private final int m_nextFreeCat;
    private final int m_CategoryCount;
    private final int m_schemaResourceID;
    private final int m_fieldsPerRow;
    private final int m_recordIdPos;
    private final int m_recordStatusPos;
    private final int m_recordPlacementPos;
    private final int m_schemaFieldCount;
    private final short[] m_fieldEntrys;
    private final int m_numEntries;
    private final DateBookEntry[] m_entries;

    public Data(DataInputStream is, boolean verbose)
        throws IOException
    {
        m_verbose = verbose;
        print("** Data **");

        if (is == null)
            throw new NullPointerException("Input stream was null");

        m_version = StreamUtil.readInt(is);
        print("Version: " + m_version);
        if (m_version != DATEBOOKVERSION)
        {
            throw new IllegalArgumentException("Illegal file format: " + m_version);
        }

        m_origName = StreamUtil.readCString(is);
        print("Orig File: " + m_origName);

        m_showHeader = StreamUtil.readCString(is);
        print("Show Header: " + m_showHeader);

        m_nextFreeCat = StreamUtil.readInt(is);
        print("Next Free Cat: " + m_nextFreeCat);

        m_CategoryCount = is.readInt();
        print("Category Count: " + m_CategoryCount);

        // If there were Category Entries we would decode them here.
        if (m_CategoryCount != 0x00)
            throw new IllegalArgumentException("Decoder does not support Category Entries");

        m_schemaResourceID = StreamUtil.readInt(is);
        print("Schema ResId: " + m_schemaResourceID);

        m_fieldsPerRow = StreamUtil.readInt(is);
        print("Fields per Row (15): " + m_fieldsPerRow);

        if (m_fieldsPerRow != 0x0f)
            throw new IllegalArgumentException("Illegal Field Count");

        m_recordIdPos = StreamUtil.readInt(is);
        print("Record Pos: " + m_recordIdPos);

        m_recordStatusPos = StreamUtil.readInt(is);
        print("Record Status Pos: " + m_recordStatusPos);

        m_recordPlacementPos = StreamUtil.readInt(is);
        print("Record Placement Pos: " + m_recordPlacementPos);

        m_schemaFieldCount = StreamUtil.readShort(is);
        print("Schema Field Count: " + m_schemaFieldCount);

        m_fieldEntrys = new short[m_schemaFieldCount];
        for (int i = 0; i < m_schemaFieldCount; i++)
        {
            m_fieldEntrys[i] = StreamUtil.readShort(is);
            print("\tEntry " + i + " is " + m_fieldEntrys[i]);
        }

        m_numEntries = StreamUtil.readInt(is);
        print("Num Entries: " + m_numEntries + " \n\t actual: " + m_numEntries/15);
        m_entries = new DateBookEntry[m_numEntries/15];

        print("Header read successfully");

        parseEntries(is);
    }

    private void parseEntries(DataInputStream is)
        throws IOException
    {
        long start = System.currentTimeMillis();
        for(int i = 0; i < m_entries.length; i++)
        {
            print("Entry: " + i + " of " + m_entries.length);
            m_entries[i] = new DateBookEntry(is, m_verbose);

            // StreamUtil.dumpBytes(is, 20);
        }
        long end = System.currentTimeMillis();

        System.err.println("Parsed " + m_entries.length + " in " + (end-start)
                + " mSec");
    }

    private void print(String str)
    {
        if (!m_verbose) return;

        System.out.println(str);
    }
}


// ---------------------------------------------------------------------------
// Class that contains an entry from the datebook
// ---------------------------------------------------------------------------
final class DateBookEntry
{
    private boolean m_verbose;

    private final int m_fieldType1, m_recordId, m_fieldType2, m_statusField,
            m_fieldType3, m_pos, m_fieldType4;
    private final long m_startTime;
    private final int m_fieldType5;
    private final long m_endTime;
    private final int m_fieldType6, m_padding1;

    private final String m_description;

    private final int m_fiedlType7, m_duration, m_fieldType8, m_padding2;

    private final String m_note;

    private final int m_fieldType9, m_unTimed, m_fieldType10, m_private,
            m_fieldType11, m_category, m_fieldType12, m_alarmSet, m_fieldType13,
            m_alarmAdvUnits, m_fieldType14, m_alarmAdvType, m_fieldType15;

    private final RepeatEvent m_repeatEvent;

    public DateBookEntry(DataInputStream is, boolean verbose)
        throws IOException
    {
        m_verbose = verbose;
        print("** datebook entry **");

        Date date;

        m_fieldType1 = StreamUtil.readInt(is);
        print("\tfieldType1: " + m_fieldType1);

        m_recordId = StreamUtil.readInt(is);
        print("\trecordId: " + m_recordId);

        m_fieldType2 = StreamUtil.readInt(is);
        print("\tfieldType2: " + m_fieldType2);

        m_statusField = StreamUtil.readInt(is);
        print("\tstatusField: " + m_statusField);
        decodeStatus(m_statusField);


        m_fieldType3 = StreamUtil.readInt(is);
        print("\tfieldType3: " + m_fieldType3);

        m_pos = StreamUtil.readInt(is);
        print("\tpos: " + m_pos);

        m_fieldType4 = StreamUtil.readInt(is);
        print("\tfieldType4: " + m_fieldType4);

        // record is stored in sec we need milli seconds
        // Don't remove the cast. It will cause calcuation errors
        m_startTime = (long)StreamUtil.readInt(is) * 1000;
        date = new Date(m_startTime);
        print("\tstartTime: " + date.toString() + " (" + m_startTime + ")");

        m_fieldType5 = StreamUtil.readInt(is);
        print("\tfieldType5: " + m_fieldType5);

        // record is stored in sec we need milli seconds
        // Don't remove the cast. It will cause calcuation errors
        m_endTime = (long)StreamUtil.readInt(is) * 1000;
        date = new Date(m_endTime);
        print("\tendTime: " + date.toString());

        m_fieldType6 = StreamUtil.readInt(is);
        print("\tfieldType6: " + m_fieldType6);

        m_padding1 = StreamUtil.readInt(is);
        print("\tpadding1: " + m_padding1);

        m_description = StreamUtil.readCString(is);
        print("\tdescription: \"" + m_description + "\"");

        m_fiedlType7 = StreamUtil.readInt(is);
        print("\tfiedlType7: " + m_fiedlType7);

        m_duration = StreamUtil.readInt(is);
        print("\tduration: " + m_duration);

        m_fieldType8 = StreamUtil.readInt(is);
        print("\tfieldType8: " + m_fieldType8);

        m_padding2 = StreamUtil.readInt(is);
        print("\tpadding2: " + m_padding2);

        m_note = StreamUtil.readCString(is);
        print("\tnote: \"" + m_note + "\"");

        m_fieldType9 = StreamUtil.readInt(is);
        print("\tfieldType9: " + m_fieldType9);

        m_unTimed = StreamUtil.readInt(is);
        print("\tunTimed: " + m_unTimed);

        m_fieldType10 = StreamUtil.readInt(is);
        print("\tfieldType10: " + m_fieldType10);

        m_private = StreamUtil.readInt(is);
        print("\tprivate: " + m_private);

        m_fieldType11 = StreamUtil.readInt(is);
        print("\tfieldType11: " + m_fieldType11);

        m_category = StreamUtil.readInt(is);
        print("\tcategory: " + m_category);

        m_fieldType12 = StreamUtil.readInt(is);
        print("\tfieldType12: " + m_fieldType12);

        m_alarmSet = StreamUtil.readInt(is);
        print("\talarmSet: " + m_alarmSet);

        m_fieldType13 = StreamUtil.readInt(is);
        print("\tfieldType13: " + m_fieldType13);

        m_alarmAdvUnits = StreamUtil.readInt(is);
        print("\talarmAdvUnits: " + m_alarmAdvUnits);

        m_fieldType14 = StreamUtil.readInt(is);
        print("\tfieldType14: " + m_fieldType14);
        switch (m_fieldType14)
        {
            case 0: print("\t\tMinutes"); break;
            case 1: print("\t\tHours");   break;
            case 2: print("\t\tDays");    break;
        }


        m_alarmAdvType = StreamUtil.readInt(is);
        print("\talarmAdvType: " + m_alarmAdvType);

        m_fieldType15 = StreamUtil.readInt(is);
        print("\tfieldType15: " + m_fieldType15);

        m_repeatEvent = new RepeatEvent(is, m_verbose);

        print("Record parsed properly");
    }

    private void decodeStatus(int status)
    {
        if ((status & 0x08) != 0)
            print("\t\tPending (0x08)");

        if ((status & 0x01) != 0)
            print("\t\tAdd (0x01)");

        if ((status & 0x02) != 0)
            print("\t\tUpdate (0x02)");

        if ((status & 0x04) != 0)
            print("\t\tDelete (0x04)");

        if ((status & 0x80) != 0)
            print("\t\tArchive (0x80)");
    }

    private void print(String str)
    {
        if (!m_verbose) return;

        System.out.println(str);
    }
}

// ---------------------------------------------------------------------------
// RepeatEvent
// ---------------------------------------------------------------------------
final class RepeatEvent
{
    public static final int DAILY = 1;
    public static final int WEEKLY = 2;
    public static final int MONTHLYBYDAY = 3;
    public static final int MONTHLYBYDATE = 4;
    public static final int YEARLYBYDATE = 5;
    public static final int YEARLYBYDAY = 6; // Never used

    private boolean m_verbose;

    private short m_dateException;
    private int m_exceptionEntry;
    private short m_repeatEventFlag;
    private final ClassEntry m_classEntry;
    private int m_brand;
    private int m_interval;
    private long m_endDate;
    private int m_firstDayWk;
    private BrandData m_brandData;

    RepeatEvent(DataInputStream is, boolean verbose)
        throws IOException
    {
        m_verbose = verbose;
        print("\n** repeat event **");

        m_dateException = StreamUtil.readShort(is);
        print("dateException: " +  m_dateException);

        if (m_dateException != 0)
        {
            print("\n** Found date exceptions, index is " + m_dateException);
            for (int i = 0; i < m_dateException; i++)
            {
                /**
                 * This was unclear in the decode document.
                 * Followed perl scripts implementation here.
                **/
                m_exceptionEntry = StreamUtil.readInt(is);
                print("\texceptionEntry: " + m_exceptionEntry);
            }
        }
        else m_exceptionEntry = 0;

        m_repeatEventFlag = StreamUtil.readShort(is);
        print("\trepeatEventFlag: " + m_repeatEventFlag);

        // The rest of these members could (should?) be pushed down into repeat
        // event decoder. They are here because this is where they exist in
        // the decode document.
        switch(m_repeatEventFlag)
        {
            case 0x0000:
            {
                m_classEntry = null;
                return;
            }
            case ((short)0xFFFF):
            {
                print("** We have a class entry **");
                m_classEntry = new ClassEntry(is, m_verbose);
                break;
            }
            default:
            {
                /**
                 * This this seems to be the right way to do this. It was
                 * not clear in the decode document
                **/
                //m_classEntry = new ClassEntry(is, m_verbose);
                m_classEntry = null;
            }
        }

        m_brand = StreamUtil.readInt(is);
        switch (m_brand)
        {
            case 1: print("\tDaily (1)"); break;
            case 2: print("\tWeekly (2)"); break;
            case 3: print("\tMonthlyByDay (3)"); break;
            case 4: print("\tMonthlyByDate (4)"); break;
            case 5: print("\tYearlyByDate(5)"); break;
            case 6: print("\tYearlyByDay (6)"); break;
            default: print("\tUnknown Repeat type (" + (m_brand&0xff) + ")" ); break;
        }


        m_interval = StreamUtil.readInt(is);
        print("Interval: " + m_interval);

        // Convert Seconds to milli seconds
        m_endDate = (long)StreamUtil.readInt(is) * 1000;
        Date date = new Date(m_endDate);
        print("Repeat EndDate: " + date.toString() + "(" + m_endDate + ")");

        m_firstDayWk = (StreamUtil.readInt(is)&0xff);

        if (m_firstDayWk < 0 || m_firstDayWk > 6)
            throw new IllegalStateException("Illegal day of week epected 1-6 got " + m_firstDayWk);

        print("first dow: " + m_firstDayWk);

        m_brandData = new BrandData(is, m_brand, m_verbose);

    }

    private void print(String str)
    {
        if (!m_verbose) return;

        System.out.println("\t\t\t" + str);
    }
}

// ---------------------------------------------------------------------------
// BrandData
// ---------------------------------------------------------------------------
final class BrandData
{
    private boolean m_verbose;

    private int m_dayIndex;
    private byte m_dayMask;  // Bug fix: DAVID
    private int m_weekIndex;
    private int m_dayNumber;
    private int m_monthIndex;

    public BrandData(DataInputStream is, int brand, boolean verbose)
        throws IOException
    {
        m_verbose = verbose;
        brand = (brand & 0xff);
        print("** brand data " + brand + " **");

        if (brand == RepeatEvent.DAILY || brand == RepeatEvent.WEEKLY ||
            brand == RepeatEvent.MONTHLYBYDAY)
        {
            m_dayIndex = StreamUtil.readInt(is);
            print("dayIndex: " + m_dayIndex);
        }

        if (brand == RepeatEvent.WEEKLY)
        {
            m_dayMask = (byte) is.readUnsignedByte();  // Bug fix: DAVID
            print("dayMask: " + m_dayMask);
        }

        if (brand == RepeatEvent.MONTHLYBYDAY)
        {
            m_weekIndex = StreamUtil.readInt(is);
            print("weekIndex: " + m_weekIndex);
        }

        if (brand == RepeatEvent.MONTHLYBYDATE ||
            brand == RepeatEvent.YEARLYBYDATE)
        {
            m_dayNumber = StreamUtil.readInt(is);
            print("dayNumber: " + m_dayNumber);
        }

        if (brand == RepeatEvent.YEARLYBYDATE)
        {
            m_monthIndex = StreamUtil.readInt(is);
            print("monthIndex: " + m_monthIndex);
        }
    }

    private void print(String str)
    {
        if (!m_verbose) return;

        System.out.println(str);
    }
}

// ---------------------------------------------------------------------------
// ClassEntry
// ---------------------------------------------------------------------------
final class ClassEntry
{
    private boolean m_verbose;
    private short m_const;
    private byte m_classByte;
    private String m_className;

    public ClassEntry(DataInputStream is, boolean verbose)
        throws IOException
    {
        m_verbose = verbose;
        print("class entry");

        m_const = StreamUtil.readShort(is);
        print("const: " + m_const);

        int len = StreamUtil.readShort(is);
        m_classByte = is.readByte();

        // I am not sure why they just did not reuse CString format !?!
        StringBuffer str = new StringBuffer(len);
        for (int i = 1; i < len; i++)
            str.append(((char)is.readByte()));


        m_className = str.toString();
        print("className: " + m_className);
    }

    private void print(String str)
    {
        if (!m_verbose) return;

        System.out.println(str);
    }
}

// ---------------------------------------------------------------------------
// CategoryEntry
// ---------------------------------------------------------------------------
/**
 * TODO: Implement Category Entry if necessary
 *
 * I am not sure what software surrports this class. Palm does not seem to use
 * it!
 *
 * Category Entry  Format (Unused)
 * Index            Long  4*Byte    Category Index
 * ID               Long  4*Byte    Category ID
 * Dirty Flag       Long  4*Byte    Category Dirty Flag
 * Long Name        Cstring    Long Category Name
 * Short Name       Cstring    Short Category Name
 */
final class CategoryEntry
{
     private boolean m_verboses;
}

// ---------------------------------------------------------------------------
// Helper utility for reading Streams
// ---------------------------------------------------------------------------
final class StreamUtil
{
    // TODO: Figure out a better way to do this
    // This is lame way to do this but oh well; DataInputStrea.readInt() has
    // wrong byte order. ByteBuffer would be my choice but can't count on
    // Java 1.4 on all web servers out there :(
    public static int readInt(DataInputStream is)
        throws IOException
    {
        byte[] buf = new byte[4];
        buf[0] = is.readByte();
        buf[1] = is.readByte();
        buf[2] = is.readByte();
        buf[3] = is.readByte();

        int res = 0;
        int i, n;
        for (i = 0; i != 4; i++)
        {
            n = buf[i] & 0xFF;
            n <<= i * 8;
            res |=  n;
        }
        return res;
    }

    // TODO: Figure out a better way to do this
    // This is lame way to do this but oh well; DataInputStrea.readInt() has
    // wrong byte order. ByteBuffer would be my choice but can't count on
    // Java 1.4 on all web servers out there :(
    public static short readShort(DataInputStream is)
        throws IOException
    {
        byte[] buf = new byte[2];
        buf[0] = is.readByte();
        buf[1] = is.readByte();

        short res = 0;
        int i, n;
        for (i = 0; i != 2; i++)
        {
            n = buf[i] & 0xFF;
            n <<= i * 8;
            res |=  n;
        }
        return res;
    }

    /**
     * Cstrings are stored as folnnlows:
     * 1. Strings less than 255 bytes are stored with the length specified in
     * the first byte followed by the actual string.
     *
     * 2. Zero length strings are stored with a 0x00 byte.
     *
     * 3. Strings 255 bytes or longer are stored with a flag byte set to 0xFF
     * followed by a short (2*Byte) that specifies the length of the string,
     * followed by the actual string.
    **/
    public static String readCString(DataInputStream is)
        throws IOException
    {
        StringBuffer scratchfile = new StringBuffer();
	// Bug fix: DAVID. Was previously readByte()
        int len = is.readUnsignedByte();

	// Bug fix: DAVID. Was previously == (byte)0xFF
        if (len == 255) len = readShort(is);

        for (int i = 0; i != len; i++)
            scratchfile.append((char)is.readByte());

        return scratchfile.toString();
    }

    /**
     * This will dump the hex values of the next N bytes in stream.
     *
     * Warning: if you call this the decoder will not be able to parse
     * anymore of the file.
    **/
    public static void dumpBytes(DataInputStream is, int count)
        throws IOException
    {
        System.err.println(
            "\nStart stream dump: "
            + "\n----------------------------------------------");
        Integer val;
        for (int i = 0; i < count; i++)
        {
            if (i%16 == 0)
                System.err.print("\n");

            val = new Integer(is.readByte() & 0xFF );
            System.err.print(
                    ((val.intValue() < 0x10) ? "0x0" : "0x")
                    + Integer.toHexString(val.intValue()).toUpperCase() + " ");
        }
        System.err.println(
                "\n\nEnd of Stream Dump"
                + "\n----------------------------------------------"
                + "\n\n");

        throw new IllegalStateException(count + " bytes dumped to standard err");
    }
}
