package com.kunzisoft.remembirthday.provider;

import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.util.Log;

import com.kunzisoft.remembirthday.element.CalendarEvent;
import com.kunzisoft.remembirthday.element.Contact;
import com.kunzisoft.remembirthday.element.DateUnknownYear;
import com.kunzisoft.remembirthday.element.EventWithoutYear;
import com.kunzisoft.remembirthday.element.Reminder;
import com.kunzisoft.remembirthday.utility.QueryTool;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Created by joker on 08/08/17.
 */

public class EventLoader {

    private final static String TAG = "EventLoader";

    /**
     * Return new event from contact or null if not found
     * @param context Context to call
     * @param contact Contact associated with event
     * @return Next event in the year or null if not fund
     */
    private synchronized static CalendarEvent getNextEventFromContact(Context context, Contact contact) throws EventException {
        Long[] eventTimes = new Long[1];
        eventTimes[0] = new DateTime(contact.getNextBirthday())
                .withHourOfDay(0)
                .withMinuteOfHour(0)
                .withSecondOfMinute(0)
                .withMillisOfSecond(0)
                .withZoneRetainFields(DateTimeZone.UTC)
                .toDateTime().toDate().getTime();
        List<CalendarEvent> calendarEvents = getEventsFromContact(context, contact, eventTimes);
        if(calendarEvents.isEmpty())
            throw new EventException("Unable to getAutoSmsById next event from contact : " + contact.toString());
        else {
            CalendarEvent event = calendarEvents.get(0);
            Log.d(TAG, "Get next event " + event + " from contact " + contact);
            return event;
        }
    }

    /**
     * Return each event from contact
     * @param context Context to call
     * @param contact Contact associated with events
     * @param years List of event's years
     * @return Events for each year
     */
    private synchronized static List<CalendarEvent> getEventsFromContactWithYears(Context context, Contact contact, List<Integer> years) {
        Long[] eventTimes = new Long[years.size()];
        for(int i = 0; i < years.size(); i++) {
            int year = years.get(i);
            long eventTime = new DateTime(contact.getBirthday().getDateWithYear(year))
                    .withHourOfDay(0)
                    .withMinuteOfHour(0)
                    .withSecondOfMinute(0)
                    .withMillisOfSecond(0)
                    .withZoneRetainFields(DateTimeZone.UTC)
                    .toDateTime().toDate().getTime();
            eventTimes[i] = eventTime;
        }
        List<CalendarEvent> events = getEventsFromContact(context, contact, eventTimes);
        Log.d(TAG, "Get events (" + events.size() + ") from contact " + contact + " with year " + years);
        return events;
    }

    /**
     * Utility class for getAutoSmsById all events from contact with list of StartTime
     * @param context Context to call
     * @param contact Contact link
     * @param eventTimes List of events' StartTime
     * @return List of events
     */
    private synchronized static List<CalendarEvent> getEventsFromContact(Context context, Contact contact, Long[] eventTimes) {
        /* Two ways
            - Get events days of anniversary and filter with name (use for the first time)
            - Create links Event-Contact in custom table (may have bugs if event remove manually from calendar)
        */
        List<CalendarEvent> calendarEvents = new ArrayList<>();

        if(contact.hasBirthday()) {
            String[] projection = new String[] {
                    CalendarContract.Events._ID,
                    CalendarContract.Events.TITLE,
                    CalendarContract.Events.DESCRIPTION,
                    CalendarContract.Events.DTSTART,
                    CalendarContract.Events.DTEND,
                    CalendarContract.Events.EVENT_TIMEZONE,
                    CalendarContract.Events.ALL_DAY};
            /*
             * Get newt event who have an all day in the day of the event with name of contact in title
             */
            String where = CalendarContract.Events.DTSTART + " IN " + String.valueOf(QueryTool.getString(eventTimes)) +
                    " AND " + CalendarContract.Events.TITLE + " LIKE ?";
            String[] whereParam = {"%" + contact.getName() + "%"};
            // TODO better retrieve

            ContentResolver contentResolver = context.getContentResolver();
            Cursor cursor = contentResolver.query(
                    CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Events.CONTENT_URI),
                    projection,
                    where,
                    whereParam,
                    null);
            if(cursor != null) {
                cursor.moveToFirst();
                while (!cursor.isAfterLast()) {
                    CalendarEvent calendarEvent;
                    long id = cursor.getLong(cursor.getColumnIndex(CalendarContract.Events._ID));
                    String title = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.TITLE));
                    String description = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DESCRIPTION));
                    boolean allDay = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.ALL_DAY)) > 0;
                    if(allDay) {
                        Date dateStart = new DateTime(
                                cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTSTART)),
                                DateTimeZone.forID(cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE))))
                                .withZone(DateTimeZone.getDefault())
                                .toDate();
                        calendarEvent = new CalendarEvent(title, dateStart, true);
                    } else {
                        Date dateStart = new DateTime(
                                cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTSTART)),
                                DateTimeZone.forID(cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE))))
                                .withZone(DateTimeZone.getDefault())
                                .toDate();
                        Date dateEnd = new DateTime(
                                cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTEND)),
                                DateTimeZone.forID(cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE))))
                                .withZone(DateTimeZone.getDefault())
                                .toDate();
                        calendarEvent = new CalendarEvent(title, dateStart, dateEnd);
                    }
                    calendarEvent.setDescription(description);
                    calendarEvent.setId(id);
                    calendarEvents.add(calendarEvent);
                    cursor.moveToNext();
                }
                cursor.close();
            }
        }
        return calendarEvents;
    }

    /**
     * Get the next event or create a new event if not exists
     * @param context Context to call
     * @param contact Contact link
     * @return The next event
     * @throws EventException If event can't be getAutoSmsById after creation
     */
    public synchronized static CalendarEvent getNextEventOrCreateNewFromContact(Context context, Contact contact) throws EventException {
        try {
            return getNextEventFromContact(context, contact);
        } catch (EventException e) {
            // If next event do not exists, create all events missing (end of 5 years)
            // TODO Replace by saveEventIfNotExistsFromContactWithBirthday
            saveEventsIfNotExistsFromAllContactWithBirthday(context);
            return getNextEventFromContact(context, contact);
        }
    }

    public synchronized static List<CalendarEvent> getEventsSavedOrCreateNewsForEachYearAfterNextEvent(Context context, Contact contact) throws EventException {
        Log.d(TAG, "Retrieve events saved for each year after next event or create them");
        List<CalendarEvent> eventsSaved = new ArrayList<>();
        CalendarEvent eventToUpdate = getNextEventOrCreateNewFromContact(context, contact);
        // Update events for each year
        EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToUpdate);
        List<CalendarEvent> eventsAfterNeeded = eventWithoutYear.getEventsAfterThisYear();
        List<CalendarEvent> eventsAfterSaved = getEventsFromContactWithYears(
                context, contact, eventWithoutYear.getListOfYearsForEventsAfterThisYear());

        for (CalendarEvent event : eventsAfterNeeded) {
            if (eventsAfterSaved.contains(event)) {
                // For getAutoSmsById id
                event = eventsAfterSaved.get(eventsAfterSaved.indexOf(event));
                eventsSaved.add(event);
            }
        }
        return eventsSaved;
    }

    public synchronized static void updateEvent(Context context, Contact contact, DateUnknownYear newBirthday) throws EventException {
        // TODO UNIFORMISE
        for (CalendarEvent event : getEventsSavedOrCreateNewsForEachYear(context, contact)) {
            // Construct each anniversary of new birthday
            int year = new DateTime(event.getDate()).getYear();
            Date newBirthdayDate = DateUnknownYear.getDateWithYear(newBirthday.getDate(), year);
            event.setDateStart(newBirthdayDate);
            event.setAllDay(true);
            ArrayList<ContentProviderOperation> operations = new ArrayList<>();
            ContentProviderOperation contentProviderOperation = EventProvider.update(event);
            operations.add(contentProviderOperation);
            try {
                ContentProviderResult[] contentProviderResults =
                        context.getContentResolver().applyBatch(CalendarContract.AUTHORITY, operations);
                for(ContentProviderResult contentProviderResult : contentProviderResults) {
                    if (contentProviderResult.count != 0)
                        Log.d(TAG, "Update event : " + event.toString());
                }
            } catch (RemoteException|OperationApplicationException e) {
                Log.e(TAG, "Unable to update event : " + e.getMessage());
            }
        }
    }

    private synchronized static List<CalendarEvent> getEventsSavedForEachYear(Context context, Contact contact) throws EventException {
        Log.d(TAG, "Retrieve events saved for each year");
        List<CalendarEvent> eventsSaved = new ArrayList<>();

        CalendarEvent eventToUpdate = getNextEventFromContact(context, contact);

        // Update events for each year
        EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToUpdate);
        List<CalendarEvent> eventsAroundNeeded = eventWithoutYear.getEventsAroundAndForThisYear();
        List<CalendarEvent> eventsAroundSaved = getEventsFromContactWithYears(
                context, contact, eventWithoutYear.getListOfYearsForEachEvent());

        for (CalendarEvent event : eventsAroundNeeded) {
            if (eventsAroundSaved.contains(event)) {
                // For getAutoSmsById id
                event = eventsAroundSaved.get(eventsAroundSaved.indexOf(event));
                eventsSaved.add(event);
            }
        }
        return eventsSaved;
    }

    private synchronized static List<CalendarEvent> getEventsSavedOrCreateNewsForEachYear(Context context, Contact contact) throws EventException {
        Log.d(TAG, "Retrieve events saved for each year");
        List<CalendarEvent> eventsSaved = new ArrayList<>();

        CalendarEvent eventToUpdate = getNextEventFromContact(context, contact);

        // Update events for each year
        EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToUpdate);
        List<CalendarEvent> eventsAroundNeeded = eventWithoutYear.getEventsAroundAndForThisYear();
        List<CalendarEvent> eventsAroundSaved = getEventsFromContactWithYears(
                context, contact, eventWithoutYear.getListOfYearsForEachEvent());

        for (CalendarEvent event : eventsAroundNeeded) {
            if (eventsAroundSaved.contains(event)) {
                // For getAutoSmsById id
                event = eventsAroundSaved.get(eventsAroundSaved.indexOf(event));
                eventsSaved.add(event);
            } else {
                // TODO Replace by saveEventIfNotExistsFromContactWithBirthday
                saveEventsIfNotExistsFromAllContactWithBirthday(context);
                // TODO getAutoSmsById all news
            }
        }
        return eventsSaved;
    }

    public synchronized static void deleteEventsFromContact(Context context, Contact contact) {
        ArrayList<ContentProviderOperation> operations = new ArrayList<>();
        try {
            for (CalendarEvent event : getEventsSavedForEachYear(context, contact)) {
                operations.add(ReminderProvider.deleteAll(context, event.getId()));
                operations.add(EventProvider.delete(event));
            }
            ContentProviderResult[] contentProviderResults =
                    context.getContentResolver().applyBatch(CalendarContract.AUTHORITY, operations);
            for(ContentProviderResult contentProviderResult : contentProviderResults) {
                Log.d(TAG, contentProviderResult.toString());
                if (contentProviderResult.uri != null)
                    Log.d(TAG, contentProviderResult.uri.toString());
            }
        } catch (RemoteException |OperationApplicationException |EventException e) {
            Log.e(TAG, "Unable to deleteById events : " + e.getMessage());
        }
    }

    public synchronized static void saveEventIfNotExistsFromContactWithBirthday(Context context, Contact contact) {
        //TODO
    }

    /**
     * Save all events and default reminders from contacts with birthday
     * @param context Context to call
     */
    public synchronized static void saveEventsIfNotExistsFromAllContactWithBirthday(Context context) {
        ContentResolver contentResolver = context.getContentResolver();

        if (contentResolver == null) {
            Log.e(TAG, "Unable to getAutoSmsById content resolver!");
            return;
        }

        long calendarId = CalendarLoader.getCalendar(context);
        if (calendarId == -1) {
            Log.e(TAG, "Unable to create calendar");
            return;
        }

        // Sync flow:
        // 1. Clear events table for this account completely
        //CalendarLoader.cleanTables(context, calendarId);
        // 2. Get birthdays from contacts
        // 3. Create events and reminders for each birthday

        //List<ContactEventOperation> contactEventOperationList = new ArrayList<>();
        ArrayList<ContentProviderOperation> allOperationList = new ArrayList<>();

        // iterate through all Contact
        List<Contact> contactList = ContactLoader.getAllContacts(context);

        int backRef = 0;
        for (Contact contact : contactList) {

            // TODO Ids
            Log.d(TAG, "BackRef is " + backRef);

            // If next event in calendar is empty, add new event
            CalendarEvent eventToAdd = CalendarEvent.buildDefaultEventFromContactToSave(context, contact);

            // TODO ENCAPSULATE
            EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToAdd);
            List<CalendarEvent> eventsAroundNeeded = eventWithoutYear.getEventsAroundAndForThisYear();
            List<CalendarEvent> eventsAroundSaved = getEventsFromContactWithYears(
                    context, contact, eventWithoutYear.getListOfYearsForEachEvent());

            for (CalendarEvent event : eventsAroundNeeded) {
                if (!eventsAroundSaved.contains(event)) {

                    // Add event operation in list of contact manager
                    allOperationList.add(EventProvider.insert(context, calendarId, event, contact));

                    int noOfReminderOperations = 0;
                    for (Reminder reminder : eventToAdd.getReminders()) {
                        allOperationList.add(ReminderProvider.insert(context, reminder, backRef));
                        noOfReminderOperations += 1;
                    }
                    // back references for the next reminders, 1 is for the event
                    backRef += 1 + noOfReminderOperations;

                }
            }
        }

        /* Create events with reminders and linkEventContract
         * intermediate commit - otherwise the binder transaction fails on large
         * operationList
         * TODO for large list > 200, make multiple apply
         */
        try {
            Log.d(TAG, "Start applying the batch...");

            /*
             * Apply all Reminder Operations
             */
            ContentProviderResult[] contentProviderResults =
                    contentResolver.applyBatch(CalendarContract.AUTHORITY, allOperationList);
            for(ContentProviderResult contentProviderResult : contentProviderResults) {
                Log.d(TAG, "ReminderOperation apply : " + contentProviderResult.toString());
            }

            Log.d(TAG, "Applying the batch was successful!");
        } catch (RemoteException |OperationApplicationException e) {
            Log.e(TAG, "Applying batch error!", e);
        }
    }

    /**
     * Event exception class
     */
    public static class EventException extends Exception {
        public EventException(String message) {
            super(message);
        }
    }
}