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.os.Build;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.jens.automation2.AutomationService;
import com.jens.automation2.Miscellaneous;
import com.jens.automation2.Rule;
import com.jens.automation2.TimeFrame;
import com.jens.automation2.TimeObject;
import com.jens.automation2.Trigger;
import com.jens.automation2.Trigger.Trigger_Enum;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;

public class DateTimeListener extends BroadcastReceiver implements AutomationListenerInterface
{
	private static AutomationService automationServiceRef;
	private static AlarmManager centralAlarmManagerInstance;
	private static boolean alarmListenerActive=false;
	private static ArrayList<ScheduleElement> alarmCandidates = new ArrayList<>();
	private static ArrayList<Integer> requestCodeList = new ArrayList<Integer>();
	static PendingIntent alarmPendingIntent = null;

	public static void startAlarmListener(final AutomationService automationServiceRef)
	{
		DateTimeListener.startAlarmListenerInternal(automationServiceRef);
	}
	public static void stopAlarmListener(Context context)
	{
		DateTimeListener.stopAlarmListenerInternal();
	}

	public static boolean isAlarmListenerActive()
	{
		return alarmListenerActive;
	}

	@Override
	public void onReceive(Context context, Intent intent)
	{
		Miscellaneous.logEvent("i", "AlarmListener", "Alarm received", 2);

		ArrayList<Rule> allRulesWithTimeFrame = Rule.findRuleCandidates(Trigger_Enum.timeFrame);
		for(int i=0; i < allRulesWithTimeFrame.size(); i++)
		{
			if(allRulesWithTimeFrame.get(i).getsGreenLight(context))
				allRulesWithTimeFrame.get(i).activate(automationServiceRef, false);
		}

		setOrResetAlarms();
	}

	@RequiresApi(api = Build.VERSION_CODES.KITKAT)
	public static void setOrResetAlarms()
	{
		alarmCandidates.clear();

		Calendar calNow = Calendar.getInstance();
		SimpleDateFormat sdf = new SimpleDateFormat("E dd.MM.yyyy HH:mm");

		clearAlarms();

		int i=0;

		ArrayList<Rule> allRulesWithTimeFrames = new ArrayList<Rule>();
		allRulesWithTimeFrames = Rule.findRuleCandidates(Trigger_Enum.timeFrame);

		/*
		 * Take care of regular executions, no repetitions in between.
		 */
		Miscellaneous.logEvent("i", "DateTimeListener", "Checking rules for single run alarm candidates.", 5);
		for(Rule oneRule : allRulesWithTimeFrames)
		{
			Miscellaneous.logEvent("i", "DateTimeListener","Checking rule " + oneRule.getName() + " for single run alarm candidates.", 5);
			if(oneRule.isRuleActive())
			{
				try
				{
					for(Trigger oneTrigger : oneRule.getTriggerSet())
					{
						Miscellaneous.logEvent("i", "DateTimeListener","Checking trigger " + oneTrigger.toString() + " for single run alarm candidates.", 5);

						if(oneTrigger.getTriggerType().equals(Trigger_Enum.timeFrame))
						{
							TimeFrame tf = new TimeFrame(oneTrigger.getTriggerParameter2());

							Calendar calSet;
							TimeObject setTime;

							if(oneTrigger.getTriggerParameter())
								setTime = tf.getTriggerTimeStart();
							else
								setTime = tf.getTriggerTimeStop();

							calSet = (Calendar) calNow.clone();
							calSet.set(Calendar.HOUR_OF_DAY, setTime.getHours());
							calSet.set(Calendar.MINUTE, setTime.getMinutes());
							calSet.set(Calendar.SECOND, 0);
							calSet.set(Calendar.MILLISECOND, 0);
							// At this point calSet would be a scheduling candidate. It's just the day that might not be right, yet.

							for(int dayOfWeek : tf.getDayList())
							{
								Calendar calSetWorkingCopy = (Calendar) calSet.clone();

								int diff = dayOfWeek - calNow.get(Calendar.DAY_OF_WEEK);
								if(diff == 0) // We're talking about the current weekday, but is the time still in the future?
								{
									if(calSetWorkingCopy.getTime().getHours() < calNow.getTime().getHours())
									{
										calSetWorkingCopy.add(Calendar.DAY_OF_MONTH, 7); //add a week
									}
									else if(calSetWorkingCopy.getTime().getHours() == calNow.getTime().getHours())
									{
										if(calSetWorkingCopy.getTime().getMinutes() <= calNow.getTime().getMinutes())
										{
											calSetWorkingCopy.add(Calendar.DAY_OF_MONTH, 7); //add a week
										}
									}
								}
								else if(diff < 0)
								{
									calSetWorkingCopy.add(Calendar.DAY_OF_WEEK, diff+7);	// it's a past weekday, schedule for next week
								}
								else
								{
									calSetWorkingCopy.add(Calendar.DAY_OF_WEEK, diff);		// it's a future weekday, schedule for that day
								}

								i++;
								i = (int)System.currentTimeMillis();
								sdf.format(calSetWorkingCopy.getTime());
								String.valueOf(i);

								alarmCandidates.add(new ScheduleElement(calSetWorkingCopy, "Rule " + oneRule.getName() + ", trigger " + oneTrigger.toString()));
							}
						}
					}
				}
				catch(Exception e)
				{
					Miscellaneous.logEvent("e", "DateTimeListener","Error checking anything for rule " + oneRule.toString() + " needs to be added to candicates list: " + Log.getStackTraceString(e), 1);
				}
			}
		}

		/*
		 * Only take care of repeated executions.
		 */
		Miscellaneous.logEvent("i", "DateTimeListener","Checking rules for repeated run alarm candidates.", 5);
		for(Rule oneRule : allRulesWithTimeFrames)
		{
			Miscellaneous.logEvent("i", "DateTimeListener","Checking rule " + oneRule.getName() + " for repeated run alarm candidates.", 5);
			if(oneRule.isRuleActive())
			{
				try
				{
					Miscellaneous.logEvent("i", "DateTimeListener","Checking rule " + oneRule.toString() , 5);

					for(Trigger oneTrigger : oneRule.getTriggerSet())
					{
						Miscellaneous.logEvent("i", "DateTimeListener","Checking trigger " + oneTrigger.toString() + " for repeated run alarm candidates.", 5);
						if(oneTrigger.getTriggerType().equals(Trigger_Enum.timeFrame))
						{
							Miscellaneous.logEvent("i", "DateTimeListener","Checking rule trigger " + oneTrigger.toString() , 5);

							/*
							 * Check for next repeated execution:
							 *
							 * Check if the rule currently applies....
							 *
							 * If no -> do nothing
							 * If yes -> Take starting time and calculate the next repeated execution
							 * 	1. Take starting time
							 * 	2. Take current time
							 * 	3. Calculate difference, but include check to see if we're after that time,
							 * 		be it start or end of the timeframe.
							 * 	4. Take div result +1 and add this on top of starting time
							 * 	5. Is this next possible execution still inside timeframe? Also consider timeframes spanning over midnight
							 */

							TimeFrame tf = new TimeFrame(oneTrigger.getTriggerParameter2());

							if(tf.getRepetition() > 0)
							{
//								if(oneTrigger.applies(calNow, Miscellaneous.getAnyContext()))
//								{
									Calendar calSchedule = getNextRepeatedExecution(oneTrigger);

									alarmCandidates.add(new ScheduleElement(calSchedule, "Rule " + oneRule.getName() + ", trigger " + oneTrigger.toString()));
//								}
							}
						}
					}
				}
				catch(Exception e)
				{
					Miscellaneous.logEvent("e", "DateTimeListener","Error checking anything for rule " + oneRule.toString() + " needs to be added to candicates list: " + Log.getStackTraceString(e), 1);
				}
			}
		}

		scheduleNextAlarm();
	}

	private static void scheduleNextAlarm()
	{
		long currentTime = System.currentTimeMillis();
		ScheduleElement scheduleCandidate = null;

		if(alarmCandidates.size() == 0)
		{
			Miscellaneous.logEvent("i", "AlarmManager", "No alarms to be scheduled.", 3);
			return;
		}
		else if(alarmCandidates.size() == 1)
		{
			// only one alarm, schedule that
			scheduleCandidate = alarmCandidates.get(0);
		}
		else if(alarmCandidates.size() > 1)
		{
			scheduleCandidate = alarmCandidates.get(0);

			for(ScheduleElement alarmCandidate : alarmCandidates)
			{
				if(Math.abs(currentTime - alarmCandidate.time.getTimeInMillis()) < Math.abs(currentTime - scheduleCandidate.time.getTimeInMillis()))
					scheduleCandidate = alarmCandidate;
			}
		}

		Intent alarmIntent = new Intent(automationServiceRef, DateTimeListener.class);

		if(Miscellaneous.getAnyContext().getApplicationContext().getApplicationInfo().targetSdkVersion >= 31)
			alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
		else
			alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
			centralAlarmManagerInstance.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, scheduleCandidate.time.getTimeInMillis(), alarmPendingIntent);
		else
			centralAlarmManagerInstance.set(AlarmManager.RTC_WAKEUP, scheduleCandidate.time.getTimeInMillis(), alarmPendingIntent);

		SimpleDateFormat sdf = new SimpleDateFormat("E dd.MM.yyyy HH:mm:ss");
		Calendar calendar = Calendar.getInstance();
		calendar.setTimeInMillis(scheduleCandidate.time.getTimeInMillis());
		Miscellaneous.logEvent("i", "AlarmManager", "Chose " + sdf.format(calendar.getTime()) + " as next scheduled alarm.", 4);
	}

	public static void clearAlarms()
	{
		Miscellaneous.logEvent("i", "AlarmManager", "Clearing possibly standing alarms.", 4);

		for(int requestCode : requestCodeList)
		{
			Intent alarmIntent = new Intent(automationServiceRef, DateTimeListener.class);
			if(alarmPendingIntent == null)
				alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, requestCode, alarmIntent, 0);

			centralAlarmManagerInstance.cancel(alarmPendingIntent);
		}

		requestCodeList.clear();
	}

	private static void startAlarmListenerInternal(AutomationService givenAutomationServiceRef)
	{
		if(!alarmListenerActive)
		{
			Miscellaneous.logEvent("i", "AlarmListener", "Starting alarm listener.", 4);
			DateTimeListener.automationServiceRef = givenAutomationServiceRef;
			centralAlarmManagerInstance = (AlarmManager)automationServiceRef.getSystemService(automationServiceRef.ALARM_SERVICE);

			alarmListenerActive = true;
			Miscellaneous.logEvent("i", "AlarmListener", "Alarm listener started.", 4);
			DateTimeListener.setOrResetAlarms();

//			// get a Calendar object with current time
//			 Calendar cal = Calendar.getInstance();
//			 // add 5 minutes to the calendar object
//			 cal.add(Calendar.SECOND, 10);
//			 centralAlarmManagerInstance.setInexactRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 5000, alarmPendingIntent);
		}
		else
			Miscellaneous.logEvent("i", "AlarmListener", "Request to start AlarmListener. But it's already active.", 5);
	}

	private static void stopAlarmListenerInternal()
	{
		if(alarmListenerActive)
		{
			Miscellaneous.logEvent("i", "AlarmListener", "Stopping alarm listener.", 4);
			clearAlarms();
			centralAlarmManagerInstance.cancel(alarmPendingIntent);
			alarmListenerActive = false;
		}
		else
			Miscellaneous.logEvent("i", "AlarmListener", "Request to stop AlarmListener. But it's not running.", 5);
	}

	@Override
	public void startListener(AutomationService automationService)
	{
		DateTimeListener.startAlarmListener(automationService);
	}
	@Override
	public void stopListener(AutomationService automationService)
	{
		DateTimeListener.stopAlarmListener(automationService);
	}

	public static boolean haveAllPermission()
	{
		return true;
	}

	@Override
	public boolean isListenerRunning()
	{
		return isAlarmListenerActive();
	}
	@Override
	public Trigger_Enum[] getMonitoredTrigger()
	{
		return new Trigger_Enum[] { Trigger_Enum.timeFrame };
	}

	static class ScheduleElement implements Comparable<ScheduleElement>
	{
		Calendar time;
		String reason;

		public ScheduleElement(Calendar timestamp, String reason)
		{
			super();
			this.time = timestamp;
			this.reason = reason;
		}

		@Override
		public int compareTo(ScheduleElement o)
		{
			if(time.getTimeInMillis() == o.time.getTimeInMillis())
				return 0;
			if(time.getTimeInMillis() < o.time.getTimeInMillis())
				return -1;
			else
				return 1;
		}

		@Override
		public String toString()
		{
			return Miscellaneous.formatDate(time.getTime()) + ", reason : " + reason;
		}
	}

	static int getNextDayIntForExecution(Trigger trigger)
	{
		TimeFrame tf = new TimeFrame(trigger.getTriggerParameter2());
		Calendar now = Calendar.getInstance();

		if(tf.getDayList().contains(now.get(Calendar.DAY_OF_WEEK)))
			return now.get(Calendar.DAY_OF_WEEK);
		else
		{
			int dayNumberOfNextExecution = now.get(Calendar.DAY_OF_WEEK);

			while(!tf.getDayList().contains(dayNumberOfNextExecution))
			{
				dayNumberOfNextExecution++;
				if(dayNumberOfNextExecution > 6)
					dayNumberOfNextExecution = 1;
			}

			return dayNumberOfNextExecution;
		}
	}

	static int getDayDelta(Calendar now, int dayNumberOfNextExecution)
	{
		int result = dayNumberOfNextExecution - now.get(Calendar.DAY_OF_WEEK);

		if(result >= 0)
			return result;
		else
			return 6 + result;
	}

	@Nullable
	@RequiresApi(api = Build.VERSION_CODES.N)
	public static Calendar getNextRepeatedExecution(Trigger trigger)
	{
		Calendar now = Calendar.getInstance();
		Miscellaneous.logEvent("i", "DateTimeListener", "Checking for next repetition execution after " + Miscellaneous.formatDate(now.getTime()), 5);
		Calendar calculationStart, calSchedule = null;
		TimeFrame tf = new TimeFrame(trigger.getTriggerParameter2());

		if(tf.getRepetition() > 0)
		{
			/*
				Are we inside of the timeframe or outside?

				Inside -> is this demanded?
							Yes:
								If last execution known, calculate from it
								If not known, calculate from start of timeframe
							No:
								Use end-time and add repetition

				Outside? -> is this demanded?
							Yes:
								If last execution known, calculate from it
								If not known, calculate from end of timeframe
							No:
								Use start-time and add repetition
			 */

			if(areWeInTimeFrame(trigger, new Date()))
			{
				if(trigger.getTriggerParameter())
				{
					if(trigger.getParentRule().getLastExecution() != null)
					{
						calculationStart = (Calendar) trigger.getParentRule().getLastExecution().clone();
					}
					else
					{
						calculationStart = (Calendar) now.clone();
						calculationStart.set(Calendar.HOUR_OF_DAY, tf.getTriggerTimeStart().getHours());
						calculationStart.set(Calendar.MINUTE, tf.getTriggerTimeStart().getMinutes());
						calculationStart.set(Calendar.SECOND, tf.getTriggerTimeStart().getSeconds());
						calculationStart.set(Calendar.MILLISECOND, 0);
					}
					long differenceInSeconds = Math.abs(now.getTimeInMillis() - calculationStart.getTimeInMillis()) / 1000;
					long nextExecutionMultiplier = Math.floorDiv(differenceInSeconds, tf.getRepetition()) + 1;
					calSchedule = (Calendar) calculationStart.clone();
					calSchedule.add(Calendar.SECOND, (int) (nextExecutionMultiplier * tf.getRepetition()));
				}
				else
				{
					calculationStart = (Calendar) now.clone();
					calculationStart.set(Calendar.HOUR_OF_DAY, tf.getTriggerTimeStop().getHours());
					calculationStart.set(Calendar.MINUTE, tf.getTriggerTimeStop().getMinutes());
					calculationStart.set(Calendar.SECOND, tf.getTriggerTimeStop().getSeconds());
					calculationStart.set(Calendar.MILLISECOND, 0);
					calSchedule = (Calendar) calculationStart.clone();
					calSchedule.add(Calendar.SECOND, (int) tf.getRepetition());
				}
			}
			else		// not in timeframe
			{
				if (!trigger.getTriggerParameter())
				{
					if (trigger.getParentRule().getLastExecution() != null)
					{
						calculationStart = (Calendar) trigger.getParentRule().getLastExecution().clone();
						long differenceInSeconds = Math.abs(now.getTimeInMillis() - calculationStart.getTimeInMillis()) / 1000;
						long nextExecutionMultiplier = Math.floorDiv(differenceInSeconds, tf.getRepetition()) + 1;
						calSchedule = (Calendar) calculationStart.clone();
						calSchedule.add(Calendar.SECOND, (int) (nextExecutionMultiplier * tf.getRepetition()));
						Miscellaneous.logEvent("i", "getNextRepeatedExecutionAfter()", "Chose " + Miscellaneous.formatDate(calSchedule.getTime()) + " as next repeated execution time.", 5);
						return calSchedule;
					}
					else
					{
						calculationStart = (Calendar) now.clone();
						if(tf.getDayList().contains(now.get(Calendar.DAY_OF_WEEK)))
						{
							calculationStart.set(Calendar.HOUR_OF_DAY, tf.getTriggerTimeStop().getHours());
							calculationStart.set(Calendar.MINUTE, tf.getTriggerTimeStop().getMinutes());
							calculationStart.set(Calendar.SECOND, tf.getTriggerTimeStop().getSeconds());
							calculationStart.set(Calendar.MILLISECOND, 0);
							calculationStart.add(Calendar.SECOND, (int) tf.getRepetition());

							int dayDelta = getDayDelta(now, getNextDayIntForExecution(trigger));
							calculationStart.add(Calendar.DAY_OF_WEEK, dayDelta);
						}
						calSchedule = (Calendar) calculationStart.clone();
						Miscellaneous.logEvent("i", "getNextRepeatedExecutionAfter()", "Chose " + Miscellaneous.formatDate(calSchedule.getTime()) + " as next repeated execution time.", 5);
						return calSchedule;
					}
					/*long differenceInSeconds = Math.abs(now.getTimeInMillis() - calculationStart.getTimeInMillis()) / 1000;
					long nextExecutionMultiplier = Math.floorDiv(differenceInSeconds, tf.getRepetition()) + 1;
					calSchedule = (Calendar) calculationStart.clone();
					calSchedule.add(Calendar.SECOND, (int) (nextExecutionMultiplier * tf.getRepetition()));*/
				}
				else
				{
					calculationStart = (Calendar) now.clone();
					calculationStart.set(Calendar.HOUR_OF_DAY, tf.getTriggerTimeStart().getHours());
					calculationStart.set(Calendar.MINUTE, tf.getTriggerTimeStart().getMinutes());
					calculationStart.set(Calendar.SECOND, tf.getTriggerTimeStart().getSeconds());
					calculationStart.set(Calendar.MILLISECOND, 0);
					calSchedule = (Calendar) calculationStart.clone();
					calSchedule.add(Calendar.SECOND, (int) (tf.getRepetition()));
				}

				if (Miscellaneous.compareTimes(calSchedule, now) > 0)
					calSchedule.add(Calendar.DAY_OF_MONTH, 1);
			}

			int dayDelta = getDayDelta(now, getNextDayIntForExecution(trigger));
			calSchedule.add(Calendar.DAY_OF_WEEK, dayDelta);

			Miscellaneous.logEvent("i", "getNextRepeatedExecutionAfter()", "Chose " + Miscellaneous.formatDate(calSchedule.getTime()) + " as next repeated execution time.", 5);
		}
		else
			Miscellaneous.logEvent("i", "getNextRepeatedExecutionAfter()", "Trigger " + trigger.toString() + " is not configured to repeat.", 5);

		return calSchedule;
	}

	public static boolean areWeInTimeFrame(Trigger trigger, Object triggeringObject)
	{
		/*
		 * Use format known from Automation
		 * 07:30:00/17:30:00/23456/300	<-- last parameter is optional: repetition in seconds
		 * Also required: inside or outside that interval
		 */

		Date triggeringTime;
//		if(triggeringObject instanceof Date)
//			triggeringTime = (Date)triggeringObject;
//		else
			triggeringTime = new Date();

		String timeString = String.valueOf(triggeringTime.getHours()) + ":" + String.valueOf(triggeringTime.getMinutes()) + ":" + String.valueOf(triggeringTime.getSeconds());
		TimeObject nowTime = TimeObject.valueOf(timeString);
		Calendar calNow = Calendar.getInstance();

		try
		{
			TimeFrame tf = new TimeFrame(trigger.getTriggerParameter2());

			if(tf.getDayList().contains(calNow.get(Calendar.DAY_OF_WEEK)))
			{
				if(
					// Regular case, start time is lower than end time
						(
							Miscellaneous.compareTimes(tf.getTriggerTimeStart(), nowTime) >= 0
									&&
							Miscellaneous.compareTimes(nowTime, tf.getTriggerTimeStop()) > 0
						)
								||
					// Other case, start time higher than end time, timeframe goes over midnight
						(
							Miscellaneous.compareTimes(tf.getTriggerTimeStart(), tf.getTriggerTimeStop()) < 0
									&&
							(Miscellaneous.compareTimes(tf.getTriggerTimeStart(), nowTime) >= 0
									||
							Miscellaneous.compareTimes(nowTime, tf.getTriggerTimeStop()) > 0)
						)
								||
					// further case: start and end times are identical, meaning a 24h window
					(
							Miscellaneous.compareTimes(tf.getTriggerTimeStart(), tf.getTriggerTimeStop()) == 0
					)
				)
					return true;
			}
		}
		catch(Exception e)
		{
			Miscellaneous.logEvent("e", "Trigger", "There was an error while checking if the time based trigger applies: " + Log.getStackTraceString(e), 1);
			return false;
		}

		return false;
	}
}