/*
 * Copyright (C) 2016 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 com.android.internal.util;

import android.annotation.Nullable;
import android.content.Intent;
import android.os.Bundle;
import android.os.IProgressListener;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.MathUtils;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

/**
 * Tracks and reports progress of a single task to a {@link IProgressListener}.
 * The reported progress of a task ranges from 0-100, but the task can be
 * segmented into smaller pieces using {@link #startSegment(int)} and
 * {@link #endSegment(int[])}, and segments can be nested.
 * <p>
 * Here's an example in action; when finished the overall task progress will be
 * at 60.
 *
 * <pre>
 * prog.setProgress(20);
 * {
 *     final int restore = prog.startSegment(40);
 *     for (int i = 0; i < N; i++) {
 *         prog.setProgress(i, N);
 *         ...
 *     }
 *     prog.endSegment(restore);
 * }
 * </pre>
 *
 * @hide
 */
public class ProgressReporter {
    private static final int STATE_INIT = 0;
    private static final int STATE_STARTED = 1;
    private static final int STATE_FINISHED = 2;

    private final int mId;

    @GuardedBy("this")
    private final RemoteCallbackList<IProgressListener> mListeners = new RemoteCallbackList<>();

    @GuardedBy("this")
    private int mState = STATE_INIT;
    @GuardedBy("this")
    private int mProgress = 0;
    @GuardedBy("this")
    private Bundle mExtras = new Bundle();

    /**
     * Current segment range: first element is starting progress of this
     * segment, second element is length of segment.
     */
    @GuardedBy("this")
    private int[] mSegmentRange = new int[] { 0, 100 };

    /**
     * Create a new task with the given identifier whose progress will be
     * reported to the given listener.
     */
    public ProgressReporter(int id) {
        mId = id;
    }

    /**
     * Add given listener to watch for progress events. The current state will
     * be immediately dispatched to the given listener.
     */
    public void addListener(@Nullable IProgressListener listener) {
        if (listener == null) return;
        synchronized (this) {
            mListeners.register(listener);
            switch (mState) {
                case STATE_INIT:
                    // Nothing has happened yet
                    break;
                case STATE_STARTED:
                    try {
                        listener.onStarted(mId, null);
                        listener.onProgress(mId, mProgress, mExtras);
                    } catch (RemoteException ignored) {
                    }
                    break;
                case STATE_FINISHED:
                    try {
                        listener.onFinished(mId, null);
                    } catch (RemoteException ignored) {
                    }
                    break;
            }
        }
    }

    /**
     * Set the progress of the currently active segment.
     *
     * @param progress Segment progress between 0-100.
     */
    public void setProgress(int progress) {
        setProgress(progress, 100, null);
    }

    /**
     * Set the progress of the currently active segment.
     *
     * @param progress Segment progress between 0-100.
     */
    public void setProgress(int progress, @Nullable CharSequence title) {
        setProgress(progress, 100, title);
    }

    /**
     * Set the fractional progress of the currently active segment.
     */
    public void setProgress(int n, int m) {
        setProgress(n, m, null);
    }

    /**
     * Set the fractional progress of the currently active segment.
     */
    public void setProgress(int n, int m, @Nullable CharSequence title) {
        synchronized (this) {
            if (mState != STATE_STARTED) {
                throw new IllegalStateException("Must be started to change progress");
            }
            mProgress = mSegmentRange[0]
                    + MathUtils.constrain((n * mSegmentRange[1]) / m, 0, mSegmentRange[1]);
            if (title != null) {
                mExtras.putCharSequence(Intent.EXTRA_TITLE, title);
            }
            notifyProgress(mId, mProgress, mExtras);
        }
    }

    /**
     * Start a new inner segment that will contribute the given range towards
     * the currently active segment. You must pass the returned value to
     * {@link #endSegment(int[])} when finished.
     */
    public int[] startSegment(int size) {
        synchronized (this) {
            final int[] lastRange = mSegmentRange;
            mSegmentRange = new int[] { mProgress, (size * mSegmentRange[1] / 100) };
            return lastRange;
        }
    }

    /**
     * End the current segment.
     */
    public void endSegment(int[] lastRange) {
        synchronized (this) {
            mProgress = mSegmentRange[0] + mSegmentRange[1];
            mSegmentRange = lastRange;
        }
    }

    @VisibleForTesting
    public int getProgress() {
        return mProgress;
    }

    int[] getSegmentRange() {
        return mSegmentRange;
    }

    /**
     * Report this entire task as being started.
     */
    public void start() {
        synchronized (this) {
            mState = STATE_STARTED;
            notifyStarted(mId, null);
            notifyProgress(mId, mProgress, mExtras);
        }
    }

    /**
     * Report this entire task as being finished.
     */
    public void finish() {
        synchronized (this) {
            mState = STATE_FINISHED;
            notifyFinished(mId, null);
            mListeners.kill();
        }
    }

    private void notifyStarted(int id, Bundle extras) {
        for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) {
            try {
                mListeners.getBroadcastItem(i).onStarted(id, extras);
            } catch (RemoteException ignored) {
            }
        }
        mListeners.finishBroadcast();
    }

    private void notifyProgress(int id, int progress, Bundle extras) {
        for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) {
            try {
                mListeners.getBroadcastItem(i).onProgress(id, progress, extras);
            } catch (RemoteException ignored) {
            }
        }
        mListeners.finishBroadcast();
    }

    private void notifyFinished(int id, Bundle extras) {
        for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) {
            try {
                mListeners.getBroadcastItem(i).onFinished(id, extras);
            } catch (RemoteException ignored) {
            }
        }
        mListeners.finishBroadcast();
    }
}
