package org.thoughtcrime.securesms.components;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.lifecycle.Observer;

import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.SimpleColorFilter;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.value.LottieValueCallback;
import com.pnikosis.materialishprogress.ProgressWheel;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.audio.AudioWaveForms;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.SlideClickListener;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;

public final class AudioView extends FrameLayout {

  private static final String TAG = Log.tag(AudioView.class);

  private static final int MODE_NORMAL = 0;
  private static final int MODE_SMALL  = 1;
  private static final int MODE_DRAFT  = 2;

  private static final int FORWARDS =  1;
  private static final int REVERSE  = -1;

  @NonNull  private final AnimatingToggle     controlToggle;
  @NonNull  private final View                progressAndPlay;
  @NonNull  private final LottieAnimationView playPauseButton;
  @NonNull  private final ImageView           downloadButton;
  @Nullable private final ProgressWheel       circleProgress;
  @NonNull  private final SeekBar             seekBar;
            private final boolean             smallView;
            private final boolean             autoRewind;

  @Nullable private final TextView duration;

  @ColorInt private final int waveFormPlayedBarsColor;
  @ColorInt private final int waveFormUnplayedBarsColor;
  @ColorInt private final int waveFormThumbTint;

  @Nullable private SlideClickListener downloadListener;
            private int                backwardsCounter;
            private int                lottieDirection;
            private boolean            isPlaying;
            private long               durationMillis;
            private AudioSlide         audioSlide;
            private Callbacks          callbacks;

            private Disposable disposable = Disposable.disposed();

  private final Observer<VoiceNotePlaybackState> playbackStateObserver = this::onPlaybackState;

  public AudioView(Context context) {
    this(context, null);
  }

  public AudioView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    setLayoutDirection(LAYOUT_DIRECTION_LTR);

    TypedArray typedArray = null;
    try {
      typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);

      int mode   = typedArray.getInteger(R.styleable.AudioView_audioView_mode, MODE_NORMAL);
      smallView  = mode == MODE_SMALL;
      autoRewind = typedArray.getBoolean(R.styleable.AudioView_autoRewind, false);

      switch (mode) {
        case MODE_NORMAL:
          inflate(context, R.layout.audio_view, this);
          break;
        case MODE_SMALL:
          inflate(context, R.layout.audio_view_small, this);
          break;
        case MODE_DRAFT:
          inflate(context, R.layout.audio_view_draft, this);
          break;
        default:
          throw new IllegalStateException("Unsupported mode: " + mode);
      }

      this.controlToggle   = findViewById(R.id.control_toggle);
      this.playPauseButton = findViewById(R.id.play);
      this.progressAndPlay = findViewById(R.id.progress_and_play);
      this.downloadButton  = findViewById(R.id.download);
      this.circleProgress  = findViewById(R.id.circle_progress);
      this.seekBar         = findViewById(R.id.seek);
      this.duration        = findViewById(R.id.duration);

      lottieDirection = REVERSE;
      this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
      this.playPauseButton.setOnLongClickListener(v -> performLongClick());
      this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());

      setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));

      int backgroundTintColor = typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.TRANSPARENT);
      if (getBackground() != null && backgroundTintColor != Color.TRANSPARENT) {
        DrawableCompat.setTint(getBackground(), backgroundTintColor);
      }

      this.waveFormPlayedBarsColor   = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
      this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
      this.waveFormThumbTint         = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);

      setProgressAndPlayBackgroundTint(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK));
    } finally {
      if (typedArray != null) {
        typedArray.recycle();
      }
    }
  }

  @Override
  protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
  }

  @Override
  protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    EventBus.getDefault().unregister(this);
    disposable.dispose();
  }

  public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
    progressAndPlay.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
  }

  public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
    return playbackStateObserver;
  }

  public void setAudio(final @NonNull AudioSlide audio,
                       final @Nullable Callbacks callbacks,
                       final boolean showControls,
                       final boolean forceHideDuration)
  {
    this.callbacks = callbacks;

    if (duration != null) {
      duration.setVisibility(View.VISIBLE);
    }

    if (seekBar instanceof WaveFormSeekBarView) {
      if (audioSlide != null && !Objects.equals(audioSlide.getUri(), audio.getUri())) {
       WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
       waveFormView.setWaveMode(false);
       seekBar.setProgress(0);
       durationMillis = 0;
      }
    }

    if (showControls && audio.isPendingDownload()) {
      controlToggle.displayQuick(downloadButton);
      seekBar.setEnabled(false);
      downloadButton.setOnClickListener(new DownloadClickedListener(audio));
      if (circleProgress != null) {
        if (circleProgress.isSpinning()) circleProgress.stopSpinning();
        circleProgress.setVisibility(View.GONE);
      }
    } else if (showControls && audio.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
      controlToggle.displayQuick(progressAndPlay);
      seekBar.setEnabled(false);
      showPlayButton();
      if (circleProgress != null) {
        circleProgress.setVisibility(View.VISIBLE);
        circleProgress.spin();
      }
    } else {
      seekBar.setEnabled(true);
      if (circleProgress != null && circleProgress.isSpinning()) circleProgress.stopSpinning();
      showPlayButton();
    }

    if (seekBar instanceof WaveFormSeekBarView) {
      WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
      waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
      if (audioSlide == null || !Objects.equals(audioSlide.getUri(), audio.getUri())) {
        disposable.dispose();
        disposable = AudioWaveForms.getWaveForm(getContext(), audio.asAttachment())
                                   .observeOn(AndroidSchedulers.mainThread())
                                   .subscribe(
                                       data -> {
                                         durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
                                         updateProgress(0, 0);
                                         if (!forceHideDuration && duration != null) {
                                           duration.setVisibility(VISIBLE);
                                         }
                                         waveFormView.setWaveData(data.getWaveForm());
                                       },
                                       t -> waveFormView.setWaveMode(false)
                                   );
      }
    }

    if (forceHideDuration && duration != null) {
      duration.setVisibility(View.GONE);
    }

    this.audioSlide = audio;

    if (SignalStore.internal().getShowArchiveStateHint() && audioSlide.asAttachment() instanceof DatabaseAttachment) {
      DatabaseAttachment dbAttachment = (DatabaseAttachment) audioSlide.asAttachment();
      View mediaArchive = findViewById(R.id.thumbnail_media_archive);
      if (mediaArchive != null) {
        mediaArchive.setVisibility(View.VISIBLE);
        switch (dbAttachment.archiveTransferState) {
          case NONE -> mediaArchive.setBackgroundTintList(ColorStateList.valueOf(Color.BLACK));
          case COPY_PENDING -> mediaArchive.setBackgroundTintList(ColorStateList.valueOf(Color.BLUE));
          case UPLOAD_IN_PROGRESS -> mediaArchive.setBackgroundTintList(ColorStateList.valueOf(Color.CYAN));
          case FINISHED -> mediaArchive.setBackgroundTintList(ColorStateList.valueOf(Color.GREEN));
          case TEMPORARY_FAILURE -> mediaArchive.setBackgroundTintList(ColorStateList.valueOf(Color.YELLOW));
          case PERMANENT_FAILURE -> mediaArchive.setBackgroundTintList(ColorStateList.valueOf(Color.RED));
        }
      }
    }
  }

  public void setDownloadClickListener(@Nullable SlideClickListener listener) {
    this.downloadListener = listener;
  }

  public @Nullable Uri getAudioSlideUri() {
    if (audioSlide != null) return audioSlide.getUri();
    else                    return null;
  }

  private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
    onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
    onProgress(voiceNotePlaybackState.getUri(),
               (double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
               voiceNotePlaybackState.getPlayheadPositionMillis());
    onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
    onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isPlaying(), voiceNotePlaybackState.isAutoReset());
  }

  private void onDuration(@NonNull Uri uri, long durationMillis) {
    if (isTarget(uri)) {
      this.durationMillis = durationMillis;
    }
  }

  private void onStart(@NonNull Uri uri, boolean statePlaying, boolean autoReset) {
    if (!isTarget(uri) || !statePlaying) {
      if (hasAudioUri()) {
        onStop(audioSlide.getUri(), autoReset);
      }

      return;
    }

    if (isPlaying) {
      return;
    }

    isPlaying = true;
    togglePlayToPause();
  }

  private void onStop(@NonNull Uri uri, boolean autoReset) {
    if (!isTarget(uri)) {
      return;
    }

    if (!isPlaying) {
      return;
    }

    isPlaying = false;
    togglePauseToPlay();

    if (autoReset || autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
      backwardsCounter = 4;
      rewind();
    }
  }

  private void onProgress(@NonNull Uri uri, double progress, long millis) {
    if (!isTarget(uri)) {
      return;
    }

    int seekProgress = (int) Math.floor(progress * seekBar.getMax());

    if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
      backwardsCounter = 0;
      seekBar.setProgress(seekProgress);
      updateProgress((float) progress, millis);
    } else {
      backwardsCounter++;
    }
  }

  private void onSpeedChanged(@NonNull Uri uri, float speed) {
    if (callbacks != null) {
      callbacks.onSpeedChanged(speed, isTarget(uri));
    }
  }

  private boolean isTarget(@NonNull Uri uri) {
    return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
  }

  private boolean hasAudioUri() {
    return audioSlide != null && audioSlide.getUri() != null;
  }

  @Override
  public void setFocusable(boolean focusable) {
    super.setFocusable(focusable);
    this.playPauseButton.setFocusable(focusable);
    this.seekBar.setFocusable(focusable);
    this.seekBar.setFocusableInTouchMode(focusable);
    this.downloadButton.setFocusable(focusable);
  }

  @Override
  public void setClickable(boolean clickable) {
    super.setClickable(clickable);
    this.playPauseButton.setClickable(clickable);
    this.seekBar.setClickable(clickable);
    this.seekBar.setOnTouchListener(clickable ? new LongTapAwareTouchListener() : new TouchIgnoringListener());
    this.downloadButton.setClickable(clickable);
  }

  @Override
  public void setEnabled(boolean enabled) {
    super.setEnabled(enabled);
    this.playPauseButton.setEnabled(enabled);
    this.seekBar.setEnabled(enabled);
    this.downloadButton.setEnabled(enabled);
  }

  private void updateProgress(float progress, long millis) {
    if (callbacks != null) {
      callbacks.onProgressUpdated(durationMillis, millis);
    }

    if (duration != null && durationMillis > 0) {
      long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis));
      duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
    }

    if (smallView && circleProgress != null) {
      circleProgress.setInstantProgress(seekBar.getProgress() == 0 ? 1 : progress);
    }
  }

  public void setTint(int foregroundTint) {
    post(()-> this.playPauseButton.addValueCallback(new KeyPath("**"),
                                                    LottieProperty.COLOR_FILTER,
                                                    new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));

    this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);

    if (circleProgress != null) {
      this.circleProgress.setBarColor(foregroundTint);
    }

    if (this.duration != null) {
      this.duration.setTextColor(foregroundTint);
    }

    if (this.seekBar.getProgressDrawable() != null) {
      this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
    }

    if (this.seekBar.getThumb() != null) {
      this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
    }
  }

  public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
    seekBar.getGlobalVisibleRect(rect);
  }

  private double getProgress() {
    if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
      return 0;
    } else {
      return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
    }
  }

  private void togglePlayToPause() {
    startLottieAnimation(FORWARDS);
  }

  private void togglePauseToPlay() {
    startLottieAnimation(REVERSE);
  }

  private void startLottieAnimation(int direction) {
    showPlayButton();

    if (lottieDirection == direction) {
      return;
    }
    lottieDirection = direction;

    playPauseButton.pauseAnimation();
    playPauseButton.setSpeed(direction * 2);
    playPauseButton.resumeAnimation();
  }

  private void showPlayButton() {
    if (circleProgress != null) {
      if (!smallView) {
        circleProgress.setVisibility(GONE);
      } else if (seekBar.getProgress() == 0) {
        circleProgress.setInstantProgress(1);
      }
    }

    playPauseButton.setVisibility(VISIBLE);
    controlToggle.displayQuick(progressAndPlay);
  }

  public void stopPlaybackAndReset() {
    if (audioSlide == null || audioSlide.getUri() == null) return;

    if (callbacks != null) {
      callbacks.onStopAndReset(audioSlide.getUri());
      rewind();
    }
  }

  private class PlayPauseClickedListener implements View.OnClickListener {

    @Override
    public void onClick(View v) {
      if (audioSlide == null || audioSlide.getUri() == null) return;

      if (callbacks != null) {
        if (lottieDirection == REVERSE) {
          callbacks.onPlay(audioSlide.getUri(), getProgress());
        } else {
          callbacks.onPause(audioSlide.getUri());
        }
      }
    }
  }

  private void rewind() {
    seekBar.setProgress(0);
    updateProgress(0, 0);
  }

  private class DownloadClickedListener implements View.OnClickListener {
    private final @NonNull AudioSlide slide;

    private DownloadClickedListener(@NonNull AudioSlide slide) {
      this.slide = slide;
    }

    @Override
    public void onClick(View v) {
      if (downloadListener != null) downloadListener.onClick(v, slide);
    }
  }

  private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {

    private boolean wasPlaying;

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    }

    @Override
    public synchronized void onStartTrackingTouch(SeekBar seekBar) {
      if (audioSlide == null || audioSlide.getUri() == null) return;

      wasPlaying = isPlaying;
      if (isPlaying) {
        if (callbacks != null) {
          callbacks.onPause(audioSlide.getUri());
        }
      }
    }

    @Override
    public synchronized void onStopTrackingTouch(SeekBar seekBar) {
      if (audioSlide == null || audioSlide.getUri() == null) return;

      if (callbacks != null) {
        if (wasPlaying) {
          callbacks.onSeekTo(audioSlide.getUri(), getProgress());
        } else {
          callbacks.onProgressUpdated(durationMillis, Math.round(durationMillis * getProgress()));
        }
      }
    }
  }

  private class LongTapAwareTouchListener implements OnTouchListener {
    private final GestureDetector gestureDetector = new GestureDetector(AudioView.this.getContext(), new GestureDetector.SimpleOnGestureListener() {
      @Override
      public void onLongPress(MotionEvent e) {
        performLongClick();
      }
    });

    @Override
    public boolean onTouch(View v, MotionEvent event) {
      return gestureDetector.onTouchEvent(event);
    }
  }

  private static class TouchIgnoringListener implements OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
      return true;
    }
  }

  @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
  public void onEventAsync(final PartProgressEvent event) {
    if (audioSlide != null && circleProgress != null && event.attachment.equals(audioSlide.asAttachment())) {
      circleProgress.setInstantProgress(((float) event.progress) / event.total);
    }
  }

  public interface Callbacks {
    void onPlay(@NonNull Uri audioUri, double progress);
    void onPause(@NonNull Uri audioUri);
    void onSeekTo(@NonNull Uri audioUri, double progress);
    void onStopAndReset(@NonNull Uri audioUri);
    void onSpeedChanged(float speed, boolean isPlaying);
    void onProgressUpdated(long durationMillis, long playheadMillis);
  }
}
