package com.dougkeen.bart.services;

import java.lang.ref.WeakReference;
import java.util.List;

import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;

import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.services.EtdService.EtdServiceBinder;
import com.dougkeen.bart.services.EtdService.EtdServiceListener;
import com.dougkeen.util.Observer;

public class BoardedDepartureService extends Service implements
		EtdServiceListener {

	private static final int DEPARTURE_NOTIFICATION_ID = 123;

	private volatile Looper mServiceLooper;
	private volatile ServiceHandler mServiceHandler;

	private boolean mBound = false;
	private EtdService mEtdService;
	private StationPair mStationPair;
	private NotificationManager mNotificationManager;
	private AlarmManager mAlarmManager;
	private Handler mHandler;
	private boolean mHasShutDown = false;

	public BoardedDepartureService() {
		super();
	}

	private static final class ServiceHandler extends Handler {
		private final WeakReference<BoardedDepartureService> mServiceRef;

		public ServiceHandler(Looper looper,
				BoardedDepartureService boardedDepartureService) {
			super(looper);
			mServiceRef = new WeakReference<BoardedDepartureService>(
					boardedDepartureService);
		}

		@Override
		public void handleMessage(Message msg) {
			BoardedDepartureService service = mServiceRef.get();
			if (service != null) {
				service.onHandleIntent((Intent) msg.obj);
			}
		}
	}

	private final ServiceConnection mConnection = new ServiceConnection() {
		@Override
		public void onServiceDisconnected(ComponentName name) {
			mEtdService = null;
			mBound = false;
		}

		@Override
		public void onServiceConnected(ComponentName name, IBinder service) {
			mEtdService = ((EtdServiceBinder) service).getService();
			if (getStationPair() != null) {
				mEtdService.registerListener(BoardedDepartureService.this,
						false);
			}
			mBound = true;
		}
	};

	@Override
	public void onCreate() {
		HandlerThread thread = new HandlerThread(
				"BartRunnerNotificationService");
		thread.start();

		mServiceLooper = thread.getLooper();
		mServiceHandler = new ServiceHandler(mServiceLooper, this);

		bindService(EtdService_.intent(this).get(), mConnection,
				Context.BIND_AUTO_CREATE);
		mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
		mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
		mHandler = new Handler();
		super.onCreate();
	}

	@Override
	public void onStart(Intent intent, int startId) {
		Message msg = mServiceHandler.obtainMessage();
		msg.arg1 = startId;
		msg.obj = intent;
		mServiceHandler.sendMessage(msg);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		mHasShutDown = false;
		onStart(intent, startId);
		return START_REDELIVER_INTENT;
	}

	@Override
	public void onDestroy() {
		shutDown(true);
		if (mBound)
			unbindService(mConnection);
		mServiceLooper.quit();
		super.onDestroy();
	}

	protected void onHandleIntent(Intent intent) {
		if (intent == null)
			return;

		final BartRunnerApplication application = (BartRunnerApplication) getApplication();
		final Departure boardedDeparture;
		if (intent.hasExtra("departure")) {
			boardedDeparture = intent.getExtras().getParcelable("departure");
		} else {
			boardedDeparture = application.getBoardedDeparture();
		}
		if (boardedDeparture == null) {
			// Nothing to notify about
			if (mNotificationManager != null) {
				mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID);
			}
			return;
		}
		if (intent.getBooleanExtra("cancelNotifications", false)
				|| intent.getBooleanExtra("clearBoardedDeparture", false)) {
			// We want to cancel the alarm
			boardedDeparture
					.cancelAlarm(getApplicationContext(), mAlarmManager);
			if (intent.getBooleanExtra("clearBoardedDeparture", false)) {
				application.setBoardedDeparture(null);
				shutDown(false);
			} else {
				updateNotification();
			}
			return;
		}

		StationPair oldStationPair = mStationPair;
		mStationPair = boardedDeparture.getStationPair();

		if (mEtdService != null && mStationPair != null
				&& !mStationPair.equals(oldStationPair)) {
			mEtdService.unregisterListener(this);
		}

		if (getStationPair() != null && mEtdService != null) {
			mEtdService.registerListener(this, false);
		}

		boardedDeparture.getAlarmLeadTimeMinutesObservable().registerObserver(
				new Observer<Integer>() {
					@Override
					public void onUpdate(Integer newValue) {
						updateNotification();
					}
				});
		boardedDeparture.getAlarmPendingObservable().registerObserver(
				new Observer<Boolean>() {
					@Override
					public void onUpdate(Boolean newValue) {
						updateNotification();
					}
				});

		updateNotification();

		pollDepartureStatus();
	}

	private void updateAlarm() {
		Departure boardedDeparture = ((BartRunnerApplication) getApplication())
				.getBoardedDeparture();
		if (boardedDeparture != null) {
			boardedDeparture
					.updateAlarm(getApplicationContext(), mAlarmManager);
		}
	}

	@Override
	public void onETDChanged(List<Departure> departures) {
		final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
				.getBoardedDeparture();
		for (Departure departure : departures) {
			if (departure.equals(boardedDeparture)
					&& (boardedDeparture.getMeanSecondsLeft() != departure
							.getMeanSecondsLeft() || boardedDeparture
							.getUncertaintySeconds() != departure
							.getUncertaintySeconds())) {
				boardedDeparture.mergeEstimate(departure);
				// Also merge back, in case boardedDeparture estimate is better
				departure.mergeEstimate(boardedDeparture);

				updateAlarm();
				break;
			}
		}
	}

	@Override
	public void onError(String errorMessage) {
		// Do nothing
	}

	@Override
	public void onRequestStarted() {
		// Do nothing
	}

	@Override
	public void onRequestEnded() {
		// Do nothing
	}

	@Override
	public StationPair getStationPair() {
		return mStationPair;
	}

	private long mNextScheduledCheckClockTime = 0;

	private void pollDepartureStatus() {
		final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
				.getBoardedDeparture();

		if (boardedDeparture == null || boardedDeparture.hasDeparted()) {
			shutDown(false);
			return;
		}

		if (mEtdService != null) {
			/*
			 * Make sure we're still listening for ETD changes (in case weak ref
			 * was garbage collected). Not a huge fan of this approach, but I
			 * think I'd rather keep the weak references to avoid memory leaks
			 * than move to soft references or some other form of stronger
			 * reference. Besides, registerListener() should only result in a
			 * few constant-time map operations, so there shouldn't be a big
			 * performance hit.
			 */
			mEtdService.registerListener(this, false);
		}

		boardedDeparture.updateAlarm(getApplicationContext(), mAlarmManager);

		updateNotification();

		final int pollIntervalMillis = getPollIntervalMillis();
		final long scheduledCheckClockTime = System.currentTimeMillis()
				+ pollIntervalMillis;
		if (mNextScheduledCheckClockTime < scheduledCheckClockTime) {
			mHandler.postDelayed(new Runnable() {
				@Override
				public void run() {
					pollDepartureStatus();
				}
			}, pollIntervalMillis);
			mNextScheduledCheckClockTime = scheduledCheckClockTime;
		}
	}

	private void shutDown(boolean isBeingDestroyed) {
		if (!mHasShutDown) {
			mHasShutDown = true;
			if (mEtdService != null) {
				mEtdService.unregisterListener(this);
			}
			if (mNotificationManager != null) {
				mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID);
			}
			if (!isBeingDestroyed)
				stopSelf();
		}
	}

	private void updateNotification() {
		if (mHasShutDown) {
			if (mEtdService != null) {
				mEtdService.unregisterListener(this);
			}
			return;
		}

		final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
				.getBoardedDeparture();
		if (boardedDeparture != null) {
			mNotificationManager.notify(DEPARTURE_NOTIFICATION_ID,
					boardedDeparture
							.createNotification(getApplicationContext()));
		}
	}

	private int getPollIntervalMillis() {
		final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
				.getBoardedDeparture();

		if (boardedDeparture.getSecondsUntilAlarm() > 3 * 60) {
			return 15 * 1000;
		} else {
			return 6 * 1000;
		}
	}

	@Override
	public IBinder onBind(Intent intent) {
		// Doesn't support binding
		return null;
	}

}
