/*
 * Copyright 2014, 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.server.telecom;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.Ringtone;
import android.media.VolumeShaper;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
 * used from the main thread.
 */
@VisibleForTesting
public class AsyncRingtonePlayer {
    // Maximum amount of time we will delay playing a ringtone while waiting for audio routing to
    // be ready.
    private static final int PLAY_DELAY_TIMEOUT_MS = 1000;
    // Message codes used with the ringtone thread.
    private static final int EVENT_PLAY = 1;
    private static final int EVENT_STOP = 2;

    /** Handler running on the ringtone thread. */
    private Handler mHandler;

    /** The current ringtone. Only used by the ringtone thread. */
    private Ringtone mRingtone;

    /**
     * Set to true if we are setting up to play or are currently playing. False if we are stopping
     * or have stopped playing.
     */
    private boolean mIsPlaying = false;

    /**
     * Set to true if BT HFP is active and audio is connected.
     */
    private boolean mIsBtActive = false;

    /**
     * A list of pending ringing ready latches, which are used to delay the ringing command until
     * audio paths are set and ringing is ready.
     */
    private final ArrayList<CountDownLatch> mPendingRingingLatches = new ArrayList<>();

    public AsyncRingtonePlayer() {
        // Empty
    }

    /**
     * Plays the appropriate ringtone for the specified call.
     * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
     * the volume of the ringtone as it plays.
     *
     * @param ringtoneSupplier The {@link Ringtone} factory.
     * @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
     * @param isHfpDeviceConnected True if there is a HFP BT device connected, false otherwise.
     */
    public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
            BiConsumer<Ringtone, Boolean> ringtoneConsumer,  boolean isHfpDeviceConnected) {
        Log.d(this, "Posting play.");
        mIsPlaying = true;
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = ringtoneSupplier;
        args.arg2 = ringtoneConsumer;
        args.arg3 = Log.createSubsession();
        args.arg4 = prepareRingingReadyLatch(isHfpDeviceConnected);
        postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
    }

    /** Stops playing the ringtone. */
    public void stop() {
        Log.d(this, "Posting stop.");
        mIsPlaying = false;
        postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
        // Clear any pending ringing latches so that we do not have to wait for its timeout to pass
        // before calling stop.
        clearPendingRingingLatches();
    }

    /**
     * Called when the BT HFP profile active state changes.
     * @param isBtActive A BT device is connected and audio is active.
     */
    public void updateBtActiveState(boolean isBtActive) {
        Log.i(this, "updateBtActiveState: " + isBtActive);
        synchronized (mPendingRingingLatches) {
            mIsBtActive = isBtActive;
            if (isBtActive) mPendingRingingLatches.forEach(CountDownLatch::countDown);
        }
    }

    /**
     * Prepares a new ringing ready latch and tracks it in a list. Once the ready latch has been
     * used, {@link #removePendingRingingReadyLatch(CountDownLatch)} must be called on this latch.
     * @param isHfpDeviceConnected true if there is a HFP device connected.
     * @return the newly prepared CountDownLatch
     */
    private CountDownLatch prepareRingingReadyLatch(boolean isHfpDeviceConnected) {
        CountDownLatch latch = new CountDownLatch(1);
        synchronized (mPendingRingingLatches) {
            // We only want to delay ringing if BT is connected but not active yet.
            boolean isDelayRequired = isHfpDeviceConnected && !mIsBtActive;
            Log.i(this, "prepareRingingReadyLatch:"
                    + " connected=" + isHfpDeviceConnected
                    + ", BT active=" + mIsBtActive
                    + ", isDelayRequired=" + isDelayRequired);
            if (!isDelayRequired) latch.countDown();
            mPendingRingingLatches.add(latch);
        }
        return latch;
    }

    /**
     * Remove a ringing ready latch that has been used and is no longer pending.
     * @param l The latch to remove.
     */
    private void removePendingRingingReadyLatch(CountDownLatch l) {
        synchronized (mPendingRingingLatches) {
            mPendingRingingLatches.remove(l);
        }
    }

    /**
     * Count down all pending ringing ready latches and then clear the list.
     */
    private void clearPendingRingingLatches() {
        synchronized (mPendingRingingLatches) {
            mPendingRingingLatches.forEach(CountDownLatch::countDown);
            mPendingRingingLatches.clear();
        }
    }

    /**
     * Posts a message to the ringtone-thread handler. Creates the handler if specified by the
     * parameter shouldCreateHandler.
     *
     * @param messageCode The message to post.
     * @param shouldCreateHandler True when a handler should be created to handle this message.
     */
    private void postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args) {
        synchronized(this) {
            if (mHandler == null && shouldCreateHandler) {
                mHandler = getNewHandler();
            }

            if (mHandler == null) {
                Log.d(this, "Message %d skipped because there is no handler.", messageCode);
            } else {
                mHandler.obtainMessage(messageCode, args).sendToTarget();
            }
        }
    }

    /**
     * Creates a new ringtone Handler running in its own thread.
     */
    private Handler getNewHandler() {
        Preconditions.checkState(mHandler == null);

        HandlerThread thread = new HandlerThread("ringtone-player");
        thread.start();

        return new Handler(thread.getLooper(), null /*callback*/, true /*async*/) {
            @Override
            public void handleMessage(Message msg) {
                switch(msg.what) {
                    case EVENT_PLAY:
                        handlePlay((SomeArgs) msg.obj);
                        break;
                    case EVENT_STOP:
                        handleStop();
                        break;
                }
            }
        };
    }

    /**
     * Starts the actual playback of the ringtone. Executes on ringtone-thread.
     */
    private void handlePlay(SomeArgs args) {
        Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
        BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
        Session session = (Session) args.arg3;
        CountDownLatch ringingReadyLatch = (CountDownLatch) args.arg4;
        args.recycle();

        Log.continueSession(session, "ARP.hP");
        try {
            // Don't bother with any of this if there is an EVENT_STOP waiting, but give the
            // consumer a chance to do anything no matter what.
            if (mHandler.hasMessages(EVENT_STOP)) {
                Log.i(this, "handlePlay: skipping play early due to pending STOP");
                removePendingRingingReadyLatch(ringingReadyLatch);
                ringtoneConsumer.accept(null, /* stopped= */ true);
                return;
            }
            Ringtone ringtone = null;
            boolean hasStopped = false;
            try {
                try {
                    Log.i(this, "handlePlay: delay ring for ready signal...");
                    boolean reachedZero = ringingReadyLatch.await(PLAY_DELAY_TIMEOUT_MS,
                            TimeUnit.MILLISECONDS);
                    Log.i(this, "handlePlay: ringing ready, timeout=" + !reachedZero);
                } catch (InterruptedException e) {
                    Log.w(this, "handlePlay: latch exception: " + e);
                }
                ringtone = ringtoneSupplier.get();
                // Ringtone supply can be slow or stop command could have been issued while waiting
                // for BT to move to CONNECTED state. Re-check for stop event.
                if (mHandler.hasMessages(EVENT_STOP)) {
                    Log.i(this, "handlePlay: skipping play due to pending STOP");
                    hasStopped = true;
                    if (ringtone != null) ringtone.stop();  // proactively release the ringtone.
                    return;
                }
                // setRingtone even if null - it also stops any current ringtone to be consistent
                // with the overall state.
                setRingtone(ringtone);
                if (mRingtone == null) {
                    // The ringtoneConsumer can still vibrate at this stage.
                    Log.w(this, "No ringtone was found bail out from playing.");
                    return;
                }
                Uri uri = mRingtone.getUri();
                String uriString = (uri != null ? uri.toSafeString() : "");
                Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
                mRingtone.setLooping(true);
                if (mRingtone.isPlaying()) {
                    Log.d(this, "Ringtone already playing.");
                    return;
                }
                mRingtone.play();
                Log.i(this, "Play ringtone, looping.");
            } finally {
                removePendingRingingReadyLatch(ringingReadyLatch);
                ringtoneConsumer.accept(ringtone, hasStopped);
            }
        } finally {
            Log.cancelSubsession(session);
        }
    }

    /**
     * Stops the playback of the ringtone. Executes on the ringtone-thread.
     */
    private void handleStop() {
        ThreadUtil.checkNotOnMainThread();
        Log.i(this, "Stop ringtone.");

        setRingtone(null);

        synchronized(this) {
            if (mHandler.hasMessages(EVENT_PLAY)) {
                Log.v(this, "Keeping alive ringtone thread for subsequent play request.");
            } else {
                mHandler.removeMessages(EVENT_STOP);
                mHandler.getLooper().quitSafely();
                mHandler = null;
                Log.v(this, "Handler cleared.");
            }
        }
    }

    /**
     * @return true if we are currently preparing or playing a ringtone, false if we are not.
     */
    public boolean isPlaying() {
        return mIsPlaying;
    }

    private void setRingtone(@Nullable Ringtone ringtone) {
        Log.i(this, "setRingtone: ringtone null="  + (ringtone == null));
        // Make sure that any previously created instance of Ringtone is stopped so the MediaPlayer
        // can be released, before replacing mRingtone with a new instance. This is always created
        // as a looping Ringtone, so if not stopped it will keep playing on the background.
        if (mRingtone != null) {
            Log.d(this, "Ringtone.stop() invoked.");
            mRingtone.stop();
        }
        mRingtone = ringtone;
    }
}
