/*
 * Copyright 2003-2021 The Music Player Daemon Project
 * http://www.musicpd.org
 *
 * 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.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.musicpd;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothClass;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.RemoteViews;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Main extends Service implements Runnable {
	private static final String TAG = "Main";
	private static final String REMOTE_ERROR = "MPD process was killed";
	private static final int MAIN_STATUS_ERROR = -1;
	private static final int MAIN_STATUS_STOPPED = 0;
	private static final int MAIN_STATUS_STARTED = 1;

	private static final int MSG_SEND_STATUS = 0;
	private static final int MSG_SEND_LOG = 1;

	private Thread mThread = null;
	private int mStatus = MAIN_STATUS_STOPPED;
	private boolean mAbort = false;
	private String mError = null;
	private final RemoteCallbackList<IMainCallback> mCallbacks = new RemoteCallbackList<IMainCallback>();
	private final IBinder mBinder = new MainStub(this);
	private boolean mPauseOnHeadphonesDisconnect = false;
	private PowerManager.WakeLock mWakelock = null;

	static class MainStub extends IMain.Stub {
		private Main mService;
		MainStub(Main service) {
			mService = service;
		}
		public void start() {
			mService.start();
		}
		public void stop() {
			mService.stop();
		}
		public void setPauseOnHeadphonesDisconnect(boolean enabled) {
			mService.setPauseOnHeadphonesDisconnect(enabled);
		}
		public void setWakelockEnabled(boolean enabled) {
			mService.setWakelockEnabled(enabled);
		}
		public boolean isRunning() {
			return mService.isRunning();
		}
		public void registerCallback(IMainCallback cb) {
			mService.registerCallback(cb);
		}
		public void unregisterCallback(IMainCallback cb) {
			mService.unregisterCallback(cb);
		}
	}

	private synchronized void sendMessage(int what, int arg1, int arg2, Object obj) {
		int i = mCallbacks.beginBroadcast();
		while (i > 0) {
			i--;
			final IMainCallback cb = mCallbacks.getBroadcastItem(i);
			try {
				switch (what) {
				case MSG_SEND_STATUS:
					switch (arg1) {
					case MAIN_STATUS_ERROR:
						cb.onError((String)obj);
						break;
					case MAIN_STATUS_STOPPED:
						cb.onStopped();
						break;
					case MAIN_STATUS_STARTED:
						cb.onStarted();
						break;
					}
					break;
				case MSG_SEND_LOG:
					cb.onLog(arg1, (String) obj);
					break;
				}
			} catch (RemoteException e) {
			}
		}
		mCallbacks.finishBroadcast();
	}

	private Bridge.LogListener mLogListener = new Bridge.LogListener() {
		@Override
		public void onLog(int priority, String msg) {
			sendMessage(MSG_SEND_LOG, priority, 0, msg);
		}
	};

	@Override
	public IBinder onBind(Intent intent) {
		return mBinder;
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		start();
		if (intent != null && intent.getBooleanExtra("wakelock", false))
			setWakelockEnabled(true);
		return START_STICKY;
	}

	@Override
	public void run() {
		if (!Loader.loaded) {
			final String error = "Failed to load the native MPD libary.\n" +
				"Report this problem to us, and include the following information:\n" +
				"SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" +
				"PRODUCT=" + Build.PRODUCT + "\n" +
				"FINGERPRINT=" + Build.FINGERPRINT + "\n" +
				"error=" + Loader.error;
			setStatus(MAIN_STATUS_ERROR, error);
			stopSelf();
			return;
		}
		synchronized (this) {
			if (mAbort)
				return;
			setStatus(MAIN_STATUS_STARTED, null);
		}
		Bridge.run(this, mLogListener);
		setStatus(MAIN_STATUS_STOPPED, null);
	}

	private synchronized void setStatus(int status, String error) {
		mStatus = status;
		mError = error;
		sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
	}

	private Notification.Builder createNotificationBuilderWithChannel() {
		final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
		if (notificationManager == null)
			return null;

		final String id = "org.musicpd";
		final String name = "MPD service";
		final int importance = 3; /* NotificationManager.IMPORTANCE_DEFAULT */

		try {
			Class<?> ncClass = Class.forName("android.app.NotificationChannel");
			Constructor<?> ncCtor = ncClass.getConstructor(String.class, CharSequence.class, int.class);
			Object nc = ncCtor.newInstance(id, name, importance);

			Method nmCreateNotificationChannelMethod =
				NotificationManager.class.getMethod("createNotificationChannel", ncClass);
			nmCreateNotificationChannelMethod.invoke(notificationManager, nc);

			Constructor nbCtor = Notification.Builder.class.getConstructor(Context.class, String.class);
			return (Notification.Builder) nbCtor.newInstance(this, id);
		} catch (Exception e)
		{
			Log.e(TAG, "error creating the NotificationChannel", e);
			return null;
		}
	}

	private void start() {
		if (mThread != null)
			return;

		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_HEADSET_PLUG);
		filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
		filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
		registerReceiver(new BroadcastReceiver() {
			@Override
			public void onReceive(Context context, Intent intent) {
				if (!mPauseOnHeadphonesDisconnect) {
					return;
				}

				if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
					if (intent.hasExtra("state") && intent.getIntExtra("state", 0) == 0)
						pause();
				} else {
					BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
					if (device.getBluetoothClass().hasService(BluetoothClass.Service.AUDIO))
						pause();
				}
			}
		}, filter);

		final Intent mainIntent = new Intent(this, Settings.class);
		mainIntent.setAction("android.intent.action.MAIN");
		mainIntent.addCategory("android.intent.category.LAUNCHER");
		final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
				mainIntent, PendingIntent.FLAG_CANCEL_CURRENT);

		Notification.Builder nBuilder;
		if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */)
		{
			nBuilder = createNotificationBuilderWithChannel();
			if (nBuilder == null)
				return;
		}
		else
			nBuilder = new Notification.Builder(this);

		Notification notification = nBuilder.setContentTitle(getText(R.string.notification_title_mpd_running))
			.setContentText(getText(R.string.notification_text_mpd_running))
			.setSmallIcon(R.drawable.notification_icon)
			.setContentIntent(contentIntent)
			.build();

		mThread = new Thread(this);
		mThread.start();

		startForeground(R.string.notification_title_mpd_running, notification);
		startService(new Intent(this, Main.class));
	}

	private void stop() {
		if (mThread != null) {
			if (mThread.isAlive()) {
				synchronized (this) {
					if (mStatus == MAIN_STATUS_STARTED)
						Bridge.shutdown();
					else
						mAbort = true;
				}
			}
			try {
				mThread.join();
				mThread = null;
				mAbort = false;
			} catch (InterruptedException ie) {}
		}
		setWakelockEnabled(false);
		stopForeground(true);
		stopSelf();
	}

	private void pause() {
		if (mThread != null) {
			if (mThread.isAlive()) {
				synchronized (this) {
					if (mStatus == MAIN_STATUS_STARTED)
						Bridge.pause();
				}
			}
		}
	}

	private void setPauseOnHeadphonesDisconnect(boolean enabled) {
		mPauseOnHeadphonesDisconnect = enabled;
	}

	private void setWakelockEnabled(boolean enabled) {
		if (enabled && mWakelock == null) {
			PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
			mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
			mWakelock.acquire();
			Log.d(TAG, "Wakelock acquired");
		} else if (!enabled && mWakelock != null) {
			mWakelock.release();
			mWakelock = null;
			Log.d(TAG, "Wakelock released");
		}
	}

	private boolean isRunning() {
		return mThread != null && mThread.isAlive();
	}

	private void registerCallback(IMainCallback cb) {
		if (cb != null) {
			mCallbacks.register(cb);
			sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
		}
	}

	private void unregisterCallback(IMainCallback cb) {
		if (cb != null) {
			mCallbacks.unregister(cb);
		}
	}

	/*
	 * Client that bind the Main Service in order to send commands and receive callback
	 */
	public static class Client {

		public interface Callback {
			public void onStarted();
			public void onStopped();
			public void onError(String error);
			public void onLog(int priority, String msg);
		}

		private boolean mBound = false;
		private final Context mContext;
		private Callback mCallback;
		private IMain mIMain = null;

		private final IMainCallback.Stub mICallback = new IMainCallback.Stub() {

			@Override
			public void onStopped() throws RemoteException {
				mCallback.onStopped();
			}

			@Override
			public void onStarted() throws RemoteException {
				mCallback.onStarted();
			}

			@Override
			public void onError(String error) throws RemoteException {
				mCallback.onError(error);
			}

			@Override
			public void onLog(int priority, String msg) throws RemoteException {
				mCallback.onLog(priority, msg);
			}
		};

		private final ServiceConnection mServiceConnection = new ServiceConnection() {

			@Override
			public void onServiceConnected(ComponentName name, IBinder service) {
				synchronized (this) {
					mIMain = IMain.Stub.asInterface(service);
					try {
						if (mCallback != null)
							mIMain.registerCallback(mICallback);
					} catch (RemoteException e) {
						if (mCallback != null)
							mCallback.onError(REMOTE_ERROR);
					}
				}
			}

			@Override
			public void onServiceDisconnected(ComponentName name) {
				if (mCallback != null)
					mCallback.onError(REMOTE_ERROR);
			}
		};

		public Client(Context context, Callback cb) throws IllegalArgumentException {
			if (context == null)
				throw new IllegalArgumentException("Context can't be null");
			mContext = context;
			mCallback = cb;
			mBound = mContext.bindService(new Intent(mContext, Main.class), mServiceConnection, Context.BIND_AUTO_CREATE);
		}

		public boolean start() {
			synchronized (this) {
				if (mIMain != null) {
					try {
						mIMain.start();
						return true;
					} catch (RemoteException e) {
					}
				}
				return false;
			}
		}

		public boolean stop() {
			synchronized (this) {
				if (mIMain != null) {
					try {
						mIMain.stop();
						return true;
					} catch (RemoteException e) {
					}
				}
				return false;
			}
		}

		public boolean setPauseOnHeadphonesDisconnect(boolean enabled) {
			synchronized (this) {
				if (mIMain != null) {
					try {
						mIMain.setPauseOnHeadphonesDisconnect(enabled);
						return true;
					} catch (RemoteException e) {
					}
				}
				return false;
			}
		}

		public boolean setWakelockEnabled(boolean enabled) {
			synchronized (this) {
				if (mIMain != null) {
					try {
						mIMain.setWakelockEnabled(enabled);
						return true;
					} catch (RemoteException e) {
					}
				}
				return false;
			}
		}

		public boolean isRunning() {
			synchronized (this) {
				if (mIMain != null) {
					try {
						return mIMain.isRunning();
					} catch (RemoteException e) {
					}
				}
				return false;
			}
		}

		public void release() {
			if (mBound) {
				synchronized (this) {
					if (mIMain != null && mICallback != null) {
						try {
							if (mCallback != null)
								mIMain.unregisterCallback(mICallback);
						} catch (RemoteException e) {
						}
					}
				}
				mBound = false;
				mContext.unbindService(mServiceConnection);
			}
		}
	}

	/*
	 * start Main service without any callback
	 */
	public static void start(Context context, boolean wakelock) {
		Intent intent = new Intent(context, Main.class)
			.putExtra("wakelock", wakelock);

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
			/* in Android 8+, we need to use this method
			   or else we'll get "IllegalStateException:
			   app is in background" */
			context.startForegroundService(intent);
		else
			context.startService(intent);
	}
}
