/*
 * Copyright (C) 2020 Tobias Brunner
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

package org.strongswan.android.logic;

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

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.UUID;

import androidx.annotation.RequiresApi;

public class Scheduler extends BroadcastReceiver
{
	private final String EXECUTE_JOB = "one.shx.strongswan.ext.Scheduler.EXECUTE_JOB";
	private final String TAG = Scheduler.class.getSimpleName();
	private final Context mContext;
	private final AlarmManager mManager;
	private final PriorityQueue<ScheduledJob> mJobs;

	@SuppressLint("UnspecifiedRegisterReceiverFlag")
	public Scheduler(Context context)
	{
		mContext = context;
		mManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
		mJobs = new PriorityQueue<>();

		IntentFilter filter = new IntentFilter();
		filter.addAction(EXECUTE_JOB);
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
		{
			mContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
		}
		else
		{
			mContext.registerReceiver(this, filter);
		}
	}

	/**
	 * Remove all pending jobs and unregister the receiver.
	 * Called via JNI.
	 */
	public void Terminate()
	{
		synchronized (this)
		{
			mJobs.clear();
		}
		mManager.cancel(createIntent());
		mContext.unregisterReceiver(this);
	}

	/**
	 * Allocate a job ID. Called via JNI.
	 *
	 * @return random ID for a new job
	 */
	public String allocateId()
	{
		return UUID.randomUUID().toString();
	}

	/**
	 * Create a pending intent to execute a job.
	 *
	 * @return pending intent
	 */
	private PendingIntent createIntent()
	{
		/* using component/class doesn't work with dynamic broadcast receivers */
		Intent intent = new Intent(EXECUTE_JOB);
		intent.setPackage(mContext.getPackageName());
		int flags = 0;
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
		{
			flags |= PendingIntent.FLAG_IMMUTABLE;
		}
		return PendingIntent.getBroadcast(mContext, 0, intent, flags);
	}

	/**
	 * Schedule the next wakeup alarm.  Uses exact alarms so the device is woken
	 * for NAT keepalives and rekeyings while it is idle, but since Android 13
	 * that special permission is denied by default, so fall back to inexact
	 * alarms instead of crashing with a SecurityException (connectivity then
	 * only degrades slightly while timers may be delayed in doze mode).
	 *
	 * @param time wakeup time based on RTC
	 */
	@RequiresApi(api = Build.VERSION_CODES.M)
	private void scheduleAlarm(long time)
	{
		PendingIntent pending = createIntent();
		boolean exact = true;
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
		{
			exact = mManager.canScheduleExactAlarms();
		}
		try
		{
			if (exact)
			{
				mManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pending);
			}
			else
			{
				mManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pending);
			}
		}
		catch (SecurityException e)
		{	/* permission might have been revoked, or OEM restrictions apply */
			Log.w(TAG, "Falling back to inexact alarm after SecurityException");
			mManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pending);
		}
	}

	/**
	 * Schedule executing a job in the future.
	 * Called via JNI from different threads.
	 *
	 * @param id job ID
	 * @param ms delta in milliseconds when the job should be executed
	 */
	@RequiresApi(api = Build.VERSION_CODES.M)
	public void scheduleJob(String id, long ms)
	{
		synchronized (this)
		{
			ScheduledJob job = new ScheduledJob(id, System.currentTimeMillis() + ms);
			mJobs.add(job);

			if (job == mJobs.peek())
			{	/* update the alarm if the job has to be executed before all others */
				scheduleAlarm(job.Time);
			}
		}
	}

	@RequiresApi(api = Build.VERSION_CODES.M)
	@Override
	public void onReceive(Context context, Intent intent)
	{
		ArrayList<ScheduledJob> jobs = new ArrayList<>();
		long now = System.currentTimeMillis();

		synchronized (this)
		{
			ScheduledJob job = mJobs.peek();
			while (job != null)
			{
				if (job.Time > now)
				{
					break;
				}
				jobs.add(mJobs.remove());
				job = mJobs.peek();
			}
			if (job != null)
			{
				scheduleAlarm(job.Time);
			}
		}

		for (ScheduledJob job : jobs)
		{
			executeJob(job.Id);
		}
	}

	/**
	 * Execute the job with the given ID.
	 *
	 * @param id job ID
	 */
	public native void executeJob(String id);

	/**
	 * Keep track of scheduled jobs.
	 */
	private static class ScheduledJob implements Comparable<ScheduledJob>
	{
		String Id;
		long Time;

		ScheduledJob(String id, long time)
		{
			Id = id;
			Time = time;
		}

		@Override
		public int compareTo(ScheduledJob o)
		{
			return Long.compare(Time, o.Time);
		}
	}
}
