/*
 * 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.systemui.statusbar.notification.row.wrapper;

import static com.android.systemui.Dependency.MAIN_HANDLER;

import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.metrics.LogMaker;
import android.os.Handler;
import android.service.notification.StatusBarNotification;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.widget.SeekBar;
import android.widget.TextView;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.widget.MediaNotificationView;
import com.android.systemui.Dependency;
import com.android.systemui.qs.QSPanel;
import com.android.systemui.qs.QuickQSPanel;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.TransformableView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
import com.android.systemui.util.Utils;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Wraps a notification containing a media template
 */
public class NotificationMediaTemplateViewWrapper extends NotificationTemplateViewWrapper {

    private static final long PROGRESS_UPDATE_INTERVAL = 1000; // 1s
    private static final String COMPACT_MEDIA_TAG = "media";
    private final Handler mHandler = Dependency.get(MAIN_HANDLER);
    private Timer mSeekBarTimer;
    private View mActions;
    private SeekBar mSeekBar;
    private TextView mSeekBarElapsedTime;
    private TextView mSeekBarTotalTime;
    private long mDuration = 0;
    private MediaController mMediaController;
    private MediaMetadata mMediaMetadata;
    private NotificationMediaManager mMediaManager;
    private View mSeekBarView;
    private Context mContext;
    private MetricsLogger mMetricsLogger;
    private boolean mIsViewVisible;

    @VisibleForTesting
    protected SeekBar.OnSeekBarChangeListener mSeekListener =
            new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            if (mMediaController != null) {
                mMediaController.getTransportControls().seekTo(mSeekBar.getProgress());
                mMetricsLogger.write(newLog(MetricsEvent.TYPE_UPDATE));
            }
        }
    };

    private MediaNotificationView.VisibilityChangeListener mVisibilityListener =
            new MediaNotificationView.VisibilityChangeListener() {
        @Override
        public void onAggregatedVisibilityChanged(boolean isVisible) {
            mIsViewVisible = isVisible;
            if (isVisible && mMediaController != null) {
                // Restart timer if we're currently playing and didn't already have one going
                PlaybackState state = mMediaController.getPlaybackState();
                if (state != null && state.getState() == PlaybackState.STATE_PLAYING
                        && mSeekBarTimer == null && mSeekBarView != null
                        && mSeekBarView.getVisibility() != View.GONE) {
                    startTimer();
                }
            } else {
                clearTimer();
            }
        }
    };

    private View.OnAttachStateChangeListener mAttachStateListener =
            new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            mIsViewVisible = false;
        }
    };

    private MediaController.Callback mMediaCallback = new MediaController.Callback() {
        @Override
        public void onSessionDestroyed() {
            clearTimer();
            mMediaController.unregisterCallback(this);
            if (mView instanceof MediaNotificationView) {
                ((MediaNotificationView) mView).removeVisibilityListener(mVisibilityListener);
                mView.removeOnAttachStateChangeListener(mAttachStateListener);
            }
        }

        @Override
        public void onPlaybackStateChanged(@Nullable PlaybackState state) {
            if (state == null) {
                return;
            }

            if (state.getState() != PlaybackState.STATE_PLAYING) {
                // Update the UI once, in case playback info changed while we were paused
                updatePlaybackUi(state);
                clearTimer();
            } else if (mSeekBarTimer == null && mSeekBarView != null
                    && mSeekBarView.getVisibility() != View.GONE) {
                startTimer();
            }
        }

        @Override
        public void onMetadataChanged(@Nullable MediaMetadata metadata) {
            if (mMediaMetadata == null || !mMediaMetadata.equals(metadata)) {
                mMediaMetadata = metadata;
                updateDuration();
            }
        }
    };

    protected NotificationMediaTemplateViewWrapper(Context ctx, View view,
            ExpandableNotificationRow row) {
        super(ctx, view, row);
        mContext = ctx;
        mMediaManager = Dependency.get(NotificationMediaManager.class);
        mMetricsLogger = Dependency.get(MetricsLogger.class);
    }

    private void resolveViews() {
        mActions = mView.findViewById(com.android.internal.R.id.media_actions);
        mIsViewVisible = mView.isShown();

        final MediaSession.Token token = mRow.getEntry().getSbn().getNotification().extras
                .getParcelable(Notification.EXTRA_MEDIA_SESSION);

        boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar();
        if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) {
            if (mSeekBarView != null) {
                mSeekBarView.setVisibility(View.GONE);
            }
            return;
        }

        // Check for existing media controller and clean up / create as necessary
        boolean shouldUpdateListeners = false;
        if (mMediaController == null || !mMediaController.getSessionToken().equals(token)) {
            if (mMediaController != null) {
                mMediaController.unregisterCallback(mMediaCallback);
            }
            mMediaController = new MediaController(mContext, token);
            shouldUpdateListeners = true;
        }

        mMediaMetadata = mMediaController.getMetadata();
        if (mMediaMetadata != null) {
            long duration = mMediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
            if (duration <= 0) {
                // Don't include the seekbar if this is a livestream
                if (mSeekBarView != null && mSeekBarView.getVisibility() != View.GONE) {
                    mSeekBarView.setVisibility(View.GONE);
                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_CLOSE));
                    clearTimer();
                } else if (mSeekBarView == null && shouldUpdateListeners) {
                    // Only log if the controller changed, otherwise we would log multiple times for
                    // the same notification when user pauses/resumes
                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_CLOSE));
                }
                return;
            } else if (mSeekBarView != null && mSeekBarView.getVisibility() == View.GONE) {
                // Otherwise, make sure the seekbar is visible
                mSeekBarView.setVisibility(View.VISIBLE);
                mMetricsLogger.write(newLog(MetricsEvent.TYPE_OPEN));
                updateDuration();
                startTimer();
            }
        }

        // Inflate the seekbar template
        ViewStub stub = mView.findViewById(R.id.notification_media_seekbar_container);
        if (stub instanceof ViewStub) {
            LayoutInflater layoutInflater = LayoutInflater.from(stub.getContext());
            stub.setLayoutInflater(layoutInflater);
            stub.setLayoutResource(R.layout.notification_material_media_seekbar);
            mSeekBarView = stub.inflate();
            mMetricsLogger.write(newLog(MetricsEvent.TYPE_OPEN));

            mSeekBar = mSeekBarView.findViewById(R.id.notification_media_progress_bar);
            mSeekBar.setOnSeekBarChangeListener(mSeekListener);

            mSeekBarElapsedTime = mSeekBarView.findViewById(R.id.notification_media_elapsed_time);
            mSeekBarTotalTime = mSeekBarView.findViewById(R.id.notification_media_total_time);

            shouldUpdateListeners = true;
        }

        if (shouldUpdateListeners) {
            if (mView instanceof MediaNotificationView) {
                MediaNotificationView mediaView = (MediaNotificationView) mView;
                mediaView.addVisibilityListener(mVisibilityListener);
                mView.addOnAttachStateChangeListener(mAttachStateListener);
            }

            if (mSeekBarTimer == null) {
                if (mMediaController != null && canSeekMedia(mMediaController.getPlaybackState())) {
                    // Log initial state, since it will not be updated
                    mMetricsLogger.write(newLog(MetricsEvent.TYPE_DETAIL, 1));
                } else {
                    setScrubberVisible(false);
                }
                updateDuration();
                startTimer();
                mMediaController.registerCallback(mMediaCallback);
            }
        }
        updateSeekBarTint(mSeekBarView);
    }

    private void startTimer() {
        clearTimer();
        if (mIsViewVisible) {
            mSeekBarTimer = new Timer(true /* isDaemon */);
            mSeekBarTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    mHandler.post(mOnUpdateTimerTick);
                }
            }, 0, PROGRESS_UPDATE_INTERVAL);
        }
    }

    private void clearTimer() {
        if (mSeekBarTimer != null) {
            mSeekBarTimer.cancel();
            mSeekBarTimer.purge();
            mSeekBarTimer = null;
        }
    }

    @Override
    public void setRemoved() {
        clearTimer();
        if (mMediaController != null) {
            mMediaController.unregisterCallback(mMediaCallback);
        }
        if (mView instanceof MediaNotificationView) {
            ((MediaNotificationView) mView).removeVisibilityListener(mVisibilityListener);
            mView.removeOnAttachStateChangeListener(mAttachStateListener);
        }
    }

    private boolean canSeekMedia(@Nullable PlaybackState state) {
        if (state == null) {
            return false;
        }

        long actions = state.getActions();
        return ((actions & PlaybackState.ACTION_SEEK_TO) != 0);
    }

    private void setScrubberVisible(boolean isVisible) {
        if (mSeekBar == null || mSeekBar.isEnabled() == isVisible) {
            return;
        }

        mSeekBar.getThumb().setAlpha(isVisible ? 255 : 0);
        mSeekBar.setEnabled(isVisible);
        mMetricsLogger.write(newLog(MetricsEvent.TYPE_DETAIL, isVisible ? 1 : 0));
    }

    private void updateDuration() {
        if (mMediaMetadata != null && mSeekBar != null) {
            long duration = mMediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
            if (mDuration != duration) {
                mDuration = duration;
                mSeekBar.setMax((int) mDuration);
                mSeekBarTotalTime.setText(millisecondsToTimeString(duration));
            }
        }
    }

    protected final Runnable mOnUpdateTimerTick = new Runnable() {
        @Override
        public void run() {
            if (mMediaController != null && mSeekBar != null) {
                PlaybackState playbackState = mMediaController.getPlaybackState();
                if (playbackState != null) {
                    updatePlaybackUi(playbackState);
                } else {
                    clearTimer();
                }
            } else {
                clearTimer();
            }
        }
    };

    private void updatePlaybackUi(PlaybackState state) {
        if (mSeekBar == null || mSeekBarElapsedTime == null) {
            return;
        }

        long position = state.getPosition();
        mSeekBar.setProgress((int) position);

        mSeekBarElapsedTime.setText(millisecondsToTimeString(position));

        // Update scrubber in case available actions have changed
        setScrubberVisible(canSeekMedia(state));
    }

    private String millisecondsToTimeString(long milliseconds) {
        long seconds = milliseconds / 1000;
        String text = DateUtils.formatElapsedTime(seconds);
        return text;
    }

    @Override
    public void onContentUpdated(ExpandableNotificationRow row) {
        // Reinspect the notification. Before the super call, because the super call also updates
        // the transformation types and we need to have our values set by then.
        resolveViews();
        super.onContentUpdated(row);
    }

    private void updateSeekBarTint(View seekBarContainer) {
        if (seekBarContainer == null) {
            return;
        }

        if (this.getNotificationHeader() == null) {
            return;
        }

        int tintColor = getNotificationHeader().getOriginalIconColor();
        mSeekBarElapsedTime.setTextColor(tintColor);
        mSeekBarTotalTime.setTextColor(tintColor);
        mSeekBarTotalTime.setShadowLayer(1.5f, 1.5f, 1.5f, mBackgroundColor);

        ColorStateList tintList = ColorStateList.valueOf(tintColor);
        mSeekBar.setThumbTintList(tintList);
        tintList = tintList.withAlpha(192); // 75%
        mSeekBar.setProgressTintList(tintList);
        tintList = tintList.withAlpha(128); // 50%
        mSeekBar.setProgressBackgroundTintList(tintList);
    }

    @Override
    protected void updateTransformedTypes() {
        // This also clears the existing types
        super.updateTransformedTypes();
        if (mActions != null) {
            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ACTIONS,
                    mActions);
        }
    }

    @Override
    public boolean isDimmable() {
        return getCustomBackgroundColor() == 0;
    }

    @Override
    public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
        return true;
    }

    /**
     * Returns an initialized LogMaker for logging changes to the seekbar
     * @return new LogMaker
     */
    private LogMaker newLog(int event) {
        String packageName = mRow.getEntry().getSbn().getPackageName();

        return new LogMaker(MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR)
                .setType(event)
                .setPackageName(packageName);
    }

    /**
     * Returns an initialized LogMaker for logging changes with subtypes
     * @return new LogMaker
     */
    private LogMaker newLog(int event, int subtype) {
        String packageName = mRow.getEntry().getSbn().getPackageName();
        return new LogMaker(MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR)
                .setType(event)
                .setSubtype(subtype)
                .setPackageName(packageName);
    }
}
