/*
 * Copyright (C) 2015 The Nevolution Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.oasisfeng.nevo.sdk;

import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.BadParcelableException;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.NetworkOnMainThreadException;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.support.annotation.CallSuper;
import android.support.annotation.Keep;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.util.Log;

import com.oasisfeng.nevo.decorator.INevoDecorator;
import com.oasisfeng.nevo.engine.INevoController;

import java.util.Collections;
import java.util.List;

import static android.content.pm.PackageManager.GET_SIGNATURES;
import static android.content.pm.PackageManager.SIGNATURE_MATCH;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static java.util.Collections.singletonList;

/**
 * Interface for notification decorator.
 *
 * <p><b>Decorator permission restriction:</b> Only notifications from packages enabled for this decorator are accessible via APIs below.
 *
 * @author Oasis
 */
@RequiresApi(M) public abstract class NevoDecoratorService extends Service {

	/** The action to bind {@link NevoDecoratorService} */
	public static final String ACTION_DECORATOR_SERVICE = "com.oasisfeng.nevo.Decorator";

	/** Optional meta-data key within the &lt;service&gt; tag, to indicate the target packages (separated by comma) of the app-specific decorator */
	public static final String META_KEY_PACKAGES = "packages";

	/** Valid constant values for {@link android.app.Notification#EXTRA_TEMPLATE} */
	public static final String TEMPLATE_BIG_TEXT	= "android.app.Notification$BigTextStyle";
	public static final String TEMPLATE_INBOX		= "android.app.Notification$InboxStyle";
	public static final String TEMPLATE_BIG_PICTURE	= "android.app.Notification$BigPictureStyle";
	public static final String TEMPLATE_MEDIA		= "android.app.Notification$MediaStyle";
	public static final String TEMPLATE_MESSAGING	= "android.app.Notification$MessagingStyle";

	/**
	 * Apply this decorator to the notification. <b>Implementation should be idempotent</b>,
	 * assuming the given notification may be (or may be not) already evolved by this decorator before.
	 *
	 * <p>Notice: Since the notification might be evolved already, the tag and ID could be altered, only the key is immutable.
	 *
	 * <p>Beware: Not all notifications can be modified, the decoration made here may be ignored if it is not modifiable.
	 * For example, sticky notification ({@link android.app.Notification#FLAG_ONGOING_EVENT FLAG_ONGOING_EVENT}
	 * or {@link android.app.Notification#FLAG_FOREGROUND_SERVICE FLAG_FOREGROUND_SERVICE}) is not modifiable at present.
	 *
	 * @param evolving the incoming notification evolved by preceding decorators and to be evolved by this decorator,
	 *                 or an already evolved notification (with or without this decorator).
	 */
	@Keep protected void apply(final MutableStatusBarNotification evolving) {}

	/** Called when connected by Nevolution engine. Override this method to perform initial process. */
	@Keep protected void onConnected() {}

	/**
	 * Called when notification (no matter decorated or not) from packages with this decorator enabled is removed.
	 * <p>
	 * This is also called with original key and {@link android.service.notification.NotificationListenerService#REASON_APP_CANCEL REASON_APP_CANCEL}
	 * when originating app requests notification removal, only if the feature "Removal-aware" of Nevolution is activated.
	 *
	 * @param key the original key for removal requested by originating app, or the real key (may be different from original key) otherwise.
	 * @param reason see REASON_XXX constants in {@link android.service.notification.NotificationListenerService}, always 0 before Android O.
	 */
	@Keep protected void onNotificationRemoved(final String key, final int reason) {}

	/**
	 * Called when notification (no matter decorated or not) from packages with this decorator enabled is removed.
	 *
	 * If notification payload is not relevant, please consider overriding {@link #onNotificationRemoved(String, int)} instead.
	 *
	 * @param reason see REASON_XXX constants in {@link android.service.notification.NotificationListenerService}, always 0 before Android O.
	 */
	@Keep protected void onNotificationRemoved(final StatusBarNotification notification, final int reason) {}

	/**
	 * Retrieve historic notifications posted with the given key (including the incoming one without decoration at the last).
	 * The number of notifications kept in archive is undefined.
	 *
	 * Decorator permission restriction applies.
	 */
	protected final List<StatusBarNotification> getArchivedNotifications(final String key, final int limit) {
		try {
			return mController.getNotifications(mWrapper, TYPE_ARCHIVED, singletonList(key), limit, null);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error retrieving archived notifications: " + key, e);
			return Collections.emptyList();
		}
	}

	/**
	 * Retrieve notifications by keys, latest one (in evolved form) for each key, no matter active or removed.
	 *
	 * The returned list may not contain entries for some of the requested keys, if not allowed or missing in archive.
	 * It may also contain multiple entries for some requested keys, since split notifications with altered tag and ID share the same key.
	 *
	 * Decorator permission restriction applies.
	 */
	protected final List<StatusBarNotification> getLatestNotifications(final List<String> keys) {
		try {
			return mController.getNotifications(mWrapper, TYPE_LATEST, keys, 0, null);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error retrieving notifications", e);
			return Collections.emptyList();
		}
	}

	/**
	 * Cancel an active notification, remove it from notification panel.
	 *
	 * Decorator permission restriction applies.
	 */
	protected final void cancelNotification(final String key) {
		try {
			mController.performNotificationAction(mWrapper, ACTION_CANCEL, key, null);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error canceling notification: " + key, e);
		}
	}

	/**
	 * Revive a previously cancelled (removed or swiped) notification, with no alerts (sound, vibration, lights).
	 * If the notification is still active, it will not be affected.
	 *
	 * Decorator permission restriction applies.
	 *
	 * @param key the real key (may be different from original key)
	 */
	protected final void reviveNotification(final String key) {
		try {
			mController.performNotificationAction(mWrapper, ACTION_REVIVE, key, null);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error reviving notification: " + key, e);
		}
	}

	/**
	 * Recast a past (either still active or already removed) notification asynchronously,
	 * which will then go through the decorators (including this one) as if just posted.
	 *
	 * Decorator permission restriction applies.
	 *
	 * @param key the original key of the notification to recast
	 * @param fillInExtras additional extras to fill in the notification being recast. These additions are only present during the recasting procedure.
	 */
	protected final void recastNotification(final String key, final @Nullable Bundle fillInExtras) {
		try {
			mController.performNotificationAction(mWrapper, ACTION_RECAST, key, fillInExtras);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error recasting notification: " + key, e);
		}
	}

	/**
	 * (Android O+ only) Snooze a STICKY (on-going or foreground) notification.
	 *
	 * <p>Non-sticky notifications are not allowed to snooze at present. If you need to snooze them,
	 * please file an issue to discuss the use case with us.
	 *
	 * @see android.service.notification.NotificationListenerService#snoozeNotification(String, long)
	 */
	protected final void snoozeNotification(final String key, final long duration) {
		try {
			final Bundle bundle = new Bundle();
			bundle.putLong(KEY_DURATION, duration);
			mController.performNotificationAction(mWrapper, ACTION_SNOOZE, key, bundle);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error recasting notification: " + key, e);
		}
	}

	/**
	 * Returns the notification channel settings for a given channel id in targeted app.
	 * If specified package is not targeted by this decorator, {@link SecurityException} will be thrown.
	 *
	 * @see android.app.NotificationManager#getNotificationChannel(String)
	 */
	@RequiresApi(O) protected final @Nullable NotificationChannel getNotificationChannel(final String pkg, final UserHandle user, final String channel) {
		if (mSupportedApiVersion < 4) return null;
		try {
			final List<NotificationChannel> channels = mController.getNotificationChannels(mWrapper, pkg, singletonList(channel), bundleIfNeeded(user));
			return channels == null || channels.isEmpty() ? null : channels.get(0);
		} catch (final RemoteException e) {
			Log.w(TAG, "Error querying notification channel in " + pkg + ": " + channel, e);
			return null;
		}
	}

	/**
	 * Create {@link NotificationChannel} for targeted app. If specified package is not targeted by this decorator, {@link SecurityException} will be thrown.
	 *
	 * @see android.app.NotificationManager#createNotificationChannel(NotificationChannel)
	 */
	@RequiresApi(O) protected final void createNotificationChannels(final String pkg, final UserHandle user, final List<NotificationChannel> channels) {
		try {
			mController.createNotificationChannels(mWrapper, pkg, channels, bundleIfNeeded(user));
		} catch (final RemoteException e) {
			Log.w(TAG, "Error creating notification channels for " + pkg + ": " + channels, e);
		}
	}

	/** @deprecated use {@link #createNotificationChannels(String, UserHandle, List)} instead */
	@Deprecated @RequiresApi(O) protected final void createNotificationChannels(final String pkg, final List<NotificationChannel> channels) {
		createNotificationChannels(pkg, Process.myUserHandle(), channels);
	}

	/**
	 * Delete {@link NotificationChannel} for targeted app. If specified package is not targeted by this decorator
	 * or specified channel is not created by this decorator, nothing will be deleted.
	 *
	 * @see android.app.NotificationManager#deleteNotificationChannel(String)
	 */
	@RequiresApi(O) protected final void deleteNotificationChannel(final String pkg, final UserHandle user, final String channel) {
		if (mSupportedApiVersion < 4) return;
		try {
			mController.deleteNotificationChannel(mWrapper, pkg, channel, bundleIfNeeded(user));
		} catch (final RemoteException e) {
			Log.w(TAG, "Error deleting notification channel for " + pkg + ": " + channel, e);
		}
	}

	/**
	 * Get the API version of Nevolution SDK supported by Nevolution engine installed on this device.
	 * If the supported API version in user's device is lowed than API version of SDK used in your project, some new APIs may not work.
	 *
	 * API version of current Nevolution SDK is defined in {@link R.integer#nevo_api_version}
	 */
	protected final int getSupportedApiVersion() {
		return mSupportedApiVersion;
	}

	@CallSuper @Override public IBinder onBind(final Intent intent) {
		for (Class<?> clazz = getClass(); clazz != NevoDecoratorService.class; clazz = clazz.getSuperclass()) {
			detectDerivedMethod(FLAG_DECORATION_AWARE, clazz, "apply", MutableStatusBarNotification.class);
			detectDerivedMethod(FLAG_REMOVAL_AWARE_KEY_ONLY, clazz, "onNotificationRemoved", String.class, int.class);
			detectDerivedMethod(FLAG_REMOVAL_AWARE, clazz, "onNotificationRemoved", StatusBarNotification.class, int.class);
		}
		return mWrapper == null ? mWrapper = new INevoDecoratorWrapper() : mWrapper;
	}

	private static Bundle bundleIfNeeded(final UserHandle user) {
		if (user == null || Process.myUserHandle().equals(user)) return null;
		final Bundle bundle = new Bundle();
		bundle.putParcelable(Intent.EXTRA_USER, user);
		return bundle;
	}

	private void detectDerivedMethod(final int flag, final Class<?> clazz, final String name, final Class<?>... parameter_types) {
		if ((mFlags & flag) == 0) try {
			if (clazz.getDeclaredMethod(name, parameter_types) != null) mFlags |= flag;
		} catch (final NoSuchMethodException ignored) {}
	}

	private static String shorten(final String name) {
		final String suffix = "Decorator";
		return name.endsWith(suffix) ? name.substring(0, name.length() - suffix.length()) : name;
	}

	private INevoDecoratorWrapper mWrapper;
	private INevoController mController;
	private int mSupportedApiVersion;
	private int mFlags;

	@RestrictTo(LIBRARY) static final int TYPE_LATEST   = 1;
	@RestrictTo(LIBRARY) static final int TYPE_ARCHIVED = 2;
	@RestrictTo(LIBRARY) static final int ACTION_CANCEL = 1;
	@RestrictTo(LIBRARY) static final int ACTION_REVIVE = 2;
	@RestrictTo(LIBRARY) static final int ACTION_RECAST = 3;
	@RestrictTo(LIBRARY) static final int ACTION_SNOOZE = 4;
	@RestrictTo(LIBRARY) static final int FLAG_DECORATION_AWARE = 0x1;
	@RestrictTo(LIBRARY) static final int FLAG_REMOVAL_AWARE_KEY_ONLY = 0x2;
	@RestrictTo(LIBRARY) static final int FLAG_REMOVAL_AWARE = 0x4;
	@RestrictTo(LIBRARY) static final String KEY_REASON = "reason";
	@RestrictTo(LIBRARY) static final String KEY_SUPPORTED_API_VERSION = "version";
	@RestrictTo(LIBRARY) static final String KEY_DURATION = "duration";

	protected final String TAG = "Nevo.Decorator[" + shorten(getClass().getSimpleName()) + "]";

	private class INevoDecoratorWrapper extends INevoDecorator.Stub {

		@Override public void apply(final/* inout */MutableStatusBarNotification evolving, final @Nullable Bundle options) {
			if (Binder.getCallingUid() != mCallerUid) throw new SecurityException();
			try {
				Log.v(TAG, "Applying to " + evolving.getKey());
				NevoDecoratorService.this.apply(evolving);
				evolving.setAllowIncrementalWriteBack();
			} catch (final Throwable t) {
				Log.e(TAG, "Error running apply()", t);
				throw asParcelableException(t);
			}
		}

		@Override public void onNotificationRemoved(final String key, final @Nullable Bundle options) {
			if (Binder.getCallingUid() != mCallerUid) throw new SecurityException();
			try {
				NevoDecoratorService.this.onNotificationRemoved(key, options != null ? options.getInt(KEY_REASON) : 0);
			} catch (final Throwable t) {
				Log.e(TAG, "Error running onNotificationRemoved()", t);
				throw asParcelableException(t);
			}
		}

		@Override public void onNotificationRemovedLight(final StatusBarNotification notification, final @Nullable Bundle options) {
			if (Binder.getCallingUid() != mCallerUid) throw new SecurityException();
			try {
				NevoDecoratorService.this.onNotificationRemoved(notification, options != null ? options.getInt(KEY_REASON) : 0);
			} catch (final Throwable t) {
				Log.e(TAG, "Error running onNotificationRemoved()", t);
				throw asParcelableException(t);
			}
		}

		@Override public int onConnected(final INevoController controller, final Bundle options) {
			RemoteImplementation.initializeIfNotYet(NevoDecoratorService.this);

			final PackageManager pm = getPackageManager();
			final int caller_uid = Binder.getCallingUid(), my_uid = Process.myUid();
			if (caller_uid != my_uid && pm.checkSignatures(caller_uid, my_uid) != SIGNATURE_MATCH) {
				final String[] caller_pkgs = pm.getPackagesForUid(caller_uid);
				if (caller_pkgs == null || caller_pkgs.length == 0) throw new SecurityException();
				try { @SuppressLint("PackageManagerGetSignatures")
				final PackageInfo caller_info = pm.getPackageInfo(caller_pkgs[0], GET_SIGNATURES);
					if (caller_info == null) throw new SecurityException();
					for (final Signature signature : caller_info.signatures)
						if (signature.hashCode() != SIGNATURE_HASH) throw new SecurityException("Caller signature mismatch");
				} catch (final PackageManager.NameNotFoundException e) { throw new SecurityException(); }	// Should not happen
			}
			mCallerUid = caller_uid;

			mController = controller;
			if (options != null) mSupportedApiVersion = options.getInt(KEY_SUPPORTED_API_VERSION);
			try {
				Log.v(TAG, "onConnected");
				NevoDecoratorService.this.onConnected();
			} catch (final Throwable t) {
				Log.e(TAG, "Error running onConnected()", t);
				throw asParcelableException(t);
			}
			return mFlags;
		}

		private RuntimeException asParcelableException(final Throwable e) {
			if (e instanceof SecurityException
					|| e instanceof BadParcelableException
					|| e instanceof IllegalArgumentException
					|| e instanceof NullPointerException
					|| e instanceof IllegalStateException
					|| e instanceof NetworkOnMainThreadException
					|| e instanceof UnsupportedOperationException)
				return (RuntimeException) e;
			return new IllegalStateException(e);
		}

		private int mCallerUid = -1;
		private static final int SIGNATURE_HASH = -541181501;
	}
}