/*
 * Copyright (C) 2011 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 android.speech.tts;

import android.annotation.NonNull;
import android.media.AudioFormat;
import android.speech.tts.TextToSpeechService.AudioOutputParams;
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
import android.util.Log;

/**
 * Speech synthesis request that plays the audio as it is received.
 */
class PlaybackSynthesisCallback extends AbstractSynthesisCallback {

    private static final String TAG = "PlaybackSynthesisRequest";
    private static final boolean DBG = false;

    private static final int MIN_AUDIO_BUFFER_SIZE = 8192;

    private final AudioOutputParams mAudioParams;

    /**
     * Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}.
     */
    private final Object mStateLock = new Object();

    // Handler associated with a thread that plays back audio requests.
    private final AudioPlaybackHandler mAudioTrackHandler;
    // A request "token", which will be non null after start() has been called.
    private SynthesisPlaybackQueueItem mItem = null;

    private volatile boolean mDone = false;

    /** Status code of synthesis */
    protected int mStatusCode;

    private final UtteranceProgressDispatcher mDispatcher;
    private final Object mCallerIdentity;
    private final AbstractEventLogger mLogger;

    PlaybackSynthesisCallback(@NonNull AudioOutputParams audioParams,
            @NonNull AudioPlaybackHandler audioTrackHandler,
            @NonNull UtteranceProgressDispatcher dispatcher, @NonNull Object callerIdentity,
            @NonNull AbstractEventLogger logger, boolean clientIsUsingV2) {
        super(clientIsUsingV2);
        mAudioParams = audioParams;
        mAudioTrackHandler = audioTrackHandler;
        mDispatcher = dispatcher;
        mCallerIdentity = callerIdentity;
        mLogger = logger;
        mStatusCode = TextToSpeech.SUCCESS;
    }

    @Override
    void stop() {
        if (DBG) Log.d(TAG, "stop()");

        SynthesisPlaybackQueueItem item;
        synchronized (mStateLock) {
            if (mDone) {
                return;
            }
            if (mStatusCode == TextToSpeech.STOPPED) {
                Log.w(TAG, "stop() called twice");
                return;
            }

            item = mItem;
            mStatusCode = TextToSpeech.STOPPED;
        }

        if (item != null) {
            // This might result in the synthesis thread being woken up, at which
            // point it will write an additional buffer to the item - but we
            // won't worry about that because the audio playback queue will be cleared
            // soon after (see SynthHandler#stop(String).
            item.stop(TextToSpeech.STOPPED);
        } else {
            // This happens when stop() or error() were called before start() was.

            // In all other cases, mAudioTrackHandler.stop() will
            // result in onSynthesisDone being called, and we will
            // write data there.
            mLogger.onCompleted(TextToSpeech.STOPPED);
            mDispatcher.dispatchOnStop();
        }
    }

    @Override
    public int getMaxBufferSize() {
        // The AudioTrack buffer will be at least MIN_AUDIO_BUFFER_SIZE, so that should always be
        // a safe buffer size to pass in.
        return MIN_AUDIO_BUFFER_SIZE;
    }

    @Override
    public boolean hasStarted() {
        synchronized (mStateLock) {
            return mItem != null;
        }
    }

    @Override
    public boolean hasFinished() {
        synchronized (mStateLock) {
            return mDone;
        }
    }

    @Override
    public int start(int sampleRateInHz, int audioFormat, int channelCount) {
        if (DBG) Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + "," + channelCount
                + ")");
        if (audioFormat != AudioFormat.ENCODING_PCM_8BIT &&
            audioFormat != AudioFormat.ENCODING_PCM_16BIT &&
            audioFormat != AudioFormat.ENCODING_PCM_FLOAT) {
            Log.w(TAG, "Audio format encoding " + audioFormat + " not supported. Please use one " +
                       "of AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT or " +
                       "AudioFormat.ENCODING_PCM_FLOAT");
        }
        mDispatcher.dispatchOnBeginSynthesis(sampleRateInHz, audioFormat, channelCount);

        int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);

        synchronized (mStateLock) {
            if (channelConfig == 0) {
                Log.e(TAG, "Unsupported number of channels :" + channelCount);
                mStatusCode = TextToSpeech.ERROR_OUTPUT;
                return TextToSpeech.ERROR;
            }
            if (mStatusCode == TextToSpeech.STOPPED) {
                if (DBG) Log.d(TAG, "stop() called before start(), returning.");
                return errorCodeOnStop();
            }
            if (mStatusCode != TextToSpeech.SUCCESS) {
                if (DBG) Log.d(TAG, "Error was raised");
                return TextToSpeech.ERROR;
            }
            if (mItem != null) {
                Log.e(TAG, "Start called twice");
                return TextToSpeech.ERROR;
            }
            SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
                    mAudioParams, sampleRateInHz, audioFormat, channelCount,
                    mDispatcher, mCallerIdentity, mLogger);
            mAudioTrackHandler.enqueue(item);
            mItem = item;
        }

        return TextToSpeech.SUCCESS;
    }

    @Override
    public int audioAvailable(byte[] buffer, int offset, int length) {
        if (DBG) Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + offset + "," + length
                + ")");

        if (length > getMaxBufferSize() || length <= 0) {
            throw new IllegalArgumentException("buffer is too large or of zero length (" +
                    + length + " bytes)");
        }

        SynthesisPlaybackQueueItem item = null;
        synchronized (mStateLock) {
            if (mItem == null) {
                mStatusCode = TextToSpeech.ERROR_OUTPUT;
                return TextToSpeech.ERROR;
            }
            if (mStatusCode != TextToSpeech.SUCCESS) {
                if (DBG) Log.d(TAG, "Error was raised");
                return TextToSpeech.ERROR;
            }
            if (mStatusCode == TextToSpeech.STOPPED) {
                return errorCodeOnStop();
            }
            item = mItem;
        }

        // Sigh, another copy.
        final byte[] bufferCopy = new byte[length];
        System.arraycopy(buffer, offset, bufferCopy, 0, length);
        mDispatcher.dispatchOnAudioAvailable(bufferCopy);

        // Might block on mItem.this, if there are too many buffers waiting to
        // be consumed.
        try {
            item.put(bufferCopy);
        } catch (InterruptedException ie) {
            synchronized (mStateLock) {
                mStatusCode = TextToSpeech.ERROR_OUTPUT;
                return TextToSpeech.ERROR;
            }
        }

        mLogger.onEngineDataReceived();
        return TextToSpeech.SUCCESS;
    }

    @Override
    public int done() {
        if (DBG) Log.d(TAG, "done()");

        int statusCode = 0;
        SynthesisPlaybackQueueItem item = null;
        synchronized (mStateLock) {
            if (mDone) {
                Log.w(TAG, "Duplicate call to done()");
                // Not an error that would prevent synthesis. Hence no
                // setStatusCode
                return TextToSpeech.ERROR;
            }
            if (mStatusCode == TextToSpeech.STOPPED) {
                if (DBG) Log.d(TAG, "Request has been aborted.");
                return errorCodeOnStop();
            }
            mDone = true;

            if (mItem == null) {
                // .done() was called before .start. Treat it as successful synthesis
                // for a client, despite service bad implementation.
                Log.w(TAG, "done() was called before start() call");
                if (mStatusCode == TextToSpeech.SUCCESS) {
                    mDispatcher.dispatchOnSuccess();
                } else {
                    mDispatcher.dispatchOnError(mStatusCode);
                }
                mLogger.onEngineComplete();
                return TextToSpeech.ERROR;
            }

            item = mItem;
            statusCode = mStatusCode;
        }

        // Signal done or error to item
        if (statusCode == TextToSpeech.SUCCESS) {
            item.done();
        } else {
            item.stop(statusCode);
        }
        mLogger.onEngineComplete();
        return TextToSpeech.SUCCESS;
    }

    @Override
    public void error() {
        error(TextToSpeech.ERROR_SYNTHESIS);
    }

    @Override
    public void error(int errorCode) {
        if (DBG) Log.d(TAG, "error() [will call stop]");
        synchronized (mStateLock) {
            if (mDone) {
                return;
            }
            mStatusCode = errorCode;
        }
    }

    @Override
    public void rangeStart(int markerInFrames, int start, int end) {
        if (mItem == null) {
            Log.e(TAG, "mItem is null");
            return;
        }
        mItem.rangeStart(markerInFrames, start, end);
    }
}
