/*
 * 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.decorators.wechat;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Process;
import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.ColorInt;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat.MessagingStyle;
import android.support.v4.graphics.drawable.IconCompat;
import android.util.Log;

import com.oasisfeng.nevo.decorators.wechat.ConversationManager.Conversation;
import com.oasisfeng.nevo.sdk.MutableNotification;
import com.oasisfeng.nevo.sdk.MutableStatusBarNotification;
import com.oasisfeng.nevo.sdk.NevoDecoratorService;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static android.app.Notification.EXTRA_SUB_TEXT;
import static android.app.Notification.EXTRA_TEXT;
import static android.app.Notification.EXTRA_TITLE;
import static android.app.Notification.FLAG_GROUP_SUMMARY;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;

/**
 * Bring state-of-art notification experience to WeChat.
 *
 * Created by Oasis on 2015/6/1.
 */
public class WeChatDecorator extends NevoDecoratorService {

	public static final String WECHAT_PACKAGE = "com.tencent.mm";
	static final String CHANNEL_MESSAGE = "message_channel_new_id";				// Channel ID used by WeChat for all message notifications
	private static final int MAX_NUM_ARCHIVED = 20;
	private static final String OLD_CHANNEL_MESSAGE = "message";				//   old name for migration
	private static final String CHANNEL_MISC = "reminder_channel_id";			// Channel ID used by WeChat for misc. notifications
	private static final String OLD_CHANNEL_MISC = "misc";						//   old name for migration
	private static final String CHANNEL_DND = "message_dnd_mode_channel_id";	// Channel ID used by WeChat for its own DND mode
	private static final String CHANNEL_VOIP = "voip_notify_channel_new_id";	// Channel ID used by WeChat for VoIP notification
	private static final String CHANNEL_GROUP_CONVERSATION = "group";			// WeChat has no separate group for group conversation
	private static final String GROUP_GROUP = "nevo.group.wechat.group";
	private static final String GROUP_BOT = "nevo.group.wechat.bot";
	private static final String GROUP_DIRECT = "nevo.group.wechat";
	private static final String GROUP_MISC = "misc";    // Not auto-grouped
	@SuppressWarnings("SpellCheckingInspection")
	static final String KEY_SERVICE_MESSAGE = "notifymessage";					// Virtual WeChat account for service notification messages

	private static final @ColorInt int PRIMARY_COLOR = 0xFF33B332;
	private static final @ColorInt int LIGHT_COLOR = 0xFF00FF00;
	static final String ACTION_SETTINGS_CHANGED = "SETTINGS_CHANGED";
	static final String PREFERENCES_NAME = "decorators-wechat";

	@Override public boolean apply(final MutableStatusBarNotification evolving) {
		final MutableNotification n = evolving.getNotification();
		if ((n.flags & FLAG_GROUP_SUMMARY) != 0) {
			if (GROUP_GROUP.equals(n.getGroup())) {
				n.extras.putCharSequence(EXTRA_SUB_TEXT, getText(R.string.header_group_chat));
			} else if (GROUP_BOT.equals(n.getGroup())) {
				n.extras.putCharSequence(EXTRA_SUB_TEXT, getText(R.string.header_bot_message));
			} else return false;
			return true;
		}
		final Bundle extras = n.extras;
		CharSequence title = extras.getCharSequence(EXTRA_TITLE);
		if (title == null || title.length() == 0) {
			Log.e(TAG, "Title is missing: " + evolving);
			return false;
		}
		final int flags = n.flags; final String channel_id = SDK_INT >= O ? n.getChannelId() : null;
		if ((flags & Notification.FLAG_ONGOING_EVENT) != 0 && CHANNEL_VOIP.equals(channel_id)) return false;

		n.color = PRIMARY_COLOR;        // Tint the small icon
		extras.putBoolean(Notification.EXTRA_SHOW_WHEN, true);
		if (isEnabled(mPrefKeyWear)) n.flags &= ~ Notification.FLAG_LOCAL_ONLY;

		if (n.tickerText == null/* Legacy misc. notifications */|| CHANNEL_MISC.equals(channel_id)) {
			if (SDK_INT >= O && channel_id == null) n.setChannelId(CHANNEL_MISC);
			n.setGroup(GROUP_MISC);        // Avoid being auto-grouped
			Log.d(TAG, "Skip further process for non-conversation notification: " + title);    // E.g. web login confirmation notification.
			return (n.flags & Notification.FLAG_FOREGROUND_SERVICE) == 0;
		}
		final CharSequence content_text = extras.getCharSequence(EXTRA_TEXT);
		if (content_text == null) return true;

		if (SDK_INT >= N && extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY) != null)
			n.flags |= Notification.FLAG_ONLY_ALERT_ONCE;		// No more alert for direct-replied notification.

		// WeChat previously uses dynamic counter starting from 4097 as notification ID, which is reused after cancelled by WeChat itself,
		//   causing conversation duplicate or overwritten notifications.
		final String pkg = evolving.getPackageName();
		final Conversation conversation;
		if (! isDistinctId(n, pkg)) {
			final int title_hash = title.hashCode();	// Not using the hash code of original title, which might have already evolved.
			evolving.setId(title_hash);
			conversation = mConversationManager.getOrCreateConversation(title_hash);
		} else conversation = mConversationManager.getOrCreateConversation(evolving.getOriginalId());

		final Icon icon = n.getLargeIcon();
		conversation.icon = icon != null ? IconCompat.createFromIcon(this, icon) : null;
		conversation.title = title;
		conversation.summary = content_text;
		conversation.ticker = n.tickerText;
		conversation.timestamp = n.when;
		conversation.ext = new Notification.CarExtender(n).getUnreadConversation();

		if (conversation.isTypeUnknown())
			conversation.setType(WeChatMessage.guessConversationType(conversation));    // mMessagingBuilder replies on the type

		MessagingStyle messaging = mMessagingBuilder.buildFromConversation(conversation, evolving);
		if (messaging == null)	// EXTRA_TEXT will be written in buildFromArchive()
			messaging = mMessagingBuilder.buildFromArchive(conversation, n, title, getArchivedNotifications(evolving.getOriginalKey(), MAX_NUM_ARCHIVED));
		if (messaging == null) return true;
		final List<MessagingStyle.Message> messages = messaging.getMessages();
		if (messages.isEmpty()) return true;

		final boolean is_group_chat = conversation.isGroupChat();
		if (SDK_INT >= P && KEY_SERVICE_MESSAGE.equals(conversation.key)) {     // Setting conversation title before Android P will make it a group chat.
			messaging.setConversationTitle(getString(R.string.header_service_message)); // A special header for this non-group conversation with multiple senders
			n.setGroup(GROUP_BOT);
		} else n.setGroup(is_group_chat ? GROUP_GROUP : conversation.isBotMessage() ? GROUP_BOT : GROUP_DIRECT);
		if (SDK_INT >= O) {
			if (is_group_chat && mUseExtraChannels && ! CHANNEL_DND.equals(channel_id))
				n.setChannelId(CHANNEL_GROUP_CONVERSATION);
			else if (channel_id == null) n.setChannelId(CHANNEL_MESSAGE);	// WeChat versions targeting O+ have its own channel for message
		}

		if (is_group_chat) messaging.setGroupConversation(true).setConversationTitle(title);
		MessagingBuilder.flatIntoExtras(messaging, extras);
		extras.putString(Notification.EXTRA_TEMPLATE, TEMPLATE_MESSAGING);
		return true;
	}

	private boolean isDistinctId(final Notification n, final String pkg) {
		if (mDistinctIdSupported != null) return mDistinctIdSupported;
		int version = 0;
		final ApplicationInfo app_info = n.extras.getParcelable("android.appInfo");
		if (app_info != null) try {
			if (pkg.equals(app_info.packageName))	// This will be Nevolution for active evolved notifications.
				//noinspection JavaReflectionMemberAccess
				version = (int) ApplicationInfo.class.getField("versionCode").get(app_info);
		} catch (final IllegalAccessException | NoSuchFieldException | ClassCastException ignored) {}    // Fall-through
		if (version == 0) try {
			version = getPackageManager().getPackageInfo(pkg, 0).versionCode;
		} catch (final PackageManager.NameNotFoundException ignored) {}
		return version != 0 && (mDistinctIdSupported = version >= 1340);	// Distinct ID is supported since WeChat 6.7.3.
	}
	private Boolean mDistinctIdSupported;

	private boolean isEnabled(final String mPrefKeyCallTweak) {
		return mPreferences.getBoolean(mPrefKeyCallTweak, false);
	}

	@Override protected boolean onNotificationRemoved(final String key, final int reason) {
		if (reason == REASON_APP_CANCEL) {		// For ongoing notification, or if "Removal-Aware" of Nevolution is activated
			Log.d(TAG, "Cancel notification: " + key);
		} else if (SDK_INT >= O && reason == REASON_CHANNEL_BANNED && ! isChannelAvailable(getUser(key))) {
			Log.w(TAG, "Channel lost, disable extra channels from now on.");
			mUseExtraChannels = false;
			mHandler.post(() -> recastNotification(key, null));
		} else if (SDK_INT < O || reason == REASON_CANCEL) {	// Exclude the removal request by us in above case. (Removal-Aware is only supported on Android 8+)
			mMessagingBuilder.markRead(key);
		}
		return false;
	}

	private boolean isChannelAvailable(final UserHandle user) {
		return getNotificationChannel(WECHAT_PACKAGE, user, CHANNEL_GROUP_CONVERSATION) != null;
	}

	private static UserHandle getUser(final String key) {
		final int pos_pipe = key.indexOf('|');
		if (pos_pipe > 0) try {
			return userHandleOf(Integer.parseInt(key.substring(0, pos_pipe)));
		} catch (final NumberFormatException ignored) {}
		Log.e(TAG, "Invalid key: " + key);
		return Process.myUserHandle();		// Only correct for single user.
	}

	@Override protected void onConnected() {
		if (SDK_INT >= O) {
			mWeChatTargetingO = isWeChatTargeting26OrAbove();
			final List<NotificationChannel> channels = new ArrayList<>();
			channels.add(makeChannel(CHANNEL_GROUP_CONVERSATION, R.string.channel_group_message, false));
			// WeChat versions targeting O+ have its own channels for message and misc
			channels.add(migrate(OLD_CHANNEL_MESSAGE,	CHANNEL_MESSAGE,	R.string.channel_message, false));
			channels.add(migrate(OLD_CHANNEL_MISC,		CHANNEL_MISC,		R.string.channel_misc, true));
			createNotificationChannels(WECHAT_PACKAGE, Process.myUserHandle(), channels);
		}
	}

	@RequiresApi(O) private NotificationChannel migrate(final String old_id, final String new_id, final @StringRes int new_name, final boolean silent) {
		final NotificationChannel channel_message = getNotificationChannel(WECHAT_PACKAGE, Process.myUserHandle(), old_id);
		deleteNotificationChannel(WECHAT_PACKAGE, Process.myUserHandle(), old_id);
		if (channel_message != null) return cloneChannel(channel_message, new_id, new_name);
		else return makeChannel(new_id, new_name, silent);
	}

	@RequiresApi(O) private NotificationChannel makeChannel(final String channel_id, final @StringRes int name, final boolean silent) {
		final NotificationChannel channel = new NotificationChannel(channel_id, getString(name), NotificationManager.IMPORTANCE_HIGH/* Allow heads-up (by default) */);
		if (silent) channel.setSound(null, null);
		else channel.setSound(getDefaultSound(), new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
				.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT).build());
		channel.enableLights(true);
		channel.setLightColor(LIGHT_COLOR);
		return channel;
	}

	@RequiresApi(O) private NotificationChannel cloneChannel(final NotificationChannel channel, final String id, final int new_name) {
		final NotificationChannel clone = new NotificationChannel(id, getString(new_name), channel.getImportance());
		clone.setGroup(channel.getGroup());
		clone.setDescription(channel.getDescription());
		clone.setLockscreenVisibility(channel.getLockscreenVisibility());
		clone.setSound(Optional.ofNullable(channel.getSound()).orElse(getDefaultSound()), channel.getAudioAttributes());
		clone.setBypassDnd(channel.canBypassDnd());
		clone.setLightColor(channel.getLightColor());
		clone.setShowBadge(channel.canShowBadge());
		clone.setVibrationPattern(channel.getVibrationPattern());
		return clone;
	}

	@Nullable private Uri getDefaultSound() {	// Before targeting O, WeChat actually plays sound by itself (not via Notification).
		return mWeChatTargetingO ? Settings.System.DEFAULT_NOTIFICATION_URI : null;
	}

	private boolean isWeChatTargeting26OrAbove() {
		try {
			return getPackageManager().getApplicationInfo(WECHAT_PACKAGE, PackageManager.GET_UNINSTALLED_PACKAGES).targetSdkVersion >= O;
		} catch (final PackageManager.NameNotFoundException e) {
			return false;
		}
	}

	@Override public void onCreate() {
		super.onCreate();
		final Context context = SDK_INT >= N ? createDeviceProtectedStorageContext() : this;
		//noinspection deprecation
		mPreferences = context.getSharedPreferences(PREFERENCES_NAME, MODE_MULTI_PROCESS);
		migrateFromLegacyPreferences();		// TODO: Remove this IO-blocking migration code (created in Aug, 2019).
		mPrefKeyWear = getString(R.string.pref_wear);

		mMessagingBuilder = new MessagingBuilder(this, new MessagingBuilder.Controller() {
			@Override public void recastNotification(final String key, final Bundle addition) {
				WeChatDecorator.this.recastNotification(key, addition);
			}
			@Override public Conversation getConversation(final int id) {
				return mConversationManager.getConversation(id);
			}
		});	// Must be called after loadPreferences().
		final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); filter.addDataScheme("package");
		registerReceiver(mPackageEventReceiver, filter);
		registerReceiver(mSettingsChangedReceiver, new IntentFilter(ACTION_SETTINGS_CHANGED));
	}

	@Override public void onDestroy() {
		unregisterReceiver(mSettingsChangedReceiver);
		unregisterReceiver(mPackageEventReceiver);
		mMessagingBuilder.close();
		super.onDestroy();
	}

	private void migrateFromLegacyPreferences() {
		if (mPreferences.getInt(PREF_KEY_MIGRATED, 0) >= 1) return;
		final SharedPreferences.Editor editor = mPreferences.edit();
		try {
			@SuppressWarnings("deprecation") final Context old_context = createPackageContext(BuildConfig.APPLICATION_ID, 0);
			final SharedPreferences old_sp = (SDK_INT >= N ? old_context.createDeviceProtectedStorageContext() : old_context)
					.getSharedPreferences(getDefaultSharedPreferencesName(old_context), 0);
			final Map<String, ?> old_entries = old_sp.getAll();
			Log.i(TAG, "Migrate from legacy preferences: " + old_entries);
			if (old_entries.isEmpty()) return;
			for (final Map.Entry<String, ?> entry : old_entries.entrySet()) {
				final Object value = entry.getValue();
				if (value instanceof Boolean) editor.putBoolean(entry.getKey(), (Boolean) value);	// Only boolean entries in legacy preferences.
			}
		} catch (final PackageManager.NameNotFoundException e) {
			Log.i(TAG, "No legacy preferences to migrate.");
		} catch (final RuntimeException e) {
			Log.e(TAG, "Error migrating legacy preferences.", e);
		}
		editor.putInt(PREF_KEY_MIGRATED, 1).apply();	// Ensure at least one entry to prevent migration on the next start.
	}
	private static final String PREF_KEY_MIGRATED = "migrated";

	private static String getDefaultSharedPreferencesName(final Context context) {
		return SDK_INT >= N ? PreferenceManager.getDefaultSharedPreferencesName(context) : context.getPackageName() + "_preferences";
	}

	private static UserHandle userHandleOf(final int user) {
		final UserHandle current_user = Process.myUserHandle();
		if (user == current_user.hashCode()) return current_user;
		if (SDK_INT >= N) UserHandle.getUserHandleForUid(user * 100000 + 1);
		final Parcel parcel = Parcel.obtain();
		try {
			parcel.writeInt(user);
			parcel.setDataPosition(0);
			return new UserHandle(parcel);
		} finally {
			parcel.recycle();
		}
	}

	private final BroadcastReceiver mPackageEventReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) {
		if (intent.getData() != null && WECHAT_PACKAGE.equals(intent.getData().getSchemeSpecificPart())) mDistinctIdSupported = null;
	}};

	private final BroadcastReceiver mSettingsChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) {
		final Bundle extras = intent.getExtras();
		final Set<String> keys = extras != null ? extras.keySet() : Collections.emptySet();
		if (keys.isEmpty()) return;
		final SharedPreferences.Editor editor = mPreferences.edit();
		for (final String key : keys) editor.putBoolean(key, extras.getBoolean(key)).apply();
		editor.apply();
	}};

	private final ConversationManager mConversationManager = new ConversationManager();
	private MessagingBuilder mMessagingBuilder;
	private boolean mWeChatTargetingO;
	private boolean mUseExtraChannels = true;	// Extra channels should not be used in Insider mode, as WeChat always removes channels not maintained by itself.
	private SharedPreferences mPreferences;
	private String mPrefKeyWear;
	private final Handler mHandler = new Handler();

	static final String TAG = "Nevo.Decorator[WeChat]";
}