-module(ts_json).
-export([encode_record/2, record_to_ejson/2,
         ejson_to_record/2, ejson_to_record/3,
         ejson_to_record_strict/2, ejson_to_record_strict/3,
         decode_datetime/1]).

-include("ts_db_records.hrl").

encode_record(Record, ExtData) -> lists:flatten(json:encode(record_to_ejson(Record, ExtData))).

% User JSON record required structure:
% {"id":        "john_doe",
%  "name":      "John Doe",
%  "email":     "john.doe@example.com",
%  "join_date": "2011-01-01T12:00.000Z"}

record_to_ejson(Record=#ts_user{},  ExtData) ->
    {struct, [
        {id, Record#ts_user.username},
        {name, Record#ts_user.name},
        {email, Record#ts_user.email},
        {join_date, encode_datetime(Record#ts_user.join_date)}] ++
        lists:map(fun ext_data_to_ejson/1, ExtData)};

% Timeline JSON record stucture:
% {"user_id":   "john_doe",
%  "id":        "personal",
%  "created":   "2011-01-01T14:00.000Z",
%  "description:"Personal time-tracking."}

record_to_ejson(Record=#ts_timeline{}, ExtData) ->
    % pull out the username and timeline id
    {Username, TimelineId} = Record#ts_timeline.ref,

    % create the EJSON struct
    {struct, [
        {user_id, Username},
        {id, TimelineId},
        {created, encode_datetime(Record#ts_timeline.created)},
        {description, Record#ts_timeline.desc}] ++
        lists:map(fun ext_data_to_ejson/1, ExtData)};

% Entry JSON record structure:
% {"user_id":       "john_doe",
%  "timeline_id":   "personal",
%  "id":            "1",
%   "timestamp":    "2011-01-01T14:01.000Z",
%   "mark":         "Workout.",
%   "uuid":         "98ed5198-0c98-47c7-96c9-52b57a2c605a",
%   "notes":        "First workout after a long break."}

record_to_ejson(Record=#ts_entry{}, ExtData) ->
    % pull out the username, timeline id, and entry id
    {Username, TimelineId, EntryId} = Record#ts_entry.ref,

    % convert the timestamp to a date-time
    DateTime = calendar:gregorian_seconds_to_datetime(Record#ts_entry.timestamp),

    % create the EJSON struct
    {struct, [
        {user_id, Username},
        {timeline_id, TimelineId},
        {id, EntryId},
        {timestamp, encode_datetime(DateTime)},
        {mark, Record#ts_entry.mark},
        {uuid, uuid:to_string(Record#ts_entry.uuid)},
        {notes, Record#ts_entry.notes}] ++
        lists:map(fun ext_data_to_ejson/1, ExtData)}.

encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
    lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0BZ", 
        [Year, Month, Day, Hour, Minute, Second, 000])).

ext_data_to_ejson({Key, Value}) ->
    case Key of
        entry_exclusions -> {Key, {array, Value}};
        _Other -> {Key, Value}
    end.

ejson_to_record(Empty, {struct, EJSONFields}) ->
    construct_record(Empty, EJSONFields, []).

ejson_to_record(Empty, Ref, EJSON) ->
    {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
    {setelement(2, Constructed, Ref), ExtData}.

ejson_to_record_strict(Empty=#ts_user{}, EJSON) ->
    {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
    case Constructed of
        #ts_user{name = undefined} -> throw("Missing user 'name' field.");
        #ts_user{email = undefined} -> throw("Missing user 'email' field.");
        #ts_user{join_date = undefined} ->
            throw("Missing user 'join_date' field.");
        _Other -> {Constructed, ExtData}
    end;

ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) ->
    {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
    case Constructed of
        #ts_timeline{created = undefined} ->
            throw("Missing timeline 'created' field.");
        #ts_timeline{desc = undefined} ->
            throw("Missing timeline 'description' field.");
        _Other -> {Constructed, ExtData}
    end;

ejson_to_record_strict(Empty=#ts_entry{}, EJSON) ->
    {Constructed, ExtData} = ejson_to_record(Empty, EJSON),
    case Constructed of
        #ts_entry{timestamp = undefined} ->
            throw("Missing timelne 'timestamp' field.");
        #ts_entry{mark = undefined} ->
            throw("Missing timeline 'mark' field.");
        #ts_entry{notes = undefined} ->
            throw("Missing timeline 'notes' field/");
        _Other -> {Constructed, ExtData}
    end.

ejson_to_record_strict(Empty, Ref, EJSON) ->
    {Constructed, ExtData} = ejson_to_record_strict(Empty, EJSON),
    {setelement(2, Constructed, Ref), ExtData}.

construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) ->
    case Key of
        id -> construct_record(User#ts_user{username = Value},
            Fields, ExtData);
        name -> construct_record(User#ts_user{name = Value}, Fields, ExtData);
        email -> construct_record(User#ts_user{email = Value}, Fields, ExtData);
        join_date -> construct_record(
            User#ts_user{join_date = decode_datetime(Value)},
            Fields, ExtData);
        _Other ->
            ExtDataProp = ejson_to_ext_data({Key, Value}),
            construct_record(User, Fields, [ExtDataProp|ExtData])
    end;
        
construct_record(Timeline=#ts_timeline{}, [{Key, Value}|Fields], ExtData) ->
    case Key of
        user_id ->
            {_, TimelineId} = Timeline#ts_timeline.ref,
            construct_record(Timeline#ts_timeline{ref = {Value, TimelineId}},
                Fields, ExtData);
        id ->
            {Username, _} = Timeline#ts_timeline.ref,
            construct_record(Timeline#ts_timeline{ref = {Username, Value}},
                Fields, ExtData);
        created -> construct_record(
                Timeline#ts_timeline{created = decode_datetime(Value)},
                Fields, ExtData);
        description -> construct_record(Timeline#ts_timeline{desc = Value},
                Fields, ExtData);
        _Other -> 
            ExtDataProp = ejson_to_ext_data({Key, Value}),
            construct_record(Timeline, Fields, [ExtDataProp|ExtData])
    end;

construct_record(Entry=#ts_entry{}, [{Key, Value}|Fields], ExtData) ->
    case Key of
        user_id ->
            {_, TimelineId, EntryId} = Entry#ts_entry.ref,
            construct_record(Entry#ts_entry{ref = {Value, TimelineId, EntryId}},
                Fields, ExtData);
        timeline_id ->
            {Username, _, EntryId} = Entry#ts_entry.ref,
            construct_record(Entry#ts_entry{ref = {Username, Value, EntryId}},
                Fields, ExtData);
        id ->
            {Username, TimelineId, _} = Entry#ts_entry.ref,
            construct_record(Entry#ts_entry{ref = {Username, TimelineId, Value}},
                Fields, ExtData);
        timestamp -> construct_record(
            Entry#ts_entry{timestamp = calendar:datetime_to_gregorian_seconds(
                decode_datetime(Value))},
            Fields, ExtData);
        mark -> construct_record(Entry#ts_entry{mark=Value}, Fields, ExtData);
        uuid -> construct_record(Entry#ts_entry{uuid=uuid:to_binary(Value)}, Fields, ExtData);
        notes -> construct_record(Entry#ts_entry{notes=Value}, Fields, ExtData);
        _Other ->
            ExtDataProp = ejson_to_ext_data({Key, Value}),
            construct_record(Entry, Fields, [ExtDataProp|ExtData])
    end;

construct_record(Record, [], ExtData) -> {Record, ExtData}.

decode_datetime(DateTimeString) ->
    % TODO: catch badmatch and badarg on whole function

    [DateString, TimeString] = re:split(DateTimeString, "[TZ]",
        [{return, list}, trim]),

    [YearString, MonthString, DayString] =
        re:split(DateString, "-", [{return, list}]),

    [HourString, MinuteString, SecondString] = 
        case re:split(TimeString, "[:\\.]", [{return, list}]) of
            [HS, MS, SS, _MSS] -> [HS, MS, SS];
            [HS, MS, SS] -> [HS, MS, SS]
        end,

    Date = {list_to_integer(YearString), list_to_integer(MonthString),
        list_to_integer(DayString)},

    Time = {list_to_integer(HourString), list_to_integer(MinuteString),
        list_to_integer(SecondString)},

    {Date, Time}.

ejson_to_ext_data({Key, Value}) ->
    case Key of
        entry_exclusions -> 
            {array, ExclList} = Value,
            {Key, ExclList};
        _Other -> {Key, Value}
    end.


