%Library for decoding Palm(R) DateBook(TM) database files.
%Copyright (C) 2006  Romain Lenglet <rlenglet@users.forge.objectweb.org>
%
%This program is free software; you can redistribute it and/or
%modify it under the terms of the GNU General Public License
%as published by the Free Software Foundation; either version 2
%of the License, or (at your option) any later version.
%
%This program is distributed in the hope that it will be useful,
%but WITHOUT ANY WARRANTY; without even the implied warranty of
%MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%GNU General Public License for more details.
%
%You should have received a copy of the GNU General Public License
%along with this program; if not, write to the Free Software
%Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

-module(palm_db).
-version(0.1). % 2006-09-06

-export([read_datebook_db/1]).

-include("palm_db.hrl").

% Exported functions:

read_datebook_db(Filename) ->
    % Generic Palm DB decoding:
    {ok, IODev} = file:open(Filename, [read, raw, binary]),
    {ok, FileSize} = file:position(IODev, eof),
    {ok, 0} = file:position(IODev, bof),
    DatabaseHdr = read_database_header(IODev),
    RecordEntries = read_all_pdb_record_entries(IODev),
    % The info blocks are ignored here (not useful for DateBook),
    % but they should be parsed and skipped properly. In order to detect their
    % start, we need to know the offset of the first record entry after
    % those blocks.
    [FirstRecordEntry|_] = RecordEntries,
    FirstRecordID = FirstRecordEntry#pdb_record_entry.local_chunk_index,
    read_app_info_bin(IODev, DatabaseHdr#database_hdr.app_info_index,
		      DatabaseHdr#database_hdr.sort_info_index,
		      FirstRecordID, FileSize),
    read_sort_info_bin(IODev, DatabaseHdr#database_hdr.sort_info_index,
		       FirstRecordID, FileSize),
    % Check that name in the header is that of the Datebook application:
    "DatebookDB" = DatabaseHdr#database_hdr.name,
    % Decoding of DateBook-specific DB records:
    Records = read_all_datebook_records(IODev, RecordEntries),
    ok = file:close(IODev),
    {DatabaseHdr, Records}.

% General PDB database decoding functions:

read_database_header(IODev) ->
    {ok, Binary} = file:read(IODev, 72),
    <<Name:31/binary, 0, Attributes:16, Version:16, CreationDate:32,
      ModificationDate:32, LastBackupDate:32, ModificationNumber:32,
      AppInfoID:32, SortInfoID:32, Type:32, Creator:32,
      UniqueIDSeed:32>> = Binary,
    #database_hdr{name = name_to_string(Name, []), attributes = Attributes,
	version = Version, creation_date = CreationDate,
        modification_date = ModificationDate,
	last_backup_date = LastBackupDate,
	modification_number = ModificationNumber, app_info_index = AppInfoID,
	sort_info_index = SortInfoID, type = Type, creator = Creator,
	unique_index_seed = UniqueIDSeed}.

name_to_string(Binary, Name) when size(Binary) == 0 ->
    lists:reverse(Name);
name_to_string(Binary, Name) ->
    <<Char:8, Tail/binary>> = Binary,
    if
	Char == 0 -> lists:reverse(Name);
	true -> name_to_string(Tail, [Char | Name])
    end.

read_record_list_header(IODev) ->
    {ok, Binary} = file:read(IODev, 6),
    <<NextRecordListID:32, NumRecords:16>> = Binary,
    #record_list{next_record_list_index = NextRecordListID,
		 num_records = NumRecords}.

read_pdb_record_entry(IODev) ->
    {ok, Binary} = file:read(IODev, 8),
    <<LocalChunkID:32, Attributes:8, UniqueID:24>> = Binary,
    #pdb_record_entry{local_chunk_index = LocalChunkID, attributes = Attributes,
		      unique_index = UniqueID}.

read_all_pdb_record_entries(IODev) ->
    RecordListHdr = read_record_list_header(IODev),
    RecordEntryList = read_pdb_record_list_next(IODev,
        RecordListHdr#record_list.num_records),
    if
	RecordListHdr#record_list.next_record_list_index > 0 ->
	    RecordEntryList ++ read_all_pdb_record_entries(IODev);
	true ->
	    RecordEntryList
    end.

read_pdb_record_list_next(IODev, 0) ->
    {ok, _} = file:read(IODev, 2), % Placeholder bytes.
    [];
read_pdb_record_list_next(IODev, Count) when Count > 0 ->
    [read_pdb_record_entry(IODev)
     | read_pdb_record_list_next(IODev, Count - 1)].

read_app_info_bin(IODev, AppInfoID, SortInfoID, FirstRecordID, FileSize) ->
    if
	AppInfoID > 0 -> % There is an app info block.
	    % Check that the current offset is correct:
	    {ok, AppInfoID} = file:position(IODev, cur),
	    {ok, Bin} =
	    if
		SortInfoID > 0 -> % There is a sort info block.
		    file:read(IODev, SortInfoID - AppInfoID);
		FirstRecordID > 0 -> % There is no soft info block, but records.
		    file:read(IODev, FirstRecordID - AppInfoID);
		true -> % There is no soft info block, and no records.
		    file:read(IODev, FileSize - AppInfoID)
	    end,
	    Bin;
	true -> % There is no app info block.
	    []
    end.

read_sort_info_bin(IODev, SortInfoID, FirstRecordID, FileSize) ->
    if
	SortInfoID > 0 -> % There is a sort info block.
	    % Check that the current offset is correct:
	    {ok, SortInfoID} = file:position(IODev, cur),
	    {ok, Bin} =
	    if
		FirstRecordID > 0 -> % There are records.
		    file:read(IODev, FirstRecordID - SortInfoID);
		true -> % There is records.
		    file:read(IODev, FileSize - SortInfoID)
	    end,
	    Bin;
	true -> % There is no sort info block.
	    []
    end.

% DateBook-specific decoding functions:

read_datebook_record(IODev, RecordEntry) ->
    RecordID = RecordEntry#pdb_record_entry.local_chunk_index,
    % Check that the current offset is correct:
    {ok, RecordID} = file:position(IODev, cur),
    {ok, <<StartHour, StartMinute, EndHour, EndMinute, StartYear:7,
	  StartMonth:4, StartDay:5, FlagChanged:1, FlagAlarm:1, FlagRepeat:1,
	  FlagNote:1, FlagExceptions:1, FlagDescription:1, _:10>>}
	= file:read(IODev, 8),
    StartTime = #datebook_time{hour = StartHour, minute = StartMinute},
    EndTime = #datebook_time{hour = EndHour, minute = EndMinute},
    StartDate = #datebook_date{year = StartYear+1904, month = StartMonth,
			       day = StartDay},
    HasChanged =
	case FlagChanged of
	    1 -> true;
	    0 -> false
	end,
    Alarm =
	if
	    FlagAlarm == 1 ->
		{ok, <<Advance/signed, AdvanceUnit>>} = file:read(IODev, 2),
		#datebook_alarm_advance{
		    advance =
			case Advance of
			    -1 -> noalarm;
			    _Else -> Advance
			end,
		    unit =
			case AdvanceUnit of
			    0 -> minutes;
			    1 -> hours;
			    2 -> days
			end};
	    true ->
		noalarm
	end,
    Repeat =
	if
	    FlagRepeat == 1 ->
		{ok, <<RepeatType, _, RepeatEndYear:7, RepeatEndMonth:4,
		      RepeatEndDay:5, RepeatFrequency, RepeatOn,
		      RepeatStartOfWeek,
		      _>>} = file:read(IODev, 8),
		#datebook_repeat{
		    type =
			case RepeatType of
			    0 -> norepeat;
			    1 -> dayly;
			    2 -> weekly;
			    3 -> monthly_by_day;
			    4 -> monthly_by_date;
			    5 -> yearly
			end,
		    end_date = #datebook_date{year = RepeatEndYear+1904,
			month = RepeatEndMonth, day = RepeatEndDay},
		    frequency = RepeatFrequency,
		    repeat_on_bitfield = RepeatOn,
		    start_of_week = RepeatStartOfWeek};
	    true ->
		norepeat
	end,
    ExceptionDates =
	if
	    FlagExceptions == 1 ->
		{ok, <<ExceptionsCount:16>>} = file:read(IODev, 2),
		read_datebook_repeat_exception_dates(IODev, ExceptionsCount);
	    true ->
		[]
	end,
    Description =
	if
	    FlagDescription == 1 ->
		read_zero_ended_string(IODev);
	    true ->
		[]
	end,
    Note =
	if
	    FlagNote == 1 ->
		read_zero_ended_string(IODev);
	    true ->
		[]
	end,
    #datebook_record{
        start_time = StartTime,
        end_time = EndTime,
        start_date = StartDate,
	has_changed = HasChanged,
	alarm = Alarm,
	repeat = Repeat,
	exception_dates = ExceptionDates,
        description = Description,
	note = Note}.

read_datebook_repeat_exception_dates(_IODev, 0) ->
    [];
read_datebook_repeat_exception_dates(IODev, ExceptionsCount) ->
    {ok, <<Year:7, Month:4, Day:5>>} = file:read(IODev, 2),
    [#datebook_date{year = Year+1904, month = Month, day = Day}
     |read_datebook_repeat_exception_dates(IODev, ExceptionsCount-1)].

read_zero_ended_string(IODev) ->
    {ok, <<Char>>} = file:read(IODev, 1),
    case Char of
	0 ->
	    [];
	_Else ->
	    [Char|read_zero_ended_string(IODev)]
    end.

read_all_datebook_records(_IODev, []) ->
    [];
read_all_datebook_records(IODev, [RecordEntry|Tail]) ->
    Record = read_datebook_record(IODev, RecordEntry),
    [Record | read_all_datebook_records(IODev, Tail)].
