/*
 * Copyright 2017 Phillip Hsu
 *
 * This file is part of ClockPlus.
 *
 * ClockPlus 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 3 of the License, or
 * (at your option) any later version.
 *
 * ClockPlus 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 ClockPlus.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.philliphsu.clock2.timers;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.SimpleArrayMap;
import android.util.Log;

import com.philliphsu.clock2.MainActivity;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.chronometer.ChronometerNotificationService;
import com.philliphsu.clock2.timers.data.AsyncTimersTableUpdateHandler;
import com.philliphsu.clock2.timers.data.TimerCursor;
import com.philliphsu.clock2.util.ContentIntentUtils;
import com.philliphsu.clock2.util.ParcelableUtil;

/**
 * Handles the notification for an active Timer.
 * TOneverDO: extend IntentService, it is ill-suited for our requirement that
 * this remains alive until we explicitly stop it. Otherwise, it would finish
 * a single task and immediately destroy itself, which means we lose all of
 * our instance state.
 */
public class TimerNotificationService extends ChronometerNotificationService {
    private static final String TAG = "TimerNotifService";

    private static final String ACTION_CANCEL_NOTIFICATION = "com.philliphsu.clock2.timers.action.CANCEL_NOTIFICATION";
    public static final String ACTION_ADD_ONE_MINUTE = "com.philliphsu.clock2.timers.action.ADD_ONE_MINUTE";

    public static final String EXTRA_TIMER = "com.philliphsu.clock2.timers.extra.TIMER";
    private static final String EXTRA_CANCEL_TIMER_ID = "com.philliphsu.clock2.timers.extra.CANCEL_TIMER_ID";

    private AsyncTimersTableUpdateHandler mUpdateHandler;
    private final SimpleArrayMap<Long, Timer> mTimers = new SimpleArrayMap<>();
    private final SimpleArrayMap<Long, TimerController> mControllers = new SimpleArrayMap<>();

    private long mMostRecentTimerId;

    /**
     * Helper method to start this Service for its default action: to show
     * the notification for the Timer with the given id.
     */
    public static void showNotification(Context context, Timer timer) {
        Intent intent = new Intent(context, TimerNotificationService.class);
        intent.putExtra(EXTRA_TIMER, ParcelableUtil.marshall(timer));
        context.startService(intent);
    }

    /**
     * Helper method to cancel the notification previously shown from calling
     * {@link #showNotification(Context, Timer)}. This does NOT start the Service
     * and call through to {@link #onStartCommand(Intent, int, int)}, because
     * the work does not require so.
     * @param timerId the id of the Timer associated with the notification
     *                you want to cancel
     */
    public static void cancelNotification(Context context, long timerId) {
        Intent intent = new Intent(context, TimerNotificationService.class)
                .setAction(ACTION_CANCEL_NOTIFICATION)
                .putExtra(EXTRA_CANCEL_TIMER_ID, timerId);
        context.startService(intent);
    }

    @Override
    protected int getSmallIcon() {
        return R.drawable.ic_timer_24dp;
    }

    @Nullable
    @Override
    protected PendingIntent getContentIntent() {
        // The base class won't call this for us because this is not a foreground service,
        // as we require multiple notifications created as needed. Instead, this is called after
        // we call registerNewNoteBuilder() in handleDefaultAction().
        // Before we called registerNewNoteBuilder(), we saved a reference to the most recent timer id.
        return ContentIntentUtils.create(this, MainActivity.PAGE_TIMERS, mMostRecentTimerId);
    }

    @Override
    protected boolean isCountDown() {
        return true;
    }

    @Override
    protected int getNoteId() {
        // Since isForeground() returns false, this won't be called by the base class.
        return 0;
    }

    @Override
    protected String getNoteTag() {
        // This is so we can cancel notifications in our static helper method
        // cancelNotification(Context, long) with the static TAG constant
        return TAG;
    }

    @Override
    protected boolean isForeground() {
        // We're going to post a separate notification for each Timer.
        // Foreground services are limited to one notification.
        return false;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mUpdateHandler = new AsyncTimersTableUpdateHandler(this, null);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // After being cancelled due to time being up, sometimes the active timer notification posts again
        // with a static 00:00 text, along with the Time's up notification. My theory is
        // our thread has enough leeway to sneak in a final call to post the notification before it
        // is actually quit().
        // As such, try cancelling the notification with this (tag, id) pair again.
        for (int i = 0; i < mTimers.size(); i++) {
            cancelNotification(mTimers.keyAt(i));
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            Log.d(TAG, "Recreated service, starting chronometer again.");
            // Restore all running timers. This restores all of the base
            // class's member state as well, due to the various API
            // calls required.
            new Thread(new Runnable() {
                @Override
                public void run() {
                    TimerCursor cursor = mUpdateHandler.getTableManager().queryStartedTimers();
                    while (cursor.moveToNext()) {
                        // We actually don't need any args since this will be
                        // passed directly to our handler method. If we were going
                        // to startService() with this, then we would need to
                        // specify them.
                        Intent intent = new Intent(
                                /*TimerNotificationService.this,
                                TimerNotificationService.class*/);
                        final Timer timer = cursor.getItem();
                        intent.putExtra(EXTRA_TIMER, ParcelableUtil.marshall(timer));
                        // TODO: Should we startService() instead?
                        handleDefaultAction(intent, 0, 0);
                    }
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    protected void handleDefaultAction(Intent intent, int flags, int startId) {
        final byte[] bytes = intent.getByteArrayExtra(EXTRA_TIMER);
        if (bytes == null) {
            throw new IllegalStateException("Cannot start TimerNotificationService without a Timer");
        }
        final Timer timer = ParcelableUtil.unmarshall(bytes, Timer.CREATOR);
        final long id = timer.getId();
        boolean updateChronometer = false;
        Timer oldTimer = mTimers.put(id, timer);
        if (oldTimer != null) {
            updateChronometer = oldTimer.endTime() != timer.endTime();
        }
        mControllers.put(id, new TimerController(timer, mUpdateHandler));
        mMostRecentTimerId = id;
        // If isUpdate == true, this won't call through because the id already exists in the
        // internal mappings as well.
        registerNewNoteBuilder(id);
        // The note's title should change here every time, especially if the Timer's label was updated.
        String title = timer.label();
        if (title.isEmpty()) {
            title = getString(R.string.timer);
        }
        setContentTitle(id, title);
        if (updateChronometer) {
            // Immediately push any duration updates, or else there will be a noticeable delay.
            setBase(id, timer.endTime());
            updateNotification(id, true);
        }
        // This handles any other notification updates like the title or actions, even if
        // the timer is not running because the current thread will update the notification
        // (besides the content text) before quitting.
        syncNotificationWithTimerState(id, timer.isRunning());
    }

    @Override
    protected void handleStartPauseAction(Intent intent, int flags, int startId) {
        long id = getActionId(intent);
        mControllers.get(id).startPause();
        syncNotificationWithTimerState(id, mTimers.get(id).isRunning());
    }

    @Override
    protected void handleStopAction(Intent intent, int flags, int startId) {
        long id = getActionId(intent);
        mControllers.get(id).stop();
        // We leave removing the notification up to AsyncTimersTableUpdateHandler
        // when it calls cancelAlarm() from onPostAsyncUpdate().
        // This calls the static helper cancelNotification(), which
        // starts this service to handle ACTION_CANCEL_NOTIFICATION.
    }

    @Override
    protected void handleAction(@NonNull String action, Intent intent, int flags, int startId) {
        if (ACTION_ADD_ONE_MINUTE.equals(action)) {
            // While the notification's countdown would automatically be extended by one minute,
            // there is a noticeable delay before the minute gets added on.
            // Update the text immediately, because there's no harm in doing so.
            long id = getActionId(intent);
            final Timer orig = mTimers.get(id);
            // We have to add the minute to a copy Timer. If we had modified the original Timer,
            // then the controller would also add another minute to the same instance, and what
            // would happen after the DB update is the Timer instance with this ID ends up
            // being extended by 2 minutes.
            //
            // Why not just do something like: `t.endTime() + 60000`? Because extending timers that
            // are paused is a special case, and Timer.addOneMinute() already has the logic to handle
            // that.
            Timer copy = Timer.create(orig.hour(), orig.minute(), orig.second(), orig.group(), orig.label());
            orig.copyMutableFieldsTo(copy);
            copy.addOneMinute();
            setBase(id, copy.endTime());
            updateNotification(id, true);
            mControllers.get(id).addOneMinute();
        } else if (ACTION_CANCEL_NOTIFICATION.equals(action)) {
            long id = intent.getLongExtra(EXTRA_CANCEL_TIMER_ID, -1);
            cancelNotification(id);
            // TODO: SHould this be before cancelNotification()? I'm worried
            // that the thread's handler will have enough leeway to sneak
            // in a notification update before it is quit. If it did,
            // then at least cancelNotification should theoretically
            // remove it...
            releaseResources(id);
        } else {
            throw new IllegalArgumentException("TimerNotificationService cannot handle action " + action);
        }
    }

    @Override
    protected void releaseResources(long id) {
        super.releaseResources(id);
        mTimers.remove(id);
        mControllers.remove(id);
        // TODO: Should we make a private method?
        // This private method would first call releaseResources(),
        // and then this block.
        if (mTimers.isEmpty()) { // We could check any map, since they should all have the same sizes
            stopSelf();
        }
    }

    private void syncNotificationWithTimerState(long id, boolean running) {
        // The actions from the last time we configured the Builder are still here.
        // We have to retain the relative ordering of the actions while updating
        // just the start/pause action, so clear them and set them again.
        clearActions(id);
        addAction(ACTION_ADD_ONE_MINUTE, R.drawable.ic_add_24dp, getString(R.string.minute), id);
        addStartPauseAction(running, id);
        addStopAction(id);

        quitCurrentThread(id);
        if (running) {
            startNewThread(id, mTimers.get(id).endTime());
        }
    }
    
    private long getActionId(Intent intent) {
        return intent.getLongExtra(EXTRA_ACTION_ID, -1);
    }
}