package com.jens.automation2.receivers;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.CalendarContract;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;

import com.jens.automation2.AutomationService;
import com.jens.automation2.Miscellaneous;
import com.jens.automation2.Rule;
import com.jens.automation2.Trigger;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

public class CalendarReceiver extends BroadcastReceiver implements AutomationListenerInterface
{
    static CalendarReceiver calendarReceiverInstance = null;
    static boolean calendarReceiverActive = false;
    static IntentFilter calendarIntentFilter = null;
    private static Intent calendarIntent = null;

    public static final int AVAILABILITY_OUT_OF_OFFICE = 4;
    public static final int AVAILABILITY_WORKING_ELSEWHERE = 5;
    public static final String calendarAlarmAction = "ALARM_FOR_CALENDAR";

    static List<AndroidCalendar> calendarsCache = null;
    static List<CalendarEvent> calendarEventsCache = null;
    static List<CalendarEvent> calendarEventsReoccurringCache = null;

    // To determine for which events which rules have been executed
    static List<RuleEventPair> calendarEventsUsed = new ArrayList<>();

    static Timer timer = null;
    static TimerTask timerTask = null;
    static Calendar nextWakeup = null;
    static AlarmManager alarmManager = null;
    static boolean wakeupNeedsToBeScheduledOrRescheduled = false;

    public static CalendarEvent getLastTriggeringEvent()
    {
        if(calendarEventsUsed.size() > 0)
        {
            return calendarEventsUsed.get(calendarEventsUsed.size() -1).event;
        }

        return null;
    }

    public static class RuleEventPair
    {
        Rule rule;
        CalendarEvent event;

        public RuleEventPair(Rule rule, CalendarEvent event)
        {
            this.rule = rule;
            this.event = event;
        }
    }

    public static void addUsedPair(RuleEventPair pair)
    {
        // Add pair only if it's not in the list already.
        for(RuleEventPair usedPair : calendarEventsUsed)
        {
            if(usedPair.rule.equals(pair.rule) && usedPair.event.equals(pair.event))
                return;
        }

        calendarEventsUsed.add(pair);
    }

    public static CalendarReceiver getInstance()
    {
        if(calendarReceiverInstance == null)
            calendarReceiverInstance = new CalendarReceiver();

        return calendarReceiverInstance;
    }

    @Override
    public void onReceive(Context context, Intent intent)
    {
        Miscellaneous.logEvent("i", "CalendarReceiver", "Received " + intent.getAction(), 4);

        if(intent.getAction().equalsIgnoreCase(Intent.ACTION_PROVIDER_CHANGED))
        {
            Miscellaneous.logEvent("i", "CalendarReceiver", "Clearing calendar caches.", 4);

            clearCaches();

            routineAtAlarm();
        }
        else if(intent.getAction().equalsIgnoreCase(calendarAlarmAction))
        {
            routineAtAlarm();
        }
    }

    static void checkForRules(Context context)
    {
        ArrayList<Rule> ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent);
        for (int i = 0; i < ruleCandidates.size(); i++)
        {
            if (ruleCandidates.get(i).getsGreenLight(context))
                ruleCandidates.get(i).activate(AutomationService.getInstance(), false);
        }
    }

    @Override
    public void startListener(AutomationService automationServiceRef)
    {
        startCalendarReceiver(automationServiceRef);
    }

    static void clearCaches()
    {
        calendarsCache = null;
        calendarEventsCache = null;
        calendarEventsReoccurringCache = null;
    }

    @Override
    public void stopListener(AutomationService automationService)
    {
        if(calendarReceiverActive)
        {
            if(calendarReceiverInstance != null)
            {
                AutomationService.getInstance().unregisterReceiver(calendarReceiverInstance);
                calendarReceiverInstance = null;
            }

            clearCaches();
            calendarEventsUsed.clear();

            calendarReceiverActive = false;
        }
    }

    @Override
    public boolean isListenerRunning()
    {
        return calendarReceiverActive;
    }

    @Override
    public Trigger.Trigger_Enum[] getMonitoredTrigger()
    {
        return new Trigger.Trigger_Enum[]{Trigger.Trigger_Enum.calendarEvent};
    }

    public static class AndroidCalendar
    {
        public int calendarId;
        public String displayName;
        public String accountString;

        @NonNull
        @Override
        public String toString()
        {
            return displayName + " (" + accountString + ")";
        }
    }

    public static class CalendarEvent
    {
        public AndroidCalendar calendar;
        public int calendarId;
        public String eventId;
        public String title;
        public String description;
        public String location;
        public String availability;
        public Calendar start, end;
        public boolean allDay, reoccurring;

        public boolean isCurrentlyActive()
        {
            Calendar now = Calendar.getInstance();
            return now.getTimeInMillis() >= start.getTimeInMillis() && now.getTimeInMillis() < end.getTimeInMillis();
        }

        @NonNull
        @Override
        public String toString()
        {
            return "Title: " + title + ", location: " + location + ", description: " + description + ", start: " + Miscellaneous.formatDate(start.getTime()) + ", end: " + Miscellaneous.formatDate(end.getTime()) + ", is currently active: " + String.valueOf(isCurrentlyActive()) + ", all day: " + String.valueOf(allDay) + ", availability: " + availability;
        }

        @Override
        public boolean equals(@Nullable Object obj)
        {
            try
            {
                CalendarEvent compareEvent = (CalendarEvent) obj;
                return calendarId == compareEvent.calendarId
                        &&
                        eventId.equals(compareEvent.eventId)
                        &&
                        title.equals(compareEvent.title)
                        &&
                        description.equals(compareEvent.description)
                        &&
                        location.equals(compareEvent.location)
                        &&
                        availability.equals(compareEvent.availability)
                        &&
                        start.getTimeInMillis() == compareEvent.start.getTimeInMillis()
                        &&
                        end.getTimeInMillis() == compareEvent.end.getTimeInMillis()
                        &&
                        allDay == compareEvent.allDay;
            }
            catch (Exception e)
            {
                Miscellaneous.logEvent("e", "CalendarReceiver compare()", Log.getStackTraceString(e), 5);
                return false;
            }
        }
    }

    public static List<AndroidCalendar> readCalendars(Context context)
    {
        if(calendarsCache == null)
        {
            calendarsCache = new ArrayList<>();

            Cursor cursor;

            cursor = context.getContentResolver().query(
                    Uri.parse("content://com.android.calendar/calendars"),

                    new String[]{ CalendarContract.Calendars._ID, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, CalendarContract.Calendars.OWNER_ACCOUNT,  },
                    null, null, null);

            cursor.moveToFirst();
            // fetching calendars name
            String CNames[] = new String[cursor.getCount()];

            for (int i = 0; i < CNames.length; i++)
            {
                try
                {
                    AndroidCalendar calendar = new AndroidCalendar();
                    calendar.calendarId = Integer.parseInt(cursor.getString(0));
                    calendar.displayName = cursor.getString(1);
                    calendar.accountString = cursor.getString(2);

                    calendarsCache.add(calendar);
                }
                catch (Exception e)
                {
                }
                cursor.moveToNext();
            }

            if (cursor != null)
                cursor.close();
        }

        return calendarsCache;
    }

    public static List<CalendarEvent> readCalendarEvents(Context context, boolean includeReoccurring, boolean includePastEvents)
    {
        if(calendarEventsCache == null)
        {
            calendarEventsCache = new ArrayList<>();

            Cursor cursor;

                cursor = context.getContentResolver().query(
                            Uri.parse("content://com.android.calendar/events"),
                            new String[] {
                                            CalendarContract.Events.CALENDAR_ID,
                                            CalendarContract.Events._ID,
                                            CalendarContract.Events.TITLE,
                                            CalendarContract.Events.DESCRIPTION,
                                            CalendarContract.Events.ALL_DAY,
                                            CalendarContract.Events.DTSTART,
                                            CalendarContract.Events.DTEND,
                                            CalendarContract.Events.EVENT_LOCATION,
                                            CalendarContract.Events.AVAILABILITY
                                        },
                            null, null, null);

            cursor.moveToFirst();
            // fetching calendars name
            String CNames[] = new String[cursor.getCount()];

            Calendar now = Calendar.getInstance();

            for (int i = 0; i < CNames.length; i++)
            {
                try
                {
                    CalendarEvent event = new CalendarEvent();
                    event.calendarId = Integer.parseInt(cursor.getString(0));

                    for(AndroidCalendar cal : readCalendars(context))
                    {
                        if(cal.calendarId == event.calendarId)
                        {
                            event.calendar = cal;
                            break;
                        }
                    }

                    event.eventId = cursor.getString(1);
                    event.title = cursor.getString(2);
                    event.description = cursor.getString(3);
                    event.allDay = cursor.getString(4).equals("1");
                    event.start = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(5)));
                    event.end = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(6)));
                    event.location = cursor.getString(7);
                    event.availability = cursor.getString(8);
                    event.reoccurring = false;

                    if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis())
                        calendarEventsCache.add(event);
                }
                catch (Exception e)
                {}
                cursor.moveToNext();
            }

            if(cursor != null)
                cursor.close();

        }

        if(includeReoccurring && calendarEventsReoccurringCache == null)
        {
            calendarEventsReoccurringCache = new ArrayList<>();

            Cursor cursor;

            Calendar queryStart, queryEnd;
            if(includePastEvents)
                queryStart = Miscellaneous.calendarFromLong(0);
            else
                queryStart = Calendar.getInstance();

            queryEnd = Calendar.getInstance();
            queryEnd.add(Calendar.YEAR, 1);

            cursor = context.getContentResolver().query(
                    Uri.parse("content://com.android.calendar/instances/when/" + queryStart.getTimeInMillis() + "/" + queryEnd.getTimeInMillis()),
                    new String[] {
                            CalendarContract.Instances.CALENDAR_ID,
                            CalendarContract.Instances._ID,
                            CalendarContract.Instances.TITLE,
                            CalendarContract.Instances.DESCRIPTION,
                            CalendarContract.Instances.ALL_DAY,
                            CalendarContract.Instances.BEGIN,
                            CalendarContract.Instances.END,
                            CalendarContract.Instances.EVENT_LOCATION,
                            CalendarContract.Instances.AVAILABILITY
                    },
                    null, null, null);

            cursor.moveToFirst();
            String CNames[] = new String[cursor.getCount()];

            Calendar now = Calendar.getInstance();

            for (int i = 0; i < CNames.length; i++)
            {
                try
                {
                    CalendarEvent event = new CalendarEvent();
                    event.calendarId = Integer.parseInt(cursor.getString(0));

                    for(AndroidCalendar cal : readCalendars(context))
                    {
                        if(cal.calendarId == event.calendarId)
                        {
                            event.calendar = cal;
                            break;
                        }
                    }

                    event.eventId = cursor.getString(1);
                    event.title = cursor.getString(2);
                    event.description = cursor.getString(3);
                    event.allDay = cursor.getString(4).equals("1");
                    event.start = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(5)));
                    event.end = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(6)));
                    event.location = cursor.getString(7);
                    event.availability = cursor.getString(8);
                    event.reoccurring = true;

                    if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis())
                    {
                        // For the moment keeping separate records to some extent
                        calendarEventsReoccurringCache.add(event);
                        calendarEventsCache.add(event);
                    }
                }
                catch (Exception e)
                {}
                cursor.moveToNext();
            }

            if(cursor != null)
                cursor.close();

        }

        return calendarEventsCache;
    }

    protected static void routineAtAlarm()
    {
        checkForRules(Miscellaneous.getAnyContext());

        // Set next timer
        calculateNextWakeup();
        armOrRearmTimer();
    }

    public static void armOrRearmTimer()
    {
        PendingIntent pi = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
        {
            if (alarmManager == null)
            {
                alarmManager = (AlarmManager) Miscellaneous.getAnyContext().getSystemService(Context.ALARM_SERVICE);
            }

            if(pi == null)
            {
                Intent intent = new Intent(Miscellaneous.getAnyContext(), CalendarReceiver.class);
                intent.setAction(calendarAlarmAction);
                pi = PendingIntent.getBroadcast(AutomationService.getInstance(), 0, intent, 0);
            }
        }
        else
        {
            timerTask = new TimerTask()
            {
                @Override
                public void run()
                {
                    routineAtAlarm();
                }
            };

            if(timer != null)
            {
                timer.cancel();
                timer.purge();
            }
            timer = new Timer();
        }

        if(nextWakeup == null)
        {
            readCalendarEvents(Miscellaneous.getAnyContext(), true,false);
            calculateNextWakeup();
        }

        // If it's now filled, go on
        if(nextWakeup != null && wakeupNeedsToBeScheduledOrRescheduled)
        {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
            {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && alarmManager.canScheduleExactAlarms()))
                {
                    try
                    {
                        alarmManager.cancel(pi);
                    }
                    catch (Exception e)
                    {

                    }
                    Miscellaneous.logEvent("i", "armOrRearmTimer()", "Scheduling wakeup for calendar at " + Miscellaneous.formatDate(nextWakeup.getTime()), 4);
                    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextWakeup.getTimeInMillis(), pi);
                    wakeupNeedsToBeScheduledOrRescheduled = false;
                }
            }
            else
                timer.schedule(timerTask, nextWakeup.getTimeInMillis());
        }
    }

    private static void calculateNextWakeup()
    {
        Calendar now = Calendar.getInstance();
        List<CalendarEvent> events = readCalendarEvents(Miscellaneous.getAnyContext(), true, false);

        if(events.size() == 0)
        {
            Miscellaneous.logEvent("i", "calculateNextWakeup()", "Supposedly no future events in calendar. Reading again...", 4);
            events = readCalendarEvents(Miscellaneous.getAnyContext(), true, false);
        }

        if (events.size() == 0)
        {
            Miscellaneous.logEvent("i", "calculateNextWakeup()", "No future events, nothing to schedule.", 4);
        }
        else
        {
            Miscellaneous.logEvent("i", "calculateNextWakeup()", String.valueOf(events.size()) + " future events in calendar. Checking if one applies.", 4);

            List<Rule> ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent);
            List<Long> wakeUpCandidatesList = new ArrayList<>();

            for (CalendarEvent event : events)
            {
                for (Rule r : ruleCandidates)
                {
                    for (Trigger t : r.getTriggerSet())
                    {
                        if (t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent) && t.checkCalendarEvent(event, true))
                        {
                            /*
                                Device needs to wakeup at start AND end of events, no matter what is specified in triggers.
                                This is because we also need to know when a trigger doesn't apply anymore to make it
                                count for hasStateNotAppliedSinceLastRuleExecution().
                                Otherwise the same rule would not get executed again even after calendar events have come and gone.
                             */

                            if(event.start.getTimeInMillis() > now.getTimeInMillis())
                                wakeUpCandidatesList.add(event.start.getTimeInMillis());

                            if(event.end.getTimeInMillis() > now.getTimeInMillis())
                                wakeUpCandidatesList.add(event.end.getTimeInMillis());
                        }
                    }
                }
            }

            Collections.sort(wakeUpCandidatesList);

            if(wakeUpCandidatesList.size() == 0)
                Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Not scheduling any calendar related wakeup as there are no future events that might match a configured trigger.", 4);
            else
            {
                if (nextWakeup == null || nextWakeup.getTimeInMillis() != wakeUpCandidatesList.get(0))
                {
                    Calendar newAlarm = Miscellaneous.calendarFromLong(wakeUpCandidatesList.get(0));
                    if (nextWakeup == null)
                        Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was null.", 4);
                    else
                        Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was " + Miscellaneous.formatDate(nextWakeup.getTime()), 4);
                    nextWakeup = newAlarm;
                    if (!wakeupNeedsToBeScheduledOrRescheduled)
                        wakeupNeedsToBeScheduledOrRescheduled = true;
                }
                else
                    Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Alarm " + Miscellaneous.formatDate(nextWakeup.getTime()) + " has been selected as next wakeup, but not rescheduling since this was not a change.", 4);
            }
        }
    }

    public static void startCalendarReceiver(final AutomationService automationServiceRef)
    {
        if (!calendarReceiverActive)
        {
            if (calendarReceiverInstance == null)
                calendarReceiverInstance = new CalendarReceiver();

            if (calendarIntentFilter == null)
            {
                calendarIntentFilter = new IntentFilter();
                calendarIntentFilter.addAction(Intent.ACTION_PROVIDER_CHANGED);
                calendarIntentFilter.addDataScheme("content");
                calendarIntentFilter.addDataAuthority("com.android.calendar", null);
            }

            if(Build.VERSION.SDK_INT >= 33)
                calendarIntent = automationServiceRef.registerReceiver(calendarReceiverInstance, calendarIntentFilter, Context.RECEIVER_EXPORTED);
            else
                calendarIntent = automationServiceRef.registerReceiver(calendarReceiverInstance, calendarIntentFilter);

            calendarReceiverActive = true;

            armOrRearmTimer();
        }
    }

    public static boolean mayRuleStillBeActivatedForPendingCalendarEvents(Rule rule)
    {
        for(RuleEventPair pair : calendarEventsUsed)
            Miscellaneous.logEvent("i", "mayRuleStillBeActivatedForPendingCalendarEvents()", "Existing pair of " + pair.rule.getName() + " and " + pair.event, 5);

        for(CalendarEvent event : readCalendarEvents(Miscellaneous.getAnyContext(), true,false))
        {
            for(Trigger t : rule.getTriggerSet())
            {
                if(t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent) && t.checkCalendarEvent(event, false))
                {
                    if (!hasEventBeenUsedInRule(rule, event))
                    {
                        /*
                            If there are multiple parallel calendar events and a rule has multiple
                            triggers of type calendar event, we don't want the rule to fire only once.
                         */
                        if(rule.getAmountOfTriggersForType(Trigger.Trigger_Enum.calendarEvent) == 1)
                        {
                            Miscellaneous.logEvent("i", "mayRuleStillBeActivatedForPendingCalendarEvents()", "Rule " + rule.getName() + " has not been used in conjunction with event " + event, 4);
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    public static boolean hasEventBeenUsedInRule(Rule rule, CalendarEvent event)
    {
        for (RuleEventPair executedPair : calendarEventsUsed)
        {
            if (executedPair.rule.equals(rule) && executedPair.event.equals(event))
                return true;
        }

        return false;
    }

    public static List<CalendarReceiver.CalendarEvent> getApplyingCalendarEvents(Rule rule)
    {
        List<CalendarReceiver.CalendarEvent> returnList = new ArrayList<>();

        try
        {
            List<CalendarReceiver.CalendarEvent> calendarEvents = CalendarReceiver.readCalendarEvents(AutomationService.getInstance(), true,false);

            for(Trigger t : rule.getTriggerSet())
            {
                if(t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent))
                {
                    for (CalendarReceiver.CalendarEvent event : calendarEvents)
                    {
                        if (t.checkCalendarEvent(event, false))
                            returnList.add(event);
                    }
                }
            }
        }
        catch(Exception e)
        {
            Miscellaneous.logEvent("e", "getApplyingCalendarEvents()", Log.getStackTraceString(e), 1);
        }

        return returnList;
    }
}