/*
 * Copyright (C) 2011 The Android Open Source 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 android.support.v4.content;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import com.mishiranu.dashchan.content.MainApplication;

/**
 * Helper to register for and send broadcasts of Intents to local objects within your process. This is has a number of
 * advantages over sending global broadcasts with {@link android.content.Context#sendBroadcast}:
 * <ul>
 * <li>You know that the data you are broadcasting won't leave your app, so don't need to worry about leaking private
 * data.
 * <li>It is not possible for other applications to send these broadcasts to your app, so you don't need to worry about
 * having security holes they can exploit.
 * <li>It is more efficient than sending a global broadcast through the system.
 * </ul>
 */
public class LocalBroadcastManager {
	private static class ReceiverRecord {
		final IntentFilter filter;
		final BroadcastReceiver receiver;
		boolean broadcasting;

		ReceiverRecord(IntentFilter _filter, BroadcastReceiver _receiver) {
			filter = _filter;
			receiver = _receiver;
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder(128);
			builder.append("Receiver{");
			builder.append(receiver);
			builder.append(" filter=");
			builder.append(filter);
			builder.append("}");
			return builder.toString();
		}
	}

	private static class BroadcastRecord {
		final Intent intent;
		final ArrayList<ReceiverRecord> receivers;

		BroadcastRecord(Intent _intent, ArrayList<ReceiverRecord> _receivers) {
			intent = _intent;
			receivers = _receivers;
		}
	}

	private final HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers = new HashMap<>();
	private final HashMap<String, ArrayList<ReceiverRecord>> mActions = new HashMap<>();

	private final ArrayList<BroadcastRecord> mPendingBroadcasts = new ArrayList<>();

	static final int MSG_EXEC_PENDING_BROADCASTS = 1;

	private final Handler mHandler;

	private static final LocalBroadcastManager INSTANCE = new LocalBroadcastManager();

	public static LocalBroadcastManager getInstance(Context context) {
		return INSTANCE;
	}

	private LocalBroadcastManager() {
		mHandler = new Handler(Looper.getMainLooper()) {
			@Override
			public void handleMessage(Message msg) {
				switch (msg.what) {
					case MSG_EXEC_PENDING_BROADCASTS:
						executePendingBroadcasts();
						break;
					default:
						super.handleMessage(msg);
				}
			}
		};
	}

	/**
	 * Register a receive for any local broadcasts that match the given IntentFilter.
	 *
	 * @param receiver
	 *            The BroadcastReceiver to handle the broadcast.
	 * @param filter
	 *            Selects the Intent broadcasts to be received.
	 *
	 * @see #unregisterReceiver
	 */
	public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
		synchronized (mReceivers) {
			ReceiverRecord entry = new ReceiverRecord(filter, receiver);
			ArrayList<IntentFilter> filters = mReceivers.get(receiver);
			if (filters == null) {
				filters = new ArrayList<>(1);
				mReceivers.put(receiver, filters);
			}
			filters.add(filter);
			for (int i = 0; i < filter.countActions(); i++) {
				String action = filter.getAction(i);
				ArrayList<ReceiverRecord> entries = mActions.get(action);
				if (entries == null) {
					entries = new ArrayList<>(1);
					mActions.put(action, entries);
				}
				entries.add(entry);
			}
		}
	}

	/**
	 * Unregister a previously registered BroadcastReceiver. <em>All</em> filters that have been registered for this
	 * BroadcastReceiver will be removed.
	 *
	 * @param receiver
	 *            The BroadcastReceiver to unregister.
	 *
	 * @see #registerReceiver
	 */
	public void unregisterReceiver(BroadcastReceiver receiver) {
		synchronized (mReceivers) {
			ArrayList<IntentFilter> filters = mReceivers.remove(receiver);
			if (filters == null) {
				return;
			}
			for (int i = 0; i < filters.size(); i++) {
				IntentFilter filter = filters.get(i);
				for (int j = 0; j < filter.countActions(); j++) {
					String action = filter.getAction(j);
					ArrayList<ReceiverRecord> receivers = mActions.get(action);
					if (receivers != null) {
						for (int k = 0; k < receivers.size(); k++) {
							if (receivers.get(k).receiver == receiver) {
								receivers.remove(k);
								k--;
							}
						}
						if (receivers.size() <= 0) {
							mActions.remove(action);
						}
					}
				}
			}
		}
	}

	/**
	 * Broadcast the given intent to all interested BroadcastReceivers. This call is asynchronous; it returns
	 * immediately, and you will continue executing while the receivers are run.
	 *
	 * @param intent
	 *            The Intent to broadcast; all receivers matching this Intent will receive the broadcast.
	 *
	 * @see #registerReceiver
	 */
	public boolean sendBroadcast(Intent intent) {
		synchronized (mReceivers) {
			final String action = intent.getAction();
			final String type = intent.resolveTypeIfNeeded(MainApplication.getInstance().getContentResolver());
			final Uri data = intent.getData();
			final String scheme = intent.getScheme();
			final Set<String> categories = intent.getCategories();

			ArrayList<ReceiverRecord> entries = mActions.get(intent.getAction());
			if (entries != null) {
				ArrayList<ReceiverRecord> receivers = null;
				for (int i = 0; i < entries.size(); i++) {
					ReceiverRecord receiver = entries.get(i);
					if (receiver.broadcasting) {
						continue;
					}

					int match = receiver.filter.match(action, type, scheme, data, categories, "LocalBroadcastManager");
					if (match >= 0) {
						if (receivers == null) {
							receivers = new ArrayList<>();
						}
						receivers.add(receiver);
						receiver.broadcasting = true;
					}
				}

				if (receivers != null) {
					for (int i = 0; i < receivers.size(); i++) {
						receivers.get(i).broadcasting = false;
					}
					mPendingBroadcasts.add(new BroadcastRecord(intent, receivers));
					if (!mHandler.hasMessages(MSG_EXEC_PENDING_BROADCASTS)) {
						mHandler.sendEmptyMessage(MSG_EXEC_PENDING_BROADCASTS);
					}
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Like {@link #sendBroadcast(Intent)}, but if there are any receivers for the Intent this function will block and
	 * immediately dispatch them before returning.
	 */
	public void sendBroadcastSync(Intent intent) {
		if (sendBroadcast(intent)) {
			executePendingBroadcasts();
		}
	}

	private void executePendingBroadcasts() {
		Context context = MainApplication.getInstance();
		while (true) {
			BroadcastRecord[] brs = null;
			synchronized (mReceivers) {
				final int N = mPendingBroadcasts.size();
				if (N <= 0) {
					return;
				}
				brs = new BroadcastRecord[N];
				mPendingBroadcasts.toArray(brs);
				mPendingBroadcasts.clear();
			}
			for (int i = 0; i < brs.length; i++) {
				BroadcastRecord br = brs[i];
				for (int j = 0; j < br.receivers.size(); j++) {
					br.receivers.get(j).receiver.onReceive(context, br.intent);
				}
			}
		}
	}
}