package ch.threema.app.fragments.mediaviews;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.TextView;

import org.slf4j.Logger;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import ch.threema.app.R;
import ch.threema.app.activities.MediaViewerActivity;
import ch.threema.app.managers.ServiceManager;
import ch.threema.app.services.ActivityService;
import ch.threema.app.services.FileService;
import ch.threema.app.services.MessageService;
import ch.threema.app.services.NotificationPreferenceService;
import ch.threema.app.utils.MimeUtil;
import ch.threema.app.utils.RuntimeUtil;

import static ch.threema.base.utils.LoggingKt.getThreemaLogger;

import ch.threema.storage.models.AbstractMessageModel;
import ch.threema.storage.models.MessageType;
import kotlin.Lazy;

import static ch.threema.storage.models.data.MessageContentsType.VOICE_MESSAGE;
import static org.koin.java.KoinJavaComponent.inject;

abstract public class MediaViewFragment extends Fragment {
    private static final Logger logger = getThreemaLogger("MediaViewFragment");

    // enums are evil
    private final int ImageState_NONE = 0;
    private final int ImageState_THUMBNAIL = 1;
    private final int ImageState_DECRYPTED = 2;

    public interface OnMediaLoadListener {
        void decrypting();

        void decrypted(boolean success);

        void loaded(File file);

        void thumbnailLoaded(Drawable bitmap);
    }

    private @Nullable AbstractMessageModel messageModel;

    private Future threadFullDecrypt;
    private final ExecutorService threadPoolExecutor = Executors.newSingleThreadExecutor();
    protected FileService fileService;
    protected MessageService messageService;
    protected Lazy<NotificationPreferenceService> notificationPreferenceService = inject(NotificationPreferenceService.class);
    private File[] decryptedFileCache;
    private OnMediaLoadListener onMediaLoadListener;
    private File decryptedFile;
    private int imageState = ImageState_NONE;
    private WeakReference<TextView> emptyTextViewReference;
    private WeakReference<ViewGroup> rootViewReference;

    private Activity activity;
    protected int position;

    protected volatile boolean isCurrentlyInFocus;

    private static final int KEEP_ALIVE_DELAY = 20000;
    private final static Handler keepAliveHandler = new Handler();
    private final Runnable keepAliveTask = new Runnable() {
        @Override
        public void run() {
            if (getActivity() != null) {
                ActivityService.activityUserInteract(getActivity());
                keepAliveHandler.postDelayed(keepAliveTask, KEEP_ALIVE_DELAY);
            }
        }
    };

    public MediaViewFragment() {
        super();
    }

    private void processBundle(Bundle bundle) {
        if (bundle != null) {
            this.position = bundle.getInt("position", 0);
            this.messageModel = ((MediaViewerActivity) this.activity).getMessageModel(this.position);
            this.decryptedFileCache = ((MediaViewerActivity) this.activity).getDecryptedFileCache();
        }
    }

    @Override
    public void onAttach(@NonNull Activity activity) {
        super.onAttach(activity);
        this.activity = activity;
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ServiceManager serviceManager = ServiceManager.get();
        if (serviceManager == null) {
            return null;
        }

        this.fileService = serviceManager.getFileService();
        this.messageService = serviceManager.getMessageService();

        final @Nullable ViewGroup rootView = (ViewGroup) inflater.inflate(this.getFragmentResourceId(), container, false);
        if (rootView == null) {
            logger.error("Missing root view");
            return null;
        }

        // keep a reference to the textview
        this.rootViewReference = new WeakReference<>(rootView);
        this.emptyTextViewReference = new WeakReference<>(rootView.findViewById(R.id.empty_text));

        processBundle(getArguments());
        if (this.messageModel == null) {
            logger.error("Missing message model");
            return null;
        }

        this.created(savedInstanceState, rootView);
        this.decryptThumbnail();

        if (messageModel.getType() == MessageType.FILE) {
            handleMimeCategory(MimeUtil.getMimeCategory(messageModel.getFileData().getMimeType()));

            handleFileName(messageModel.getFileData().getFileName());
        }

        return rootView;
    }

    @Nullable
    protected AbstractMessageModel getMessageModel() {
        return this.messageModel;
    }

    public void setOnImageLoaded(OnMediaLoadListener onMediaLoadListener) {
        this.onMediaLoadListener = onMediaLoadListener;

        //if image already loaded!
        this.fireLoadedFile();
    }

    public void killDecryptThread() {
        if (this.threadFullDecrypt != null) {
            this.threadFullDecrypt.cancel(true);
            this.threadFullDecrypt = null;
        }
    }

    private void fireLoadedFile() {
        if (onMediaLoadListener != null && decryptedFile != null) {
            this.onMediaLoadListener.loaded(this.decryptedFile);
        }
    }

    private void decryptThumbnail() {
        if (messageModel != null && fileService != null) {
            logger.debug("show thumbnail of {}", this.position);
            Drawable thumbnail = null;
            try {
                Bitmap messageThumbnail = this.fileService.getMessageThumbnailBitmap(messageModel, null);
                if (messageThumbnail != null) {
                    thumbnail = new BitmapDrawable(getResources(), messageThumbnail);
                }
            } catch (Exception e) {
                // no thumbnail file
            }

            if (thumbnail == null && messageModel.getMessageContentsType() == VOICE_MESSAGE) {
                thumbnail = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_microphone_outline);
                if (thumbnail != null) {
                    thumbnail.setTint(ContextCompat.getColor(requireContext(), R.color.material_dark_grey));
                }
            }

            if (thumbnail != null) {
                this.showThumbnail(thumbnail);

                this.imageState = ImageState_THUMBNAIL;
                if (this.onMediaLoadListener != null) {
                    this.onMediaLoadListener.thumbnailLoaded(thumbnail);
                }
            } else {
                this.showFileNotFoundContent();
            }
        }
    }

    public void destroy() {
        if (messageModel != null) {
            logger.debug("destroy decrypted image in fragment {}", this.position);
        }

        this.killDecryptThread();
    }

    public void hide() {
        if (messageModel != null) {
            logger.debug("hide fragment {}", this.position);
        }
        this.killDecryptThread();
        this.decryptThumbnail();
    }

    public void showDecrypted() {
        this.killDecryptThread();

        logger.debug("showDecrypted {} imageState = {}", position, this.imageState);

        //already decrypted
        if (this.imageState == ImageState_DECRYPTED) {
            this.fireLoadedFile();
            return;
        }

        this.handleDecryptingFile();
        //use cached files!
        if (this.decryptedFileCache[this.position] != null && this.decryptedFileCache[this.position].exists()) {
            this.fileDecrypted(this.decryptedFileCache[this.position]);
            return;
        }

        //load decrypted image
        if (messageModel != null && fileService != null) {
            this.killDecryptThread();

            this.threadFullDecrypt = threadPoolExecutor.submit(() -> {
                try {
                    logger.debug("show decrypted of {}", position);
                    final File decrypted = fileService.decryptMessageFileToTempFile(messageModel);
                    if (decrypted == null || !decrypted.exists()) {
                        throw new Exception("Decrypted file not found");
                    }

                    RuntimeUtil.runOnUiThread(() -> {
                        fileDecrypted(decrypted);

                        if (onMediaLoadListener != null) {
                            onMediaLoadListener.decrypted(true);
                        }
                    });

                } catch (Exception x) {
                    logger.error("Exception", x);
                    RuntimeUtil.runOnUiThread(() -> {
                        if (onMediaLoadListener != null) {
                            onMediaLoadListener.decrypted(false);
                        }

                        //reload thumbnail, if failed, show broken image!
                        decryptThumbnail();
                        handleDecryptFailure();
                    });
                }
            });
        }
    }

    protected void showFileNotFoundContent() {
        if (this.emptyTextViewReference != null && this.emptyTextViewReference.get() != null) {
            this.emptyTextViewReference.get().setText(R.string.media_file_not_found);
            this.emptyTextViewReference.get().setVisibility(View.VISIBLE);
        }
        this.imageState = ImageState_NONE;
    }

    @UiThread
    private void fileDecrypted(File file) {
        if (file == null || !file.exists()) {
            return;
        }
        logger.debug("file decrypted {}", this.position);
        this.decryptedFile = file;
        this.decryptedFileCache[this.position] = this.decryptedFile;

        if (this.emptyTextViewReference != null && this.emptyTextViewReference.get() != null) {
            this.emptyTextViewReference.get().setVisibility(View.GONE);
        }

        this.handleDecryptedFile(file);
        this.imageState = ImageState_DECRYPTED;
        this.fireLoadedFile();
    }

    protected void keepScreenOn(boolean value) {
        if (value) {
            getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            keepAliveHandler.postDelayed(keepAliveTask, KEEP_ALIVE_DELAY);
        } else {
            getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            keepAliveHandler.removeCallbacks(keepAliveTask);
        }
    }

    public void setIsCurrentlyInFocus(boolean isCurrentlyInFocus) {
        if (this.isCurrentlyInFocus && !isCurrentlyInFocus) {
            this.isCurrentlyInFocus = false;
            onFocusLost();
        } else {
            this.isCurrentlyInFocus = isCurrentlyInFocus;
        }
    }

    protected void onFocusLost() {
    }

    protected void showUi(boolean show) {
        if (isAdded() && getActivity() != null) {
            if (show) {
                ((MediaViewerActivity) getActivity()).showUi();
            } else {
                ((MediaViewerActivity) getActivity()).hideUi();
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        keepAliveHandler.removeCallbacksAndMessages(null);
    }

    protected abstract void created(@Nullable Bundle savedInstanceState, @NonNull ViewGroup rootView);

    protected abstract int getFragmentResourceId();

    /**
     * This method is called with a thumbnail when the fragment is created. It can be overridden to
     * set the thumbnail. Otherwise no thumbnail is shown. The given thumbnail has low quality,
     * therefore it is only recommended to use as thumbnail.
     *
     * @param thumbnail the thumbnail of the displayed file
     */
    protected void showThumbnail(@NonNull Drawable thumbnail) {
        // nothing to do
    }

    protected abstract void handleDecryptingFile();

    protected abstract void handleDecryptFailure();

    abstract protected void handleDecryptedFile(File file);

    @Nullable
    protected ViewGroup getRootViewReference() {
        if (this.rootViewReference != null) {
            return this.rootViewReference.get();
        } else {
            return null;
        }
    }

    /**
     * This method is called with the mime category when the fragment is created. If a subclass needs
     * the mime category, this method can be overridden.
     * Note that this is only called for messages of type MessageType.FILE
     *
     * @param category the mime category of the displayed file
     */
    protected void handleMimeCategory(@NonNull MimeUtil.MimeCategory category) {
        // nothing to do
    }

    /**
     * This method is called with the filename when the fragment is created. If a subclass needs the
     * filename, this method can be overridden.
     * Note that this is only called for messages of type MessageType.FILE
     *
     * @param fileName the filename of the displayed file
     */
    protected void handleFileName(@Nullable String fileName) {
        // nothing to do
    }

    @Override
    public void onPause() {
        setUserVisibleHint(false);
        super.onPause();
    }

    @Override
    public void setMenuVisibility(boolean menuVisible) {
        super.setMenuVisibility(menuVisible);
        if (!menuVisible) {
            setUserVisibleHint(false);
        }
    }
}
