/**
 * Copyright (C) 2018 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.car.radio.media;

import android.content.Context;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager.ProgramInfo;
import android.hardware.radio.RadioMetadata;
import android.media.MediaMetadata;
import android.media.Rating;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.car.broadcastradio.support.Program;
import com.android.car.broadcastradio.support.media.BrowseTree;
import com.android.car.broadcastradio.support.platform.ImageResolver;
import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
import com.android.car.radio.R;
import com.android.car.radio.service.RadioAppServiceWrapper;
import com.android.car.radio.service.RadioAppServiceWrapper.ConnectionState;
import com.android.car.radio.storage.RadioStorage;
import com.android.car.radio.util.Log;

import java.util.Objects;

/**
 * Implementation of tuner's MediaSession.
 */
public class TunerSession {
    private static final String TAG = "BcRadioApp.media";

    private final Object mLock = new Object();
    private final MediaSession mSession;

    private final Context mContext;
    private final BrowseTree mBrowseTree;
    @Nullable private final ImageResolver mImageResolver;
    private final RadioAppServiceWrapper mAppService;

    private final RadioStorage mRadioStorage;

    private final PlaybackState.Builder mPlaybackStateBuilder =
            new PlaybackState.Builder();
    @Nullable private ProgramInfo mCurrentProgram;

    /**
     * Custom order that puts RDS_RT ahead of RDS_PS.
     * RDS_PS is often used to scroll data in RDS_RT in some regions, and for the interests of
     * driver distration we want to prevent RDS_PS scrolling from updating UI so frequently.
     */
    public static final String[] PROGRAM_NAME_ORDER = new String[] {
            RadioMetadata.METADATA_KEY_PROGRAM_NAME,
            RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME,
            RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME,
            RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME,
            RadioMetadata.METADATA_KEY_RDS_RT,
            RadioMetadata.METADATA_KEY_RDS_PS
    };

    public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree,
            @NonNull RadioAppServiceWrapper appService, @Nullable ImageResolver imageResolver) {
        mSession = new MediaSession(context, TAG);

        mContext = Objects.requireNonNull(context);
        mBrowseTree = Objects.requireNonNull(browseTree);
        mImageResolver = imageResolver;
        mAppService = Objects.requireNonNull(appService);

        mRadioStorage = RadioStorage.getInstance(context);

        // ACTION_PAUSE is reserved for time-shifted playback
        mPlaybackStateBuilder.setActions(
                PlaybackState.ACTION_STOP
                | PlaybackState.ACTION_PLAY
                | PlaybackState.ACTION_SKIP_TO_PREVIOUS
                | PlaybackState.ACTION_SKIP_TO_NEXT
                | PlaybackState.ACTION_SET_RATING
                | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID
                | PlaybackState.ACTION_PLAY_FROM_URI);
        mSession.setRatingType(Rating.RATING_HEART);
        onPlaybackStateChanged(PlaybackState.STATE_NONE);
        mSession.setCallback(new TunerSessionCallback());

        // TunerSession is a part of RadioAppService, so observeForever is fine here.
        appService.getPlaybackState().observeForever(this::onPlaybackStateChanged);
        appService.getCurrentProgram().observeForever(this::updateMetadata);
        mRadioStorage.getFavorites().observeForever(
                favorites -> updateMetadata(mAppService.getCurrentProgram().getValue()));

        mSession.setActive(true);

        mAppService.getConnectionState().observeForever(this::onSelfStateChanged);
    }

    private void onSelfStateChanged(@ConnectionState int state) {
        if (state == RadioAppServiceWrapper.STATE_ERROR) {
            mSession.setActive(false);
        }
    }

    private void updateMetadata(@Nullable ProgramInfo info) {
        synchronized (mLock) {
            if (info == null) return;
            boolean fav = mRadioStorage.isFavorite(info.getSelector());
            MediaMetadata currMetaData = mSession.getController().getMetadata();
            MediaMetadata newMetaData = ProgramInfoExt.toMediaDisplayMetadata(info, fav,
                    mImageResolver, PROGRAM_NAME_ORDER);
            if (!Objects.equals(currMetaData, newMetaData)) {
                mSession.setMetadata(newMetaData);
            }
        }
    }

    private void onPlaybackStateChanged(@PlaybackState.State int state) {
        synchronized (mPlaybackStateBuilder) {
            mPlaybackStateBuilder.setState(state,
                    PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f);
            mSession.setPlaybackState(mPlaybackStateBuilder.build());
        }
    }

    private void selectionError() {
        mAppService.setMuted(true);
        mPlaybackStateBuilder.setErrorMessage(mContext.getString(R.string.invalid_selection));
        onPlaybackStateChanged(PlaybackState.STATE_ERROR);
        mPlaybackStateBuilder.setErrorMessage(null);
    }

    /** See {@link MediaSession#getSessionToken}. */
    public MediaSession.Token getSessionToken() {
        return mSession.getSessionToken();
    }

    /** See {@link MediaSession#getController}. */
    public MediaController getController() {
        return mSession.getController();
    }

    /** See {@link MediaSession#release}. */
    public void release() {
        mSession.release();
    }

    private class TunerSessionCallback extends MediaSession.Callback {
        @Override
        public void onStop() {
            mAppService.setMuted(true);
        }

        @Override
        public void onPlay() {
            mAppService.tuneToDefaultIfNeeded();
            mAppService.setMuted(false);
        }

        @Override
        public void onSkipToNext() {
            mAppService.skip(true);
        }

        @Override
        public void onSkipToPrevious() {
            mAppService.skip(false);
        }

        @Override
        public void onSetRating(Rating rating) {
            synchronized (mLock) {
                ProgramInfo info = mAppService.getCurrentProgram().getValue();
                if (info == null) return;

                if (rating.hasHeart()) {
                    mRadioStorage.addFavorite(Program.fromProgramInfo(info));
                } else {
                    mRadioStorage.removeFavorite(info.getSelector());
                }
            }
        }

        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            if (mBrowseTree.getRoot().getRootId().equals(mediaId)) {
                // general play command
                onPlay();
                return;
            }

            ProgramSelector selector = mBrowseTree.parseMediaId(mediaId);
            if (selector != null) {
                mAppService.tune(selector);
            } else {
                Log.w(TAG, "Invalid media ID: " + mediaId);
                selectionError();
            }
        }

        @Override
        public void onPlayFromUri(Uri uri, Bundle extras) {
            ProgramSelector selector = ProgramSelectorExt.fromUri(uri);
            if (selector != null) {
                mAppService.tune(selector);
            } else {
                Log.w(TAG, "Invalid URI: " + uri);
                selectionError();
            }
        }
    }
}
