public enum AttachmentType {
    IMAGE,
    LINK,
    TEXT,
    AUDIO,
    LIST
} public enum TaskCategory {
    BUSINESS(R.string.task_category_business, R.drawable.icon_category_business),
    PERSONAL(R.string.task_category_personal, R.drawable.icon_category_personal),
    HEALTH(R.string.task_category_health, R.drawable.icon_category_health),
    REPAIRS(R.string.task_category_repairs, R.drawable.icon_category_repairs),
    SHOPPING(R.string.task_category_shopping, R.drawable.icon_category_shopping);

    private @StringRes int friendlyNameRes;
    private @DrawableRes int iconRes;

    TaskCategory(@StringRes int friendlyNameRes, @DrawableRes int iconRes) {
        this.friendlyNameRes = friendlyNameRes;
        this.iconRes = iconRes;

    }

    public int getFriendlyNameRes() {
        return friendlyNameRes;
    }
    public int getIconRes() {
        return iconRes;
    }

    public static List<String> getFriendlyValues(Context context) {
        List<String> friendlyValues = new ArrayList<>();
        for (TaskCategory tc : values()) {
            friendlyValues.add(context.getResources().getString(tc.friendlyNameRes));
        }
        return friendlyValues;
    }
} public enum ReminderRepeatEndType {
    FOREVER(R.string.reminder_repeat_end_type_forever),
    UNTIL_DATE(R.string.reminder_repeat_end_type_until_date),
    FOR_X_EVENTS(R.string.reminder_repeat_end_type_for_x_events);

    private @StringRes
    int friendlyNameRes;

    ReminderRepeatEndType(@StringRes int friendlyNameRes) {
        this.friendlyNameRes = friendlyNameRes;

    }

    public int getFriendlyNameRes() {
        return friendlyNameRes;
    }

    public static List<String> getFriendlyValues(Context context) {
        List<String> friendlyValues = new ArrayList<>();
        for (ReminderRepeatEndType rc : values()) {
            friendlyValues.add(context.getResources().getString(rc.friendlyNameRes));
        }
        return friendlyValues;
    }
} public enum ReminderRepeatType {
    DAILY(R.string.reminder_repeat_type_daily),
    WEEKLY(R.string.reminder_repeat_type_weekly),
    MONTHLY(R.string.reminder_repeat_type_monthly),
    YEARLY(R.string.reminder_repeat_type_yearly);

    private @StringRes
    int friendlyNameRes;

    ReminderRepeatType(@StringRes int friendlyNameRes) {
        this.friendlyNameRes = friendlyNameRes;

    }

    public int getFriendlyNameRes() {
        return friendlyNameRes;
    }

    public static List<String> getFriendlyValues(Context context) {
        List<String> friendlyValues = new ArrayList<>();
        for (ReminderRepeatType rc : values()) {
            friendlyValues.add(context.getResources().getString(rc.friendlyNameRes));
        }
        return friendlyValues;
    }
} public enum TaskSortType {
    DATE(R.string.task_sort_type_date, R.string.task_sort_type_date_message),
    PLACE(R.string.task_sort_type_location, R.string.task_sort_type_location_message);

    private @StringRes
    int friendlyNameRes;
    int friendlyMessageRes;

    TaskSortType(@StringRes int friendlyNameRes, @StringRes int friendlyMessageRes) {
        this.friendlyNameRes = friendlyNameRes;
        this.friendlyMessageRes = friendlyMessageRes;

    }

    public int getFriendlyNameRes() {
        return friendlyNameRes;
    }

    public int getFriendlyMessageRes() {
        return friendlyMessageRes;
    }

    public static List<String> getFriendlyValues(Context context) {
        List<String> friendlyValues = new ArrayList<>();
        for (TaskSortType tst : values()) {
            friendlyValues.add(context.getResources().getString(tst.friendlyNameRes));
        }
        return friendlyValues;
    }
} public enum TaskStatus implements Serializable {
    UNPROGRAMMED(R.string.task_status_unprogrammed),
    PROGRAMMED(R.string.task_status_programmed),
    DONE(R.string.task_status_done);

    private @StringRes
    int friendlyNameRes;

    TaskStatus(@StringRes int friendlyNameRes) {
        this.friendlyNameRes = friendlyNameRes;

    }

    public static List<String> getFriendlyValues(Context context) {
        List<String> friendlyValues = new ArrayList<>();
        for (TaskStatus ts : values()) {
            friendlyValues.add(context.getResources().getString(ts.friendlyNameRes));
        }
        return friendlyValues;
    }

    /*
    * PROGRAMMED =      Tasks UNCOMPLETED by user. TaskStatus.ACTIVE + TaskStatus.OVERDUE
    * UNPROGRAMMED =    Tasks UNCOMPLETED by user. Tasks with ReminderType=NONE
    * DONE =            Tasks COMPLETED by user. doneDate != null
    * */
} public enum ReminderType implements Serializable {
    NONE(R.string.reminder_type_none),
    ONE_TIME(R.string.reminder_type_one_time),
    REPEATING(R.string.reminder_type_repeating),
    LOCATION_BASED(R.string.reminder_type_location_based);

    private @StringRes
    int friendlyNameRes;

    ReminderType(@StringRes int friendlyNameRes) {
        this.friendlyNameRes = friendlyNameRes;

    }

    public int getFriendlyNameRes() {
        return friendlyNameRes;
    }

    public static List<String> getFriendlyValues(Context context) {
        List<String> friendlyValues = new ArrayList<>();
        for (ReminderType rt : values()) {
            friendlyValues.add(context.getResources().getString(rt.friendlyNameRes));
        }
        return friendlyValues;
    }
} public enum TaskViewModelType implements Serializable {
    HEADER,
    UNPROGRAMMED_REMINDER,
    ONE_TIME_REMINDER,
    REPEATING_REMINDER,
    LOCATION_BASED_REMINDER
} public class CouldNotDeleteDataException extends Exception {

    private static final String DEFAULT_MESSAGE = "Data could not be deleted from the database.";

    public CouldNotDeleteDataException() {
        super(DEFAULT_MESSAGE);
    }
    public CouldNotDeleteDataException(String message) {
        super(message);
    }
    public CouldNotDeleteDataException(String message, Throwable cause) {
        super(message, cause);
    }

} public class CouldNotGetDataException extends Exception {

    private static final String DEFAULT_MESSAGE = "Data could not be fetched from the database.";

    public CouldNotGetDataException() {
        super(DEFAULT_MESSAGE);
    }
    public CouldNotGetDataException(String message) {
        super(message);
    }
    public CouldNotGetDataException(String message, Throwable cause) {
        super(message, cause);
    }

} public class CouldNotInsertDataException extends Exception {

    private static final String DEFAULT_MESSAGE = "Could not insert data into the database.";

    public CouldNotInsertDataException() {
        super(DEFAULT_MESSAGE);
    }
    public CouldNotInsertDataException(String message) {
        super(message);
    }
    public CouldNotInsertDataException(String message, Throwable cause) {
        super(message, cause);
    }

} public class CouldNotUpdateDataException extends Exception {

    private static final String DEFAULT_MESSAGE = "Data could not be updated on the database.";

    public CouldNotUpdateDataException() {
        super(DEFAULT_MESSAGE);
    }
    public CouldNotUpdateDataException(String message) {
        super(message);
    }
    public CouldNotUpdateDataException(String message, Throwable cause) {
        super(message, cause);
    }

} public class PlaceNotFoundException extends Exception {

    private static final String DEFAULT_MESSAGE = "Place not found in the database.";

    public PlaceNotFoundException() {
        super(DEFAULT_MESSAGE);
    }
    public PlaceNotFoundException(String message) {
        super(message);
    }
    public PlaceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }

} public class UnprogrammedTasksByTitleComparator implements Comparator<Task> {
    @Override
    public int compare(Task o1, Task o2) {
        if(o1.getTitle() == null)
            return -1;
        if(o2.getTitle() == null)
            return 1;
        return o1.getTitle().compareTo(o2.getTitle());
    }
} public abstract class Attachment implements Serializable {
    private int id;
    private int taskId;

    public Attachment() {}   //id-less Constructor used when creating Attachments
    public Attachment(int id, int taskId) {
        this.id = id;
        this.taskId = taskId;
    }

    public abstract AttachmentType getType();

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    public int getTaskId() {
        return taskId;
    }
    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

} public class AudioAttachment extends Attachment {

    private String audioFilename;

    public AudioAttachment() { /* Parameter-less constructor for audio attachment creation */ }
    public AudioAttachment(String audioFilename) {
        this.audioFilename = audioFilename;
    }
    public AudioAttachment(int id, int reminderId, String audioFilename) {
        super(id, reminderId);
        this.audioFilename = audioFilename;
    }

    @Override
    public AttachmentType getType() {
        return AttachmentType.AUDIO;
    }

    public String getAudioFilename() {
        return audioFilename;
    }
    public void setAudioFilename(String audioFilename) {
        this.audioFilename = audioFilename;
    }
} public class ImageAttachment extends Attachment {

    private byte[] thumbnail;
    private String imageFilename;

    public ImageAttachment() { /* Parameter-less constructor for image attachment creation */ }
    public ImageAttachment(byte[] thumbnail, String imageFilename) {
        this.thumbnail = thumbnail;
        this.imageFilename = imageFilename;
    }
    public ImageAttachment(int id, int reminderId, byte[] thumbnail, String fullImagePath) {
        super(id, reminderId);
        this.thumbnail = thumbnail;
        this.imageFilename = fullImagePath;
    }

    @Override
    public AttachmentType getType() {
        return AttachmentType.IMAGE;
    }


    public byte[] getThumbnail() {
        return thumbnail;
    }
    public void setThumbnail(byte[] thumbnail) {
        this.thumbnail = thumbnail;
    }

    public String getImageFilename() {
        return imageFilename;
    }
    public void setImageFilename(String imageFilename) {
        this.imageFilename = imageFilename;
    }
} public class LinkAttachment extends Attachment {

    private String link;

    public LinkAttachment(String link) {
        this.link = link;
    }
    public LinkAttachment(int id, int reminderId, String link) {
        super(id, reminderId);
        this.link = link;
    }

    @Override
    public AttachmentType getType() {
        return AttachmentType.LINK;
    }


    public String getLink() {
        return link;
    }
    public void setLink(String link) throws MalformedLinkException {
        if(isValid(link))
            this.link = link;
        else
            throw new MalformedLinkException("Link '" + link + "' is invalid");
    }
    private boolean isValid(String link) {
        return Patterns.WEB_URL.matcher(link).matches();
    }
} public class ListAttachment extends Attachment implements Serializable {

    private List<ListItemAttachment> items;

    public ListAttachment() {
        this.items = new ArrayList<>();
    }
    public ListAttachment(int id, int reminderId, String itemsJsonText) {
        super(id, reminderId);

        setItemsJson(itemsJsonText);
    }

    @Override
    public AttachmentType getType() {
        return AttachmentType.LIST;
    }


    public List<ListItemAttachment> getItems() {
        return items;
    }

    public void removeItem(int position) {

    }

    public String getItemsJson() {
        Gson gson = new Gson();
        return gson.toJson(items);
    }

    public void setItemsJson(String itemsJsonText) {
        Type listType = new TypeToken<List<ListItemAttachment>>() {}.getType();
        Gson gson = new Gson();
        items = gson.fromJson(itemsJsonText, listType);
    }

} public class LocationBasedReminder extends Reminder implements Serializable {

    private int placeId;
    private Place place;
    private boolean triggerEntering;
    private boolean triggerExiting;

    public LocationBasedReminder() {} //Parameter-less argument for Reminder creation

    public LocationBasedReminder(int placeId, @NonNull Place place, boolean triggerEntering, boolean triggerExiting) {
        init(placeId, place, triggerEntering, triggerExiting);
    }

    public LocationBasedReminder(int id, int taskId, int placeId, @Nullable Place place, boolean triggerEntering, boolean triggerExiting) {
        super(id, taskId);
        init(placeId, place, triggerEntering, triggerExiting);
    }

    private void init(int placeId, @Nullable Place place, boolean triggerEntering, boolean triggerExiting) {
        this.placeId = placeId;
        this.place = place;
        this.triggerEntering = triggerEntering;
        this.triggerExiting = triggerExiting;
    }


    @Override
    public ReminderType getType() {
        return ReminderType.LOCATION_BASED;
    }

    public int getPlaceId() {
        return placeId;
    }
    public void setPlaceId(int placeId) {
        this.placeId = placeId;
    }

    public Place getPlace() {return place;}
    public void setPlace(Place place) {this.place = place;}

    public boolean getTriggerEntering() {return triggerEntering;}
    public void setTriggerEntering(boolean triggerEntering) {this.triggerEntering = triggerEntering;}

    public boolean getTriggerExiting() {return triggerExiting;}
    public void setTriggerExiting(boolean triggerExiting) {this.triggerExiting = triggerExiting;}

    public String getEnteringExitingString(Context context) {
        if(triggerEntering && triggerExiting)
             return context.getResources().getString(R.string.fragment_detail_location_based_reminder_entering_or_exiting);
        else if(triggerEntering)
            return context.getResources().getString(R.string.fragment_detail_location_based_reminder_entering);
        else
            return context.getResources().getString(R.string.fragment_detail_location_based_reminder_exiting);
    }

    @Override
    public String toString() {
        String                      res = "Reminder ID=" + getId() + "\r\n";
                                    res += " Type=" + getType().name() + "\r\n";
                                    res += " TaskID=" + getTaskId() + "\r\n";
                                    res += " PlaceID=" + placeId + "\r\n";
        if(place != null)           res +=  " Place=" + place.toString() + "\r\n";
                                    res +=  " triggerEntering=" + triggerEntering + "\r\n";
                                    res +=  " triggerExiting=" + triggerExiting;
        return res;
    }
} public class OneTimeReminder extends Reminder implements Serializable {

    private Calendar date;
    private Time time;

    public OneTimeReminder() {} //Parameter-less constructor used when creating reminder

    public OneTimeReminder(@NonNull Calendar date, @NonNull Time time) {
        init(date, time);
    }

    public OneTimeReminder(int id, int taskId, @NonNull Calendar date, @NonNull Time time) {
        super(id, taskId);
        init(date, time);
    }

    private void init(@NonNull Calendar date, @NonNull Time time) {
        this.date = date;
        this.time = time;
    }


    @Override
    public ReminderType getType() {
        return ReminderType.ONE_TIME;
    }

    public Calendar getDate() {
        return date;
    }
    public void setDate(Calendar date) {
        this.date = date;
    }

    public Time getTime() {
        return time;
    }
    public void setTime(Time time) {
        this.time = time;
    }


    @Override
    public String toString() {
        String                      res = "Reminder ID=" + getId() + "\r\n";
                                    res += " Type=" + getType().name() + "\r\n";
                                    res += " TaskID=" + getTaskId() + "\r\n";
                                    res +=  " Date=" + date.toString() + "\r\n";
                                    res +=  " Time=" + time.toString();
        return res;
    }
} public abstract class Reminder implements Serializable {

    private int id;
    private int taskId;

    public Reminder(){
        this.id = -1;
        this.taskId = -1;
    }

    public Reminder(int id, int taskId) {
        this.id = id;
        this.taskId = taskId;
    }

    public abstract ReminderType getType();

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    public int getTaskId() {
        return taskId;
    }
    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }
} public class RepeatingReminder extends Reminder implements Serializable {

    private Calendar date;
    private Time time;
    private ReminderRepeatType repeatType;
    private int repeatInterval;
    private ReminderRepeatEndType repeatEndType;
    private int repeatEndNumberOfEvents;
    private Calendar repeatEndDate;


    public RepeatingReminder() {} //Parameter-less argument for Reminder creation

    public RepeatingReminder(@NonNull Calendar date, @NonNull Time time, @NonNull ReminderRepeatType repeatType, int repeatInterval,
                             @NonNull ReminderRepeatEndType repeatEndType, int repeatEndNumberOfEvents, @Nullable Calendar repeatEndDate) {
        init(date, time, repeatType, repeatInterval, repeatEndType, repeatEndNumberOfEvents, repeatEndDate);
    }

    public RepeatingReminder(int id, int taskId, @NonNull Calendar date, @NonNull Time time, @NonNull ReminderRepeatType repeatType, int repeatInterval,
                             @NonNull ReminderRepeatEndType repeatEndType, int repeatEndNumberOfEvents, @Nullable Calendar repeatEndDate) {
        super(id, taskId);
        init(date, time, repeatType, repeatInterval, repeatEndType, repeatEndNumberOfEvents, repeatEndDate);

    }

    private void init(@NonNull Calendar date, @NonNull Time time, @NonNull ReminderRepeatType repeatType, int repeatInterval,
                      @NonNull ReminderRepeatEndType repeatEndType, int repeatEndNumberOfEvents, @Nullable Calendar repeatEndDate) {
        this.date = date;
        this.time = time;
        this.repeatType = repeatType;
        this.repeatInterval = repeatInterval;
        this.repeatEndType = repeatEndType;
        this.repeatEndNumberOfEvents = repeatEndNumberOfEvents;
        this.repeatEndDate = repeatEndDate;
    }


    @Override
    public ReminderType getType() {
        return ReminderType.REPEATING;
    }


    public String getRepeatText(Context context) {
        String result = context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_interval_every) + " ";
        switch (getRepeatType()) {
            case DAILY:
                result  += getRepeatInterval() + " " + context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_interval_days);
                break;
            case WEEKLY:
                result  += getRepeatInterval() + " " + context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_interval_weeks);
                break;
            case MONTHLY:
                result  += getRepeatInterval() + " " + context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_interval_months);
                break;
            case YEARLY:
                result  += getRepeatInterval() + " " + context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_interval_years);
                break;
        }

        result += ", ";

        switch (getRepeatEndType()) {
            case FOREVER:
                result  += context.getResources().getString(R.string.reminder_repeat_end_type_forever);
                break;
            case FOR_X_EVENTS:
                result  += context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_end_for_x_events_1) + " " + getRepeatEndNumberOfEvents() + " " +
                context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_end_for_x_events_2);
                break;
            case UNTIL_DATE:
                DateFormat df = SharedPreferenceUtil.getDateFormat(context);
                result  += context.getResources().getString(R.string.fragment_edit_repeating_reminder_repeat_end_until_date) + " " + df.formatCalendar(getRepeatEndDate());
                break;
        }

        return result;
    }


    public Calendar getDate() {
        return date;
    }
    public void setDate(Calendar date) {
        this.date = date;
    }

    public Time getTime() {
        return time;
    }
    public void setTime(Time time) {
        this.time = time;
    }

    public ReminderRepeatType getRepeatType() {
        return repeatType;
    }
    public void setRepeatType(ReminderRepeatType repeatType) {
        this.repeatType = repeatType;
    }

    public int getRepeatInterval() {
        return repeatInterval;
    }
    public void setRepeatInterval(int repeatInterval) {
        this.repeatInterval = repeatInterval;
    }

    public ReminderRepeatEndType getRepeatEndType() {
        return repeatEndType;
    }
    public void setRepeatEndType(ReminderRepeatEndType repeatEndType) {
        this.repeatEndType = repeatEndType;
    }

    public int getRepeatEndNumberOfEvents() {
        return repeatEndNumberOfEvents;
    }
    public void setRepeatEndNumberOfEvents(int repeatEndNumberOfEvents) {
        this.repeatEndNumberOfEvents = repeatEndNumberOfEvents;
    }

    public Calendar getRepeatEndDate() {
        return repeatEndDate;
    }
    public void setRepeatEndDate(Calendar repeatEndDate) {
        this.repeatEndDate = repeatEndDate;
    }



    @Override
    public String toString() {
        String                      res = "Reminder ID=" + getId() + "\r\n";
                                    res +=  " Type=" + getType().name() + "\r\n";
                                    res +=  " TaskID=" + getTaskId() + "\r\n";
                                    res +=  " Date=" + date.toString() + "\r\n";
                                    res +=  " Time=" + time.toString() + "\r\n";
                                    res +=  " RepeatType=" + repeatType.toString() + "\r\n";
                                    res +=  " RepeatInterval=" + repeatInterval + "\r\n";
                                    res +=  " RepeatEndType=" + repeatEndType.toString() + "\r\n";
                                    res +=  " RepeatEndNumberOfEvents=" + repeatEndNumberOfEvents + "\r\n";
        if(repeatEndDate != null)   res +=  " RepeatEndDate=" + repeatEndDate.toString();
        return res;
    }
} public class Place implements Serializable {

    private static final int DEFAULT_RADIUS = 500;  //Default radius 500m

    private int id;
    private String alias;
    private String address;
    private double latitude;
    private double longitude;
    private int radius;
    private boolean isOneOff;       //Places are one-off when a reminder is created with a Place = Other,
                                    // The place is saved in the database but it isn't a frequent one nor will it appear in saved places

    public Place(String alias, String address, double latitude, double longitude, int radius, boolean isOneOff) {
        this.alias = alias;
        this.address = address;
        this.latitude = latitude;
        this.longitude = longitude;
        this.radius = radius;
        this.isOneOff = isOneOff;
    }

    public Place(int id, String alias, String address, double latitude, double longitude, int radius, boolean isOneOff) {
        this(alias, address, latitude, longitude, radius, isOneOff);
        this.id = id;
    }

    public Place(Place place) {
        this(place.getId(), place.getAlias(), place.getAddress(), place.getLatitude(), place.getLongitude(), place.getRadius(), place.isOneOff());
    }

    public Place() {
        radius = DEFAULT_RADIUS;
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    public String getAlias() {
        return alias;
    }
    public void setAlias(String alias) {
        this.alias = alias;
    }

    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }

    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }
    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public int getRadius() {
        return radius;
    }
    public void setRadius(int radius) {
        this.radius = radius;
    }

    public boolean isOneOff() {
        return isOneOff;
    }
    public void setOneOff(boolean oneOff) {
        isOneOff = oneOff;
    }

    @Override
    public String toString() {
        return  "ID=" + id + "\r\n" +
                " alias=" + alias + "\r\n" +
                " address=" + address + "\r\n" +
                " latitude=" + latitude + "\r\n" +
                " longitude=" + longitude + "\r\n" +
                " radius=" + radius + "\r\n" +
                " isOneOff=" + isOneOff;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == null)
            return false;

        if(obj instanceof Place) {
            Place that = (Place) obj;
            return (this.getId() == that.getId() &&
                    this.getAlias().equals(that.getAlias()) &&
                    this.getLatitude() == that.getLatitude() &&
                    this.getLongitude() == that.getLongitude() &&
                    this.getRadius() == that.getRadius());
        }
        return false;
    }
} public class Task implements Serializable {

    private int id;
    private TaskStatus status;
    private String title;
    private String description;
    private TaskCategory category;
    private ReminderType reminderType;
    private Reminder reminder;
    private Calendar doneDate;
    private ArrayList<Attachment> attachments;

    public Task() {     //Empty constructor for creating new tasks
        this.id = -1;
        this.status = TaskStatus.UNPROGRAMMED;
        this.title = "";
        this.description = "";
        this.reminderType = ReminderType.NONE;
        this.attachments = new ArrayList<>();
    }

    public Task(@NonNull TaskStatus status, @NonNull String title, @NonNull String description, @NonNull TaskCategory category, @NonNull ReminderType reminderType, @Nullable Reminder reminder, @Nullable Calendar doneDate) {
        this.status = status;
        this.title = title;
        this.description = description;
        this.category = category;
        this.reminderType = reminderType;
        this.reminder = reminder;
        this.doneDate = doneDate;
        this.attachments = new ArrayList<>();
    }

    public Task(int id, @NonNull TaskStatus status, @NonNull String title, @NonNull String description, @NonNull TaskCategory category, @NonNull ReminderType reminderType, @Nullable Reminder reminder, @Nullable Calendar doneDate) {
        this(status, title, description, category, reminderType, reminder, doneDate);
        this.id = id;
    }


    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    public TaskStatus getStatus() {
        return status;
    }
    public void setStatus(TaskStatus status) {
        this.status = status;
    }

    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }

    public TaskCategory getCategory() {
        return category;
    }
    public void setCategory(TaskCategory category) {
        this.category = category;
    }

    public ReminderType getReminderType() {
        return reminderType;
    }
    public void setReminderType(ReminderType reminderType) {
        this.reminderType = reminderType;
    }

    public Reminder getReminder() {
        return reminder;
    }
    public void setReminder(Reminder reminder) {
        this.reminder = reminder;
    }

    public Calendar getDoneDate() {
        return doneDate;
    }
    public void setDoneDate(Calendar doneDate) {
        this.doneDate = doneDate;
    }

    public ArrayList<Attachment> getAttachments() {
        return attachments;
    }
    public void setAttachments(ArrayList<Attachment> attachments) {
        this.attachments = attachments;
    }
    public void addAttachment(Attachment attachment) {
        this.attachments.add(attachment);
    }
    public void clearAttachments() {
        attachments.clear();
    }



    @Override
    public String toString() {
        String                      res = "Task ID=" + id + "\r\n Status= " + status.name() + "\r\n Title=" + title + "\r\n Description=" + description + "\r\n Category=" + category.name() + "\r\n";
                                    res += " ReminderType=" + reminderType.name();
        if(reminder != null)        res += " Reminder=" + reminder.toString() + "\r\n";
        if(doneDate != null)        res += " DoneDate=" + doneDate.toString();
                                    res += " Attachments="  + attachments.size();
        return res;
    }
} public class TextAttachment extends Attachment {

    private String text;

    public TextAttachment(String text) {
        this.text = text;
    }
    public TextAttachment(int id, int reminderId, String text) {
        super(id, reminderId);
        this.text = text;
    }

    @Override
    public AttachmentType getType() {
        return AttachmentType.TEXT;
    }


    public String getText() {
        return text;
    }
    public void setText(String text) {
        this.text = text;
    }
} public class Time implements Comparable<Time>, Serializable {
    private int hour;
    private int minute;
    private TimeFormat displayTimeFormat = TimeFormat.FORMAT_24H;

    public Time() {
        this.hour = 0;
        this.minute = 0;
    }
    public Time(int timeInMinutes) {
        setTimeInMinutes(timeInMinutes);
    }

    public Time(int hour, int minute) {
        this.hour = hour;
        this.minute = minute;
    }

    public Time(int hour, int minute, TimeFormat displayTimeFormat) {
        this(hour, minute);
        this.displayTimeFormat = displayTimeFormat;
    }

    public int getHour(){
        return hour;
    }
    public void setHour(int hour) throws InvalidParameterException {
        if(hour >= 0 && hour <= 24)
            this.hour = hour;
        else
            throw new InvalidParameterException("Value of parameter (" + hour + "), is outside 0-24");
    }

    public int getMinute() {
        return minute;
    }
    public void setMinute(int minute) throws InvalidParameterException {
        if(hour >= 0 && hour <= 24)
            this.minute = minute;
        else
            throw new InvalidParameterException("Value of parameter (" + minute + "), is outside 0-60");
    }


    public TimeFormat getDisplayTimeFormat() {
        return displayTimeFormat;
    }
    public void setDisplayTimeFormat(TimeFormat displayTimeFormat) {
        this.displayTimeFormat = displayTimeFormat;
    }




    public boolean before(Time when) throws IllegalArgumentException {
        if(when == null)
            throw new IllegalArgumentException("Argument is null. Cannot compare");
        return this.getTimeInMinutes() < when.getTimeInMinutes();
    }

    public boolean after(Time when) throws IllegalArgumentException {
        if(when == null)
            throw new IllegalArgumentException("Argument is null. Cannot compare");
        return this.getTimeInMinutes() > when.getTimeInMinutes();
    }

    public int getTimeInMinutes() {
        return (hour*60) + minute;
    }
    public void setTimeInMinutes(int minutes) {
        if(minutes >= 0 && minutes <= 24*60) {
            this.hour = minutes/60;
            this.minute = minutes%60;
        }
        else
            throw new InvalidParameterException("Value of parameter (" + minute + "), is outside 0-60");
    }

    @Override
    public int compareTo(Time o) {
        int thisTime = this.getTimeInMinutes();
        int thatTime = o.getTimeInMinutes();
        return (thisTime<thatTime ? -1 : (thisTime==thatTime ? 0 : 1));
    }

    @Override
    public String toString() {
        switch (displayTimeFormat) {
            case FORMAT_24H:
                return String.format(Locale.getDefault(), "%1$02d:%2$02d", hour, minute);
            case FORMAT_12H:
                String amPm = "am";
                int hour12h = hour;
                if(hour > 12) {
                    amPm = "pm";
                    hour12h = hour - 12;
                }
                return String.format(Locale.getDefault(), "%1$02d:%2$02d %3$s", hour12h, minute, amPm);
            default:
                throw new IllegalArgumentException("displayTimeFormat is null. Cannot toString()");
        }
    }
} public class CalendarUtil {

    public static Calendar getNewInstanceZeroedCalendar() {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal;
    }

    public static void copyCalendar(Calendar copyFrom, Calendar copyTo) {
        if(copyFrom == null || copyTo == null)
            throw new NullPointerException("copyCalendar(), One of both parameters are null");

        copyTo.setTimeZone(copyFrom.getTimeZone());
        copyTo.setTimeInMillis(copyFrom.getTimeInMillis());
    }

    public static long getDifferenceMinutesBetween(Calendar a, Calendar b) {
        long differenceMinutes = ( a.getTimeInMillis() - b.getTimeInMillis() ) / 60000; //60 * 1000    toMinutes * toSeconds
        return differenceMinutes;
    }

    public static Calendar getCalendarFromDateAndTime(Calendar date, Time time) {
        if(date == null || time == null)
            throw new NullPointerException("getCalendarFromDateAndTime(), One of both parameters are null");

        Calendar cal = Calendar.getInstance();
        copyCalendar(date, cal);

        cal.set(Calendar.HOUR_OF_DAY, time.getHour());
        cal.set(Calendar.MINUTE, time.getMinute());

        return cal;
    }
} public class SharedPreferenceUtil {

    public static boolean doShowTapTargetSequenceFor(Context context, TapTargetSequenceType tapTargetSequenceType) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);

        boolean flag = preferences.getBoolean(tapTargetSequenceType.name(), true);
        //boolean flag = true;

        if(flag)
            preferences.edit().putBoolean(tapTargetSequenceType.name(), false).apply();

        return flag;
    }

    public static DateFormat getDateFormat(Context context) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        String dfPref = preferences.getString(context.getResources().getString(R.string.settings_date_format_key), null);
        DateFormat pref;
        try {
            pref = DateFormat.valueOf(dfPref);
        } catch (Exception e) {
            pref = null;
        }

        if(pref == null) {
            Log.d("SharedPreferenceUtil", "getDateFormat() found null, setting PRETTY_DATE");
            DateFormat df = DateFormat.PRETTY_DATE;
            setDateFormat(df, context);
            return df;
        }
        else return pref;
    }

    public static void setDateFormat(DateFormat df, Context context) {
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
        editor.putString(context.getResources().getString(R.string.settings_date_format_key), df.name());
        editor.apply();
    }





    public static TimeFormat getTimeFormat(Context context) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        String tfPref = preferences.getString(context.getResources().getString(R.string.settings_time_format_key), null);
        TimeFormat pref;
        try {
            pref = TimeFormat.valueOf(tfPref);
        } catch (Exception e) {
            pref = null;
        }

        if(pref == null) {
            Log.d("SharedPreferenceUtil", "getTimeFormat() found null, setting FORMAT_24H");
            TimeFormat tf = TimeFormat.FORMAT_24H;
            setTimeFormat(tf, context);
            return tf;
        }
        else return pref;
    }

    public static void setTimeFormat(TimeFormat tf, Context context) {
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
        editor.putString(context.getResources().getString(R.string.settings_time_format_key), tf.name());
        editor.apply();
    }


    public static List<Integer> getTriggeredTaskList(Context context) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        String json = preferences.getString(context.getResources().getString(R.string.settings_triggered_task_list_key), null);
        List<Integer> tasks = new ArrayList<>();

        if(json != null) {
            try {
                Type listType = new TypeToken<List<Integer>>() {}.getType();
                Gson gson = new Gson();
                tasks = gson.fromJson(json, listType);
            } catch (Exception e) { /* Do nothing */}
        }

        return tasks;
    }

    public static void setTriggeredTaskList(List<Integer> tasks, Context context) {
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
        editor.putString(context.getResources().getString(R.string.settings_triggered_task_list_key), new Gson().toJson(tasks));
        editor.apply();
    }


    public static void removeIdFromTriggeredTasks(Context context, int taskId) {
        List<Integer> triggeredTasks = getTriggeredTaskList(context);
        Iterator<Integer> iter = triggeredTasks.iterator();
        while(iter.hasNext()) {
            if (iter.next().equals(taskId)) {
                iter.remove();
            }
        }

        setTriggeredTaskList(triggeredTasks, context);
    }

    public static TriggerMinutesBeforeNotificationType getTriggerMinutesBeforeNotification(Context context) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        String tmPref = preferences.getString(context.getResources().getString(R.string.settings_trigger_minutes_before_notification_key), null);
        TriggerMinutesBeforeNotificationType pref;
        try {
            pref = TriggerMinutesBeforeNotificationType.valueOf(tmPref);
        } catch (Exception e) {
            pref = null;
        }

        if(pref == null) {
            Log.d("SharedPreferenceUtil", "getTriggerMinutesBeforeNotification() found null, setting 5 minutes");
            TriggerMinutesBeforeNotificationType tm = TriggerMinutesBeforeNotificationType.MINUTES_5;
            setTriggerMinutesBeforeNotification(tm, context);
            return tm;
        }
        else return pref;
    }

    public static void setTriggerMinutesBeforeNotification(TriggerMinutesBeforeNotificationType triggerMinutesBeforeNotification, Context context) {
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
        editor.putString(context.getResources().getString(R.string.settings_trigger_minutes_before_notification_key), triggerMinutesBeforeNotification.name());
        editor.apply();
    }

} public class TaskUtil {


    public static boolean checkIfOverdue(Reminder reminder) {
        if(reminder == null)
            return false;

        if(reminder.getType().equals(ReminderType.LOCATION_BASED))
            return false;

        if(reminder.getType().equals(ReminderType.REPEATING)) {
            return ( getRepeatingReminderNextCalendar((RepeatingReminder) reminder) == null );
        }

        //For OneTime reminders
        CalendarPeriod overdueCP = new CalendarPeriod(CalendarPeriodType.OVERDUE);
        Calendar endDate = getReminderEndCalendar(reminder);
        return overdueCP.isInPeriod(endDate);
    }


    public static Calendar getReminderEndCalendar(Reminder reminder) {
        switch (reminder.getType()) {
            case NONE:
            case LOCATION_BASED:
                return null;

            case ONE_TIME:
                return CalendarUtil.getCalendarFromDateAndTime( ((OneTimeReminder)reminder).getDate(), ((OneTimeReminder)reminder).getTime() );

            case REPEATING:
                return getRepeatingReminderEndCalendar(((RepeatingReminder)reminder));

            default:
                throw new InvalidParameterException("Invalid ReminderType param on TaskUtil.getReminderEndCalendar()");
        }
    }


    public static Calendar getRepeatingReminderEndCalendar(RepeatingReminder repeatingReminder) {
        Calendar cal = Calendar.getInstance();

        switch (repeatingReminder.getRepeatEndType()) {
            case FOREVER:
                //TODO: Need to return Date.MAX or so
                cal.add(Calendar.YEAR, 100);
                break;

            case UNTIL_DATE:
                cal = CalendarUtil.getCalendarFromDateAndTime( repeatingReminder.getRepeatEndDate(), repeatingReminder.getTime());
                break;

            case FOR_X_EVENTS:
                cal = CalendarUtil.getCalendarFromDateAndTime( repeatingReminder.getDate(), repeatingReminder.getTime());
                int dateField = getDateFieldFromRepeatType(repeatingReminder.getRepeatType());

                for (int i = 0; i < repeatingReminder.getRepeatEndNumberOfEvents(); i++)
                    cal.add(dateField, repeatingReminder.getRepeatInterval());
                break;
        }
        return cal;
    }


    public static Calendar getRepeatingReminderNextCalendar(RepeatingReminder repeatingReminder) {

        Calendar today = Calendar.getInstance();
        Calendar endDate = getRepeatingReminderEndCalendar(repeatingReminder);
        Calendar cal = CalendarUtil.getCalendarFromDateAndTime( repeatingReminder.getDate(), repeatingReminder.getTime());

        //TODO: Cant use getDateFieldFromRepeatType(), Gives off a weird warning
        //final int dateField = getDateFieldFromRepeatType(repeatingReminder.getRepeatType());


        while(true) {
            if (cal.compareTo(endDate) >= 0)    //If cal passed endDate, reminder is overdue, return null
                return null;
            if(cal.compareTo(today) >= 0) {
                return cal;
            }

            //TODO: Cant use getDateFieldFromRepeatType(), Gives off a weird warning
            //cal.add(dateField, repeatingReminder.getRepeatInterval()); break;
            switch (repeatingReminder.getRepeatType()) {
                case DAILY: cal.add(Calendar.DAY_OF_WEEK, repeatingReminder.getRepeatInterval()); break;
                case WEEKLY: cal.add(Calendar.WEEK_OF_YEAR, repeatingReminder.getRepeatInterval()); break;
                case MONTHLY: cal.add(Calendar.MONTH, repeatingReminder.getRepeatInterval()); break;
                case YEARLY: cal.add(Calendar.YEAR, repeatingReminder.getRepeatInterval()); break;
                default: throw new InvalidParameterException("Invalid RepeatType parameter in TaskUtil.getRepeatingReminderEndCalendar()");
            }
        }
    }

    private static int getDateFieldFromRepeatType(ReminderRepeatType repeatType) {

        switch (repeatType) {
            case DAILY: return Calendar.DAY_OF_MONTH;
            case WEEKLY: return Calendar.WEEK_OF_YEAR;
            case MONTHLY: return Calendar.MONTH;
            case YEARLY: return Calendar.YEAR;
            default: throw new InvalidParameterException("Invalid RepeatType parameter in TaskUtil.getRepeatingReminderEndCalendar()");
        }
    }


} public class TaskSortingUtil {

    private CalendarPeriod periodLastYear;
    private CalendarPeriod periodLastMonth;
    private CalendarPeriod periodLastWeek;
    private CalendarPeriod periodYesterday;
    private CalendarPeriod periodOverdue;
    private CalendarPeriod periodToday;
    private CalendarPeriod periodTomorrow;
    private CalendarPeriod periodThisWeek;
    private CalendarPeriod periodNextWeek;
    private CalendarPeriod periodThisMonth;
    private CalendarPeriod periodNextMonth;
    private CalendarPeriod periodThisYear;
    private CalendarPeriod periodNextYear;

    private ArrayList<Task> tasksPast;
    private ArrayList<Task> tasksLastYear;
    private ArrayList<Task> tasksLastMonth;
    private ArrayList<Task> tasksLastWeek;
    private ArrayList<Task> tasksYesterday;
    private ArrayList<Task> tasksOverdue;
    private ArrayList<Task> tasksLocationBased;
    private ArrayList<Task> tasksToday;
    private ArrayList<Task> tasksTomorrow;
    private ArrayList<Task> tasksThisWeek;
    private ArrayList<Task> tasksNextWeek;
    private ArrayList<Task> tasksThisMonth;
    private ArrayList<Task> tasksNextMonth;
    private ArrayList<Task> tasksThisYear;
    private ArrayList<Task> tasksNextYear;
    private ArrayList<Task> tasksFuture;

    public TaskSortingUtil() {
        periodLastYear = new CalendarPeriod(CalendarPeriodType.LAST_YEAR);
        periodLastMonth = new CalendarPeriod(CalendarPeriodType.LAST_MONTH);
        periodLastWeek = new CalendarPeriod(CalendarPeriodType.LAST_WEEK);
        periodYesterday = new CalendarPeriod(CalendarPeriodType.YESTERDAY);
        periodOverdue = new CalendarPeriod(CalendarPeriodType.OVERDUE);
        periodToday = new CalendarPeriod(CalendarPeriodType.TODAY);
        periodTomorrow = new CalendarPeriod(CalendarPeriodType.TOMORROW);
        periodThisWeek = new CalendarPeriod(CalendarPeriodType.THIS_WEEK);
        periodNextWeek = new CalendarPeriod(CalendarPeriodType.NEXT_WEEK);
        periodThisMonth = new CalendarPeriod(CalendarPeriodType.THIS_MONTH);
        periodNextMonth = new CalendarPeriod(CalendarPeriodType.NEXT_MONTH);
        periodThisYear = new CalendarPeriod(CalendarPeriodType.THIS_YEAR);
        periodNextYear = new CalendarPeriod(CalendarPeriodType.NEXT_YEAR);

        tasksPast = new ArrayList<>();
        tasksLastYear = new ArrayList<>();
        tasksLastMonth = new ArrayList<>();
        tasksLastWeek = new ArrayList<>();
        tasksYesterday = new ArrayList<>();
        tasksOverdue = new ArrayList<>();
        tasksLocationBased = new ArrayList<>();
        tasksToday = new ArrayList<>();
        tasksTomorrow = new ArrayList<>();
        tasksThisWeek = new ArrayList<>();
        tasksNextWeek = new ArrayList<>();
        tasksThisMonth = new ArrayList<>();
        tasksNextMonth = new ArrayList<>();
        tasksThisYear = new ArrayList<>();
        tasksNextYear = new ArrayList<>();
        tasksFuture = new ArrayList<>();
    }


    /* Programmed Tasks */
    public ArrayList<TaskViewModel> generateProgrammedTaskHeaderList(List<Task> tasks, TaskSortType sortType, Resources resources) throws InvalidClassException {
        ArrayList<TaskViewModel> result = new ArrayList<>();

        clearTaskBuckets();

        if(sortType == TaskSortType.DATE) {
            Collections.sort(tasks, new TasksByReminderDateComparator());   //TODO: this comparator needs to take into account the RepeatingReminder's "next" event date to sort Tasks with RepeatingReminders properly
            for (Task current : tasks) {

                if(current.getReminderType() == null)
                    throw new NullPointerException("NULL ReminderType passed into TaskSortingUtil.generateProgrammedTaskHeaderList()");

                switch (current.getReminderType()) {
                    case NONE:
                        throw new InvalidClassException("Wrong ReminderType passed into TaskSortingUtil.generateProgrammedTaskHeaderList()");
                    case LOCATION_BASED:
                        tasksLocationBased.add(current);
                        break;
                    case ONE_TIME:
                        insertProgrammedTaskIntoBucket(current, TaskUtil.getReminderEndCalendar(current.getReminder()));
                        break;
                    case REPEATING:
                        insertProgrammedTaskIntoBucket(current, TaskUtil.getRepeatingReminderNextCalendar(((RepeatingReminder)current.getReminder())) );
                        break;
                    default:
                        throw new InvalidParameterException("Unhandled ReminderType passed into TaskSortingUtil.generateProgrammedTaskHeaderList()");
                }
            }

            if(tasksOverdue.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_overdue), true));
                dumpTaskBucketIntoViewModelList(tasksOverdue, result);
            }

            if(tasksLocationBased.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_location_based), false));
                dumpTaskBucketIntoViewModelList(tasksLocationBased, result);
            }

            if(tasksToday.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_today), false));
                dumpTaskBucketIntoViewModelList(tasksToday, result);
            }

            if(tasksTomorrow.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_tomorrow), false));
                dumpTaskBucketIntoViewModelList(tasksTomorrow, result);
            }

            if(tasksThisWeek.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_this_week), false));
                dumpTaskBucketIntoViewModelList(tasksThisWeek, result);
            }
            if(tasksNextWeek.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_next_week), false));
                dumpTaskBucketIntoViewModelList(tasksNextWeek, result);
            }

            if(tasksThisMonth.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_this_month), false));
                dumpTaskBucketIntoViewModelList(tasksThisMonth, result);
            }

            if(tasksNextMonth.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_next_month), false));
                dumpTaskBucketIntoViewModelList(tasksNextMonth, result);
            }

            if(tasksThisYear.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_this_year), false));
                dumpTaskBucketIntoViewModelList(tasksThisYear, result);
            }

            if(tasksNextYear.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_next_year), false));
                dumpTaskBucketIntoViewModelList(tasksNextYear, result);
            }

            if(tasksFuture.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_future), false));
                dumpTaskBucketIntoViewModelList(tasksFuture, result);
            }

        } else if(sortType == TaskSortType.PLACE) {
            handleSortingByPlace(tasks, result, resources);
        }


        return result;
    }

    private void insertProgrammedTaskIntoBucket(Task task, Calendar taskDate) {
        if(taskDate == null)
            tasksOverdue.add(task);

        else if(periodOverdue.isInPeriod(taskDate))
            tasksOverdue.add(task);

        else if (periodToday.isInPeriod(taskDate))
            tasksToday.add(task);

        else if (periodTomorrow.isInPeriod(taskDate))
            tasksTomorrow.add(task);

        else if (periodThisWeek.isInPeriod(taskDate))
            tasksThisWeek.add(task);

        else if (periodNextWeek.isInPeriod(taskDate))
            tasksNextWeek.add(task);

        else if (periodThisMonth.isInPeriod(taskDate))
            tasksThisMonth.add(task);

        else if (periodNextMonth.isInPeriod(taskDate))
            tasksNextMonth.add(task);

        else if (periodThisYear.isInPeriod(taskDate))
            tasksThisYear.add(task);

        else if (periodNextYear.isInPeriod(taskDate))
            tasksNextYear.add(task);

        else
            tasksFuture.add(task);
    }






    /* Done Tasks */
    public ArrayList<TaskViewModel> generateDoneTaskHeaderList(List<Task> tasks, TaskSortType sortType, Resources resources) throws InvalidClassException {
        ArrayList<TaskViewModel> result = new ArrayList<>();

        clearTaskBuckets();

        if(sortType == TaskSortType.DATE) {
            Collections.sort(tasks, new TasksByDoneDateComparator());   //Sorting by doneDate!

            for (Task current : tasks) {

                if(current.getReminderType() == null)
                    throw new NullPointerException("NULL ReminderType passed into TaskSortingUtil.generateProgrammedTaskHeaderList()");

                insertDoneTaskIntoBucket(current, current.getDoneDate());
            }

            if(tasksToday.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_today), false));
                dumpTaskBucketIntoViewModelList(tasksToday, result);
            }

            if(tasksYesterday.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_yesterday), false));
                dumpTaskBucketIntoViewModelList(tasksYesterday, result);
            }

            if(tasksThisWeek.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_this_week), false));
                dumpTaskBucketIntoViewModelList(tasksThisWeek, result);
            }
            if(tasksLastWeek.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_last_week), false));
                dumpTaskBucketIntoViewModelList(tasksLastWeek, result);
            }

            if(tasksThisMonth.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_this_month), false));
                dumpTaskBucketIntoViewModelList(tasksThisMonth, result);
            }

            if(tasksLastMonth.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_last_month), false));
                dumpTaskBucketIntoViewModelList(tasksLastMonth, result);
            }

            if(tasksThisYear.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_this_year), false));
                dumpTaskBucketIntoViewModelList(tasksThisYear, result);
            }

            if(tasksLastYear.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_last_year), false));
                dumpTaskBucketIntoViewModelList(tasksLastYear, result);
            }

            if(tasksPast.size() > 0) {
                result.add(new TaskViewModel(resources.getString(R.string.task_header_past), false));
                dumpTaskBucketIntoViewModelList(tasksPast, result);
            }

        } else if(sortType == TaskSortType.PLACE) {
            handleSortingByPlace(tasks, result, resources);
        }

        return result;
    }

    private void insertDoneTaskIntoBucket(Task task, Calendar taskDate) {
        if (periodToday.isInPeriod(taskDate))
            tasksToday.add(task);

        else if (periodYesterday.isInPeriod(taskDate))
            tasksYesterday.add(task);

        else if (periodThisWeek.isInPeriod(taskDate))
            tasksThisWeek.add(task);

        else if (periodLastWeek.isInPeriod(taskDate))
            tasksLastWeek.add(task);

        else if (periodThisMonth.isInPeriod(taskDate))
            tasksThisMonth.add(task);

        else if (periodLastMonth.isInPeriod(taskDate))
            tasksLastMonth.add(task);

        else if (periodThisYear.isInPeriod(taskDate))
            tasksThisYear.add(task);

        else if (periodLastYear.isInPeriod(taskDate))
            tasksLastYear.add(task);
        else
            tasksPast.add(task);
    }





    private void handleSortingByPlace(List<Task> tasks, ArrayList<TaskViewModel> result, Resources resources) {
        Collections.sort(tasks, new TasksByPlaceComparator());

        if(tasks.size() == 0)
            return;

        if(tasks.get(0).getReminderType() != ReminderType.LOCATION_BASED) {   //There are no Location-based reminders, insert them all into result
            result.add(new TaskViewModel(resources.getString(R.string.task_header_no_location), false));
            dumpTaskBucketIntoViewModelList(tasks, result);
        } else {
            String lastPlaceAlias = ((LocationBasedReminder)tasks.get(0).getReminder()).getPlace().getAlias();     //Grab first location alias
            result.add(new TaskViewModel(lastPlaceAlias, false));
            result.add(new TaskViewModel(tasks.get(0), TaskViewModelType.LOCATION_BASED_REMINDER));


            for (int i = 1; i < tasks.size(); i++) {

                if(tasks.get(i).getReminderType() != ReminderType.LOCATION_BASED) { //Got to a NON Location-based reminder? Add the rest of reminders and break.
                    result.add(new TaskViewModel(resources.getString(R.string.task_header_no_location), false));

                    List<Task> otherTasks = new ArrayList<>();
                    for (int j = i; j < tasks.size(); j++)
                        otherTasks.add(tasks.get(j));
                    dumpTaskBucketIntoViewModelList(otherTasks, result);
                    break;
                }

                String placeAlias = ((LocationBasedReminder)tasks.get(i).getReminder()).getPlace().getAlias();
                if(placeAlias.compareTo(lastPlaceAlias) != 0) {   //New location alias? Add a header
                    lastPlaceAlias = placeAlias;
                    result.add(new TaskViewModel(lastPlaceAlias, false));
                }
                result.add(new TaskViewModel(tasks.get(i), TaskViewModelType.LOCATION_BASED_REMINDER));
            }
        }
    }

    private void clearTaskBuckets() {
        tasksLastYear.clear();
        tasksLastMonth.clear();
        tasksLastWeek.clear();
        tasksYesterday.clear();
        tasksOverdue.clear();
        tasksLocationBased.clear();
        tasksToday.clear();
        tasksTomorrow.clear();
        tasksThisWeek.clear();
        tasksNextWeek.clear();
        tasksThisMonth.clear();
        tasksNextMonth.clear();
        tasksThisYear.clear();
        tasksNextYear.clear();
        tasksFuture.clear();
    }

    private void dumpTaskBucketIntoViewModelList(List<Task> tasks, List<TaskViewModel> result) {
        for (Task task: tasks) {
            switch (task.getReminderType()) {
                case NONE:
                    result.add(new TaskViewModel(task, TaskViewModelType.UNPROGRAMMED_REMINDER));
                    break;

                case ONE_TIME:
                    result.add(new TaskViewModel(task, TaskViewModelType.ONE_TIME_REMINDER));
                    break;

                case REPEATING:
                    result.add(new TaskViewModel(task, TaskViewModelType.REPEATING_REMINDER));
                    break;

                case LOCATION_BASED:
                    result.add(new TaskViewModel(task, TaskViewModelType.LOCATION_BASED_REMINDER));
                    break;

                default:
                    throw new InvalidParameterException("Unhandled ReminderType passed into TaskSortingUtil.dumpTaskBucketIntoViewModelList()");
            }
        }

    }



} public class TaskTriggerViewModel implements Serializable {

    //DATA
    private Task task = null;
    private Calendar triggerDate = null;
    private Time triggerTime = null;

    public TaskTriggerViewModel(@NonNull Task task, @NonNull Calendar triggerDate, @NonNull Time triggerTime) {
        this.task = task;
        this.triggerDate = triggerDate;
        this.triggerTime = triggerTime;
    }

    public Task getTask() { return task; }
    public Calendar getTriggerDate() { return triggerDate; }
    public Time getTriggerTime() { return triggerTime; }

    public Calendar getTriggerDateWithTime() {
        Calendar cal = CalendarUtil.getNewInstanceZeroedCalendar();
        cal.setTimeInMillis(getTriggerDate().getTimeInMillis());

        cal.set(Calendar.HOUR_OF_DAY, getTriggerTime().getHour());
        cal.set(Calendar.MINUTE, getTriggerTime().getMinute());

        return cal;
    }
} public class TaskViewModel {

    //DATA
    private Task task = null;
    private String headerTitle = null;
    private boolean headerTitleRed;
    private TaskViewModelType viewModelType;


    public TaskViewModel(@NonNull Task task, @NonNull TaskViewModelType viewModelType) {

        if(viewModelType == TaskViewModelType.HEADER)
            throw new InvalidParameterException("TaskViewModelType cannot be of type HEADER");

        this.task = task;
        this.viewModelType = viewModelType;
    }

    public TaskViewModel(@NonNull String headerTitle, @NonNull boolean headerTitleRed) {
        this.headerTitle = headerTitle;
        this.headerTitleRed = headerTitleRed;
        viewModelType = TaskViewModelType.HEADER;
    }


    public Task getTask() {
        return task;
    }
    public String getHeaderTitle() {
        return headerTitle;
    }
    public boolean isHeaderTitleRed() {return headerTitleRed;}

    public TaskViewModelType getViewModelType() {
        return viewModelType;
    }
}
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.gms.location.Geofence;
import java.io.InvalidClassException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import ve.com.abicelis.remindy.enums.AttachmentType;
import ve.com.abicelis.remindy.enums.TaskCategory;
import ve.com.abicelis.remindy.enums.ReminderRepeatEndType;
import ve.com.abicelis.remindy.enums.ReminderRepeatType;
import ve.com.abicelis.remindy.enums.TaskSortType;
import ve.com.abicelis.remindy.enums.TaskStatus;
import ve.com.abicelis.remindy.enums.ReminderType;
import ve.com.abicelis.remindy.enums.TaskViewModelType;
import ve.com.abicelis.remindy.exception.CouldNotDeleteDataException;
import ve.com.abicelis.remindy.exception.CouldNotGetDataException;
import ve.com.abicelis.remindy.exception.CouldNotInsertDataException;
import ve.com.abicelis.remindy.exception.CouldNotUpdateDataException;
import ve.com.abicelis.remindy.exception.PlaceNotFoundException;
import ve.com.abicelis.remindy.model.UnprogrammedTasksByTitleComparator;
import ve.com.abicelis.remindy.model.attachment.Attachment;
import ve.com.abicelis.remindy.model.attachment.AudioAttachment;
import ve.com.abicelis.remindy.model.attachment.ImageAttachment;
import ve.com.abicelis.remindy.model.attachment.LinkAttachment;
import ve.com.abicelis.remindy.model.attachment.ListAttachment;
import ve.com.abicelis.remindy.model.reminder.LocationBasedReminder;
import ve.com.abicelis.remindy.model.reminder.OneTimeReminder;
import ve.com.abicelis.remindy.model.reminder.Reminder;
import ve.com.abicelis.remindy.model.reminder.RepeatingReminder;
import ve.com.abicelis.remindy.model.Place;
import ve.com.abicelis.remindy.model.Task;
import ve.com.abicelis.remindy.model.attachment.TextAttachment;
import ve.com.abicelis.remindy.model.Time;
import ve.com.abicelis.remindy.util.CalendarUtil;
import ve.com.abicelis.remindy.util.SharedPreferenceUtil;
import ve.com.abicelis.remindy.util.TaskUtil;
import ve.com.abicelis.remindy.util.sorting.TaskSortingUtil;
import ve.com.abicelis.remindy.viewmodel.TaskTriggerViewModel;
import ve.com.abicelis.remindy.viewmodel.TaskViewModel;
    }


    private ContentValues getValuesFromRepeatingReminder(RepeatingReminder repeatingReminder) {
        ContentValues values = new ContentValues();
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_TASK_FK.getName(), repeatingReminder.getTaskId());
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_DATE.getName(), repeatingReminder.getDate().getTimeInMillis());
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_TIME.getName(), repeatingReminder.getTime().getTimeInMinutes());
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_REPEAT_TYPE.getName(), repeatingReminder.getRepeatType().name());
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_REPEAT_INTERVAL.getName(), repeatingReminder.getRepeatInterval());
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_REPEAT_END_TYPE.getName(), repeatingReminder.getRepeatEndType().name());
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_REPEAT_END_NUMBER_OF_EVENTS.getName(), (repeatingReminder.getRepeatEndType() == ReminderRepeatEndType.FOR_X_EVENTS ? repeatingReminder.getRepeatEndNumberOfEvents() : -1));
        values.put(RemindyContract.RepeatingReminderTable.COLUMN_NAME_REPEAT_END_DATE.getName(), (repeatingReminder.getRepeatEndType() == ReminderRepeatEndType.UNTIL_DATE ? repeatingReminder.getRepeatEndDate().getTimeInMillis() : -1));
        return values;
    }


    private ContentValues getValuesFromLocationBasedReminder(LocationBasedReminder locationBasedReminder) {
        ContentValues values = new ContentValues();
        values.put(RemindyContract.LocationBasedReminderTable.COLUMN_NAME_TASK_FK.getName(), locationBasedReminder.getTaskId());
        values.put(RemindyContract.LocationBasedReminderTable.COLUMN_NAME_PLACE_FK.getName(), locationBasedReminder.getPlaceId());
        values.put(RemindyContract.LocationBasedReminderTable.COLUMN_NAME_TRIGGER_ENTERING.getName(), String.valueOf(locationBasedReminder.getTriggerEntering()));
        values.put(RemindyContract.LocationBasedReminderTable.COLUMN_NAME_TRIGGER_EXITING.getName(), String.valueOf(locationBasedReminder.getTriggerExiting()));
        return values;
    }


    private ContentValues getValuesFromPlace(Place place) {
        ContentValues values = new ContentValues();
        values.put(RemindyContract.PlaceTable.COLUMN_NAME_ALIAS.getName(), place.getAlias());
        values.put(RemindyContract.PlaceTable.COLUMN_NAME_ADDRESS.getName(), place.getAddress());
        values.put(RemindyContract.PlaceTable.COLUMN_NAME_LATITUDE.getName(), place.getLatitude());
        values.put(RemindyContract.PlaceTable.COLUMN_NAME_LONGITUDE.getName(), place.getLongitude());
        values.put(RemindyContract.PlaceTable.COLUMN_NAME_RADIUS.getName(), place.getRadius());
        values.put(RemindyContract.PlaceTable.COLUMN_NAME_IS_ONE_OFF.getName(), place.isOneOff());
        return values;
    }


    private ContentValues getValuesFromAttachment(Attachment attachment) {
        ContentValues values = new ContentValues();
        values.put(RemindyContract.AttachmentTable.COLUMN_NAME_TASK_FK.getName(), attachment.getTaskId());
        values.put(RemindyContract.AttachmentTable.COLUMN_NAME_TYPE.getName(), attachment.getType().name());

        switch (attachment.getType()) {
            case AUDIO:
                values.put(RemindyContract.AttachmentTable.COLUMN_NAME_CONTENT_TEXT.getName(), ((AudioAttachment) attachment).getAudioFilename());
                break;
            case IMAGE:
                values.put(RemindyContract.AttachmentTable.COLUMN_NAME_CONTENT_BLOB.getName(), ((ImageAttachment) attachment).getThumbnail());
                values.put(RemindyContract.AttachmentTable.COLUMN_NAME_CONTENT_TEXT.getName(), ((ImageAttachment) attachment).getImageFilename());
                break;
            case TEXT:
                values.put(RemindyContract.AttachmentTable.COLUMN_NAME_CONTENT_TEXT.getName(), ((TextAttachment) attachment).getText());
                break;
            case LIST:
                values.put(RemindyContract.AttachmentTable.COLUMN_NAME_CONTENT_TEXT.getName(), ((ListAttachment) attachment).getItemsJson());
                break;
            case LINK:
                values.put(RemindyContract.AttachmentTable.COLUMN_NAME_CONTENT_TEXT.getName(), ((LinkAttachment) attachment).getLink());
                break;
            default:
                throw new InvalidParameterException("AttachmentType is invalid. Value = " + attachment.getType());
        }
        return values;
    }
















    /* Cursor to Model */

    private Task getTaskFromCursor(Cursor cursor) {
        int id = cursor.getInt(cursor.getColumnIndex(RemindyContract.TaskTable._ID));
        TaskStatus status = TaskStatus.valueOf(cursor.getString(cursor.getColumnIndex(RemindyContract.TaskTable.COLUMN_NAME_STATUS.getName())));
        String title = cursor.getString(cursor.getColumnIndex(RemindyContract.TaskTable.COLUMN_NAME_TITLE.getName()));
        String description = cursor.getString(cursor.getColumnIndex(RemindyContract.TaskTable.COLUMN_NAME_DESCRIPTION.getName()));
        TaskCategory category = TaskCategory.valueOf(cursor.getString(cursor.getColumnIndex(RemindyContract.TaskTable.COLUMN_NAME_CATEGORY.getName())));


        ReminderType reminderType;
        try {
            reminderType = ReminderType.valueOf(cursor.getString(cursor.getColumnIndex(RemindyContract.TaskTable.COLUMN_NAME_REMINDER_TYPE.getName())));
        } catch (IllegalArgumentException e) { //Thrown if task has no reminder
            reminderType = null;
        }

        Calendar doneDate = null;
        if (status == TaskStatus.DONE) {
            long doneDateLong = cursor.getLong(cursor.getColumnIndex(RemindyContract.TaskTable.COLUMN_NAME_DONE_DATE.getName()));
            if (doneDateLong != -1) {
                doneDate = Calendar.getInstance();
                doneDate.setTimeInMillis(doneDateLong);
            }
        }

        return new Task(id, status, title, description, category, reminderType, null, doneDate);
    }


    private OneTimeReminder getOneTimeReminderFromCursor(Cursor cursor) {
        int id = cursor.getInt(cursor.getColumnIndex(RemindyContract.OneTimeReminderTable._ID));
        int taskId = cursor.getInt(cursor.getColumnIndex(RemindyContract.OneTimeReminderTable.COLUMN_NAME_TASK_FK.getName()));

        Calendar date = Calendar.getInstance();
        date.setTimeInMillis(cursor.getLong(cursor.getColumnIndex(RemindyContract.OneTimeReminderTable.COLUMN_NAME_DATE.getName())));

        Time time = new Time(cursor.getInt(cursor.getColumnIndex(RemindyContract.OneTimeReminderTable.COLUMN_NAME_TIME.getName())));
        time.setDisplayTimeFormat(SharedPreferenceUtil.getTimeFormat(mContext));