/*
 * Copyright (C) 2019 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.
 */

#pragma once

#include "Sound.h"

#include <any>
#include <android-base/thread_annotations.h>
#include <audio_utils/clock.h>
#include <media/AudioTrack.h>

namespace android::soundpool {

// This is the amount of time to wait after stop is called when stealing an
// AudioTrack to allow the sound to ramp down.  If this is 0, glitches
// may occur when stealing an AudioTrack.
inline constexpr int64_t kStopWaitTimeNs = 20 * NANOS_PER_MILLISECOND;

inline constexpr size_t kCacheLineSize = 64; /* std::hardware_constructive_interference_size */

class StreamManager; // forward decl

/**
 * A Stream is associated with a StreamID exposed to the app to play a Sound.
 *
 * The Stream uses monitor locking strategy on mLock.
 * https://en.wikipedia.org/wiki/Monitor_(synchronization)
 *
 * where public methods are guarded by a lock (as needed)
 *
 * For Java equivalent APIs, see
 * https://developer.android.com/reference/android/media/SoundPool
 *
 * Streams are paired by the StreamManager, so one stream in the pair may be "stopping"
 * while the other stream of the pair has been prepared to run
 * (and the streamID returned to the app) pending its pair to be stopped.
 * The pair of a Stream may be obtained by calling getPairStream(),
 * where this->getPairStream()->getPairStream() == this; (pair is a commutative relationship).
 *
 * playPairStream() and getPairPriority() access the paired stream.
 * See also StreamManager.h for details of physical layout implications of paired streams.
 */
class alignas(kCacheLineSize) Stream {
public:
    enum state { IDLE, PAUSED, PLAYING };
    // The PAUSED, PLAYING state directly corresponds to the AudioTrack state of an active Stream.
    //
    // The IDLE state indicates an inactive Stream.   An IDLE Stream may have a non-nullptr
    // AudioTrack, which may be recycled for use if the SoundID matches the next Stream playback.
    //
    // PAUSED -> PLAYING through resume()  (see also autoResume())
    // PLAYING -> PAUSED through pause()   (see also autoPause())
    //
    // IDLE is the initial state of a Stream and also when a stream becomes inactive.
    // {PAUSED, PLAYING} -> IDLE through stop() (or if the Sound finishes playing)
    // IDLE -> PLAYING through play().  (there is no way to start a Stream in paused mode).

    ~Stream();
    void setStreamManager(StreamManager* streamManager) { // non-nullptr
        mStreamManager = streamManager; // set in StreamManager constructor, not changed
    }

    // The following methods are monitor locked by mLock.
    //
    // For methods taking a streamID:
    // if the streamID matches the Stream's mStreamID, then method proceeds
    // else the command is ignored with no effect.

    // returns true if the stream needs to be explicitly stopped.
    bool requestStop(int32_t streamID);
    void stop();                    // explicit stop(), typically called from the worker thread.
    void clearAudioTrack();
    void pause(int32_t streamID);
    void autoPause();               // see the Java SoundPool.autoPause documentation for details.
    void resume(int32_t streamID);
    void autoResume();
    void mute(bool muting);
    void dump() const NO_THREAD_SAFETY_ANALYSIS; // disable for ALOGV (see func for details).

    // returns the pair stream if successful, nullptr otherwise.
    // garbage is used to release tracks and data outside of any lock.
    Stream* playPairStream(std::vector<std::any>& garbage,
                           int32_t playerIId = PLAYER_PIID_INVALID);

    // These parameters are explicitly checked in the SoundPool class
    // so never deviate from the Java API specified values.
    void setVolume(int32_t streamID, float leftVolume, float rightVolume);
    void setRate(int32_t streamID, float rate);
    void setPriority(int32_t streamID, int priority);
    void setLoop(int32_t streamID, int loop);
    void setPlay(int32_t streamID, const std::shared_ptr<Sound> &sound, int32_t soundID,
           float leftVolume, float rightVolume, int32_t priority, int32_t loop, float rate);
    void setStopTimeNs(int64_t stopTimeNs); // systemTime() clock monotonic.

    // The following getters are not locked and have weak consistency.
    // These are considered advisory only - being stale is of nuisance.
    int32_t getPriority() const NO_THREAD_SAFETY_ANALYSIS { return mPriority; }
    int32_t getPairPriority() const NO_THREAD_SAFETY_ANALYSIS {
        return getPairStream()->getPriority();
    }
    int64_t getStopTimeNs() const NO_THREAD_SAFETY_ANALYSIS { return mStopTimeNs; }

    // Can change with setPlay()
    int32_t getStreamID() const NO_THREAD_SAFETY_ANALYSIS { return mStreamID; }

    // Can change with play_l()
    int32_t getSoundID() const NO_THREAD_SAFETY_ANALYSIS { return mSoundID; }

    bool hasSound() const NO_THREAD_SAFETY_ANALYSIS { return mSound.get() != nullptr; }

    // This never changes.  See top of header.
    Stream* getPairStream() const;

    // Stream ID of ourselves, or the pair depending on who holds the AudioTrack
    int getCorrespondingStreamID();

protected:
    // AudioTrack callback interface implementation
    class StreamCallback : public AudioTrack::IAudioTrackCallback {
      public:
        StreamCallback(Stream * stream, bool toggle) : mStream(stream), mToggle(toggle) {}
        size_t onMoreData(const AudioTrack::Buffer& buffer) override;
        void onUnderrun() override;
        void onLoopEnd(int32_t loopsRemaining) override;
        void onMarker(uint32_t markerPosition) override;
        void onNewPos(uint32_t newPos) override;
        void onBufferEnd() override;
        void onNewIAudioTrack() override;
        void onStreamEnd() override;
        size_t onCanWriteMoreData(const AudioTrack::Buffer& buffer) override;

        // Holding a raw ptr is technically unsafe, but, Stream objects persist
        // through the lifetime of the StreamManager through the use of a
        // unique_ptr<Stream[]>. Ensuring lifetime will cause us to give up
        // locality as well as pay RefBase/sp performance cost, which we are
        // unwilling to do. Non-owning refs to unique_ptrs are idiomatically raw
        // ptrs, as below.
        Stream * const mStream;
        const bool mToggle;
    };

    sp<StreamCallback> mCallback;
private:
    // garbage is used to release tracks and data outside of any lock.
    void play_l(const std::shared_ptr<Sound>& sound, int streamID,
            float leftVolume, float rightVolume, int priority, int loop, float rate,
            std::vector<std::any>& garbage, int playerIId) REQUIRES(mLock);
    void stop_l() REQUIRES(mLock);
    void setVolume_l(float leftVolume, float rightVolume) REQUIRES(mLock);

    // For use with AudioTrack callback.
    void onBufferEnd(int toggle, int tries) NO_THREAD_SAFETY_ANALYSIS;

    // StreamManager should be set on construction and not changed.
    // release mLock before calling into StreamManager
    StreamManager*     mStreamManager = nullptr;

    mutable std::mutex  mLock;
    std::atomic_int32_t mStreamID GUARDED_BY(mLock) = 0; // Valid streamIDs are always positive.
    int                 mState GUARDED_BY(mLock) = IDLE;
    std::shared_ptr<Sound> mSound GUARDED_BY(mLock);    // Non-null if playing.
    int32_t             mSoundID GUARDED_BY(mLock) = 0; // SoundID associated with AudioTrack.
    float               mLeftVolume GUARDED_BY(mLock) = 0.f;
    float               mRightVolume GUARDED_BY(mLock) = 0.f;
    int32_t             mPriority GUARDED_BY(mLock) = INT32_MIN;
    int32_t             mLoop GUARDED_BY(mLock) = 0;
    float               mRate GUARDED_BY(mLock) = 0.f;
    bool                mAutoPaused GUARDED_BY(mLock) = false;
    bool                mMuted GUARDED_BY(mLock) = false;

    sp<AudioTrack>      mAudioTrack GUARDED_BY(mLock);
    int                 mToggle GUARDED_BY(mLock) = 0;
    int64_t             mStopTimeNs GUARDED_BY(mLock) = 0;  // if nonzero, time to wait for stop.
};

} // namespace android::soundpool
