package com.oasisfeng.android.content;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import androidx.annotation.Nullable;

/**
 * Make changes of SharedPreferences in one process propagate to all other processes with the same SharedPreferences (only after commit/apply).
 *
 * @author Oasis
 */
public class CrossProcessSharedPreferences {

	private static final long DELAY_BEFORE_PREFS_RELOAD = 500;

	private static final String KActionSharedPrefsUpdated = "com.oasisfeng.android.content.ACTION_SHARED_PREFS_CHANGED";
	private static final String KExtraName = "name";
	private static final String KExtraKey = "key";
	private static final String KExtraPid = "pid";

	/** Cross process version of {@link Context#getSharedPreferences(String, int)} except for the mode is always {@link Context#MODE_PRIVATE} */
	public static SharedPreferences get(final Context context, final String name) {
		//noinspection deprecation
		return get(context, name, Context.MODE_PRIVATE);
	}

	/** @deprecated mode is officially deprecated by Android SDK, use {@link #get(Context, String)} instead. */
	@Deprecated public static SharedPreferences get(final Context context, final String name, final int mode) {
		if (mSingleton == null) synchronized(mLock) {
			if (mSingleton == null) mSingleton = new CrossProcessSharedPreferences(context);
		}
		return mSingleton.getSharedPreferences(context, name, mode);
	}

	/** Cross process version of {@link PreferenceManager#getDefaultSharedPreferences(Context)} */
	public static SharedPreferences getDefault(final Context context) {
		return PreferenceManager.getDefaultSharedPreferences(new ContextWrapper(context) {
			@Override public SharedPreferences getSharedPreferences(final String name, final int mode) {
				return get(context, name);
			}
		});
	}

	private SharedPreferencesWrapper getSharedPreferences(final Context context, final String name, final int mode) {
		final SharedPreferences prefs = context.getSharedPreferences(name, mode);
		if (prefs == null) return null;		// Should not happen, but still check for safety (in case of custom implementation)
		SharedPreferencesWrapper wrapper = mTracked.get(prefs);		// SharedPreferences instance should be singleton.
		if (wrapper != null) return wrapper;

		Log.d(TAG, "Tracking shared preferences: " + name);
		wrapper = new SharedPreferencesWrapper(name, prefs);
		mTracked.put(prefs, wrapper);
		return wrapper;
	}

	private CrossProcessSharedPreferences(final Context context) {
		mAppContext = context.getApplicationContext();
		mAppContext.registerReceiver(mUpdateReceiver, new IntentFilter(KActionSharedPrefsUpdated));
	}

	private final BroadcastReceiver mUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context c, final Intent intent) {
		final int my_pid = Process.myPid();
		final int pid = intent.getIntExtra(KExtraPid, my_pid);
		final String name = intent.getStringExtra(KExtraName);
		final String key = intent.getStringExtra(KExtraKey);
		if (pid == my_pid || TextUtils.isEmpty(name) || TextUtils.isEmpty(key)) return;
		Log.d(TAG, "Shared preferences updated in process " + pid + ": " + name + " (key: " + key + ")");

		// Do actual update in a small delay to workaround the synchronization issue in most time.
		final String handler_token = name + ":" + key;
		mHandler.removeCallbacksAndMessages(handler_token);
		mHandler.postAtTime(new Runnable() { @Override public void run() {
			updateNow(name, key);
		}}, handler_token, SystemClock.uptimeMillis() + DELAY_BEFORE_PREFS_RELOAD);
	}};

	private void updateNow(final String name, final String key) {
		if (mTracked.isEmpty()) return;
		@SuppressWarnings("deprecation")	// Force reload from disk
		final SharedPreferences prefs = mAppContext.getSharedPreferences(name, Context.MODE_MULTI_PROCESS);
		final SharedPreferencesWrapper wrapper = mTracked.get(prefs);
		if (wrapper == null) return;
		wrapper.notifyListeners(key);
	}

	private final Context mAppContext;
	private final Map<SharedPreferences, SharedPreferencesWrapper> mTracked = new HashMap<>();
	private final Handler mHandler = new Handler(Looper.getMainLooper());

	private static @Nullable CrossProcessSharedPreferences mSingleton;
	private static final Object mLock = new Object();

	private static final String TAG = "MPSharedPrefs";

	private class SharedPreferencesWrapper implements SharedPreferences, OnSharedPreferenceChangeListener {

		@Override public void registerOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener listener) {
			synchronized (this) {
				mListeners.put(listener, Boolean.TRUE);
			}
			mDelegate.registerOnSharedPreferenceChangeListener(listener);
		}

		@Override public void unregisterOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener listener) {
			mDelegate.unregisterOnSharedPreferenceChangeListener(listener);
			synchronized (this) {
				mListeners.remove(listener);
			}
		}

		void notifyListeners(final String key) {
			final List<OnSharedPreferenceChangeListener> listeners;
			synchronized (this) {
				listeners = new ArrayList<>(mListeners.keySet());
			}
			for (final OnSharedPreferenceChangeListener listener : listeners) {
				Log.d(CrossProcessSharedPreferences.TAG, "Notify listener: " + listener);
				listener.onSharedPreferenceChanged(this, key);
			}
		}

		SharedPreferencesWrapper(final String name, final SharedPreferences prefs) {
			mName = name;
			mDelegate = prefs;
			prefs.registerOnSharedPreferenceChangeListener(this);
		}

		@TargetApi(Build.VERSION_CODES.HONEYCOMB)
		@Override public Set<String> getStringSet(final String key, final Set<String> defValues) { return mDelegate.getStringSet(key, defValues); }
		@Override public Map<String, ?> getAll() { return mDelegate.getAll(); }
		@Override public String getString(final String key, final String defValue) { return mDelegate.getString(key, defValue); }
		@Override public int getInt(final String key, final int defValue) { return mDelegate.getInt(key, defValue); }
		@Override public long getLong(final String key, final long defValue) { return mDelegate.getLong(key, defValue); }
		@Override public float getFloat(final String key, final float defValue) { return mDelegate.getFloat(key, defValue); }
		@Override public boolean getBoolean(final String key, final boolean defValue) { return mDelegate.getBoolean(key, defValue); }
		@Override public boolean contains(final String key) { return mDelegate.contains(key); }
		@Override public Editor edit() { return mDelegate.edit(); }

		@SuppressLint("ApplySharedPref") @Override public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
			Log.d(TAG, key + " changed in shared preferences " + mName + ", broadcast this change to other processes.");
			sharedPreferences.edit().commit();		// Force commit to avoid reloading ahead of flushing.
			final Intent intent = new Intent(KActionSharedPrefsUpdated).putExtra(KExtraName, mName).putExtra(KExtraKey, key).putExtra(KExtraPid, Process.myPid());
			mAppContext.sendBroadcast(intent);
		}

		private final String mName;
		private final SharedPreferences mDelegate;
		private final Map<OnSharedPreferenceChangeListener, Boolean> mListeners = new WeakHashMap<>();
	}
}