package com.lemenzo.gallery.viewer;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.app.PendingIntent;
import android.app.AlertDialog;
import android.content.Context;
import android.database.Cursor;
import android.content.Intent;
import android.net.Uri;
import android.graphics.ImageDecoder;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.MediaController;
import android.widget.TextView;

import com.lemenzo.gallery.R;
import com.lemenzo.gallery.details.MediaDetailsBottomSheet;
import com.lemenzo.gallery.editor.EditorActivity;
import com.lemenzo.gallery.loader.BitmapDecodeUtils;
import com.lemenzo.gallery.model.MediaItem;
import com.lemenzo.gallery.model.MediaType;
import com.lemenzo.gallery.util.IntentUtils;
import com.lemenzo.gallery.privacy.PrivateShare;
import com.lemenzo.gallery.video.VideoEditorActivity;
import com.lemenzo.gallery.integration.VaultContract;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class ViewerActivity extends Activity {
    private static final String TAG = "LemenzoGalleryViewer";
    private static final String EXTRA_URI = "uri";
    private static final int REQUEST_FAVORITE = 6201;
    private static final int REQUEST_TRASH = 6202;

    private final ArrayList<MediaItem> items = new ArrayList<>();
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private int index;
    private FrameLayout container;
    private View topControls;
    private View bottomControls;
    private ImageButton favoriteButton;
    private ViewerViewModel viewModel;
    private int windowStartOffset;
    private boolean viewerEndReached;
    private boolean viewerPageLoading;
    private boolean externalSingleItem;
    private long currentMotionVideoLength;
    private VideoPageView activeVideo;

    public static Intent intent(Context context, MediaItem item) {
        return new Intent(context, ViewerActivity.class)
                .putExtra(EXTRA_URI, item.uri.toString());
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);
        setContentView(R.layout.activity_viewer);
        container = findViewById(R.id.media_container);
        topControls = findViewById(R.id.top_controls);
        bottomControls = findViewById(R.id.bottom_controls);
        favoriteButton = findViewById(R.id.favorite_button);

        findViewById(R.id.back_button).setOnClickListener(v -> finish());
        findViewById(R.id.share_button).setOnClickListener(v -> {
            MediaItem item = current();
            if (item != null) IntentUtils.share(this, item.uri, item.mimeType);
        });
        favoriteButton.setOnClickListener(v -> setFavorite());
        findViewById(R.id.edit_button).setOnClickListener(v -> edit());
        findViewById(R.id.info_button).setOnClickListener(v -> showDetails());
        findViewById(R.id.delete_button).setOnClickListener(v -> trash());
        findViewById(R.id.more_button).setOnClickListener(v -> showMore());

        String target = getIntent().getStringExtra(EXTRA_URI);
        if (target == null && Intent.ACTION_VIEW.equals(getIntent().getAction()) && getIntent().getData() != null) {
            target = getIntent().getData().toString();
        }
        final String targetUri = target;
        if (targetUri == null) { finish(); return; }
        viewModel = new ViewerViewModel(this);
        executor.execute(() -> {
            ViewerViewModel.Window window = viewModel.loadWindow(targetUri);
            int position = viewModel.find(window.items, targetUri);
            MediaItem direct = null;
            if (window.items.isEmpty()) direct = mediaItemFromUri(Uri.parse(targetUri));
            final MediaItem directItem = direct;
            runOnUiThread(() -> {
                if (isFinishing()) return;
                items.clear();
                if (directItem != null) {
                    items.add(directItem);
                    externalSingleItem = true;
                    windowStartOffset = 0;
                    viewerEndReached = true;
                    index = 0;
                } else {
                    items.addAll(window.items);
                    windowStartOffset = window.startOffset;
                    viewerEndReached = window.endReached;
                    index = position >= 0 ? position : 0;
                }
                if (items.isEmpty()) { finish(); return; }
                showCurrent();
                if (!externalSingleItem) prefetchViewerEdges();
            });
        });
    }


    private MediaItem mediaItemFromUri(Uri uri) {
        if (uri == null) return null;
        String mime = getContentResolver().getType(uri);
        String name = uri.getLastPathSegment();
        long size = 0L;
        try (Cursor cursor = getContentResolver().query(uri,
                new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE },
                null, null, null)) {
            if (cursor != null && cursor.moveToFirst()) {
                int nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
                int sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE);
                if (nameColumn >= 0 && !cursor.isNull(nameColumn)) name = cursor.getString(nameColumn);
                if (sizeColumn >= 0 && !cursor.isNull(sizeColumn)) size = cursor.getLong(sizeColumn);
            }
        } catch (Exception e) {
            Log.d(TAG, "Unable to query external media metadata", e);
        }
        if (mime == null) {
            String lower = name == null ? "" : name.toLowerCase(java.util.Locale.ROOT);
            mime = lower.endsWith(".mp4") || lower.endsWith(".webm") || lower.endsWith(".mkv")
                    ? "video/*" : "image/*";
        }
        MediaType type = mime.startsWith("video/") ? MediaType.VIDEO : MediaType.IMAGE;
        long now = System.currentTimeMillis();
        return new MediaItem(0L, uri, name == null ? getString(R.string.app_name) : name, mime,
                now, now / 1000L, 0, 0, size, 0L, "", 0L, false, false, type, now);
    }

    private MediaItem current() {
        return items.isEmpty() ? null : items.get(index);
    }

    private void next() {
        if (externalSingleItem) return;
        if (index < items.size() - 1) { index++; showCurrent(); prefetchViewerEdges(); }
        else loadNextViewerPage(true);
    }

    private void previous() {
        if (externalSingleItem) return;
        if (index > 0) { index--; showCurrent(); prefetchViewerEdges(); }
        else loadPreviousViewerPage(true);
    }

    private void prefetchViewerEdges() {
        if (externalSingleItem) return;
        if (index >= items.size() - 24) loadNextViewerPage(false);
        if (index <= 24) loadPreviousViewerPage(false);
    }

    private void loadNextViewerPage(boolean advanceAfterLoad) {
        if (viewerPageLoading || viewerEndReached || viewModel == null) return;
        viewerPageLoading = true; final int start = windowStartOffset; final int size = items.size();
        executor.execute(() -> {
            List<MediaItem> page = viewModel.nextPage(start, size);
            runOnUiThread(() -> {
                if (isFinishing() || isDestroyed()) return;
                viewerPageLoading = false;
                viewerEndReached = page.size() < ViewerViewModel.PAGE_SIZE;
                if (!page.isEmpty()) { items.addAll(page); if (advanceAfterLoad && index < items.size() - 1) { index++; showCurrent(); } }
            });
        });
    }

    private void loadPreviousViewerPage(boolean moveAfterLoad) {
        if (viewerPageLoading || windowStartOffset <= 0 || viewModel == null) return;
        viewerPageLoading = true; final int oldStart = windowStartOffset;
        executor.execute(() -> {
            List<MediaItem> page = viewModel.previousPage(oldStart);
            runOnUiThread(() -> {
                if (isFinishing() || isDestroyed()) return;
                viewerPageLoading = false;
                if (!page.isEmpty()) {
                    items.addAll(0, page); windowStartOffset = Math.max(0, oldStart - page.size()); index += page.size();
                    if (moveAfterLoad && index > 0) { index--; showCurrent(); }
                }
            });
        });
    }

    private void showCurrent() {
        MediaItem item = current();
        if (item == null) return;

        if (activeVideo != null) {
            activeVideo.release();
            activeVideo = null;
        }
        container.removeAllViews();
        ((TextView) findViewById(R.id.viewer_title)).setText(item.displayName);
        favoriteButton.setImageResource(
                item.favorite ? R.drawable.ic_favorite_filled : R.drawable.ic_favorite);
        favoriteButton.setContentDescription(getString(
                item.favorite ? R.string.unfavorite : R.string.favorite));
        favoriteButton.setVisibility(externalSingleItem ? View.GONE : View.VISIBLE);
        findViewById(R.id.delete_button).setVisibility(externalSingleItem ? View.GONE : View.VISIBLE);

        View editButton = findViewById(R.id.edit_button);
        editButton.setVisibility(View.VISIBLE);
        currentMotionVideoLength = 0L;
        getWindow().setColorMode(ActivityInfo.COLOR_MODE_DEFAULT);
        if (item.type == MediaType.VIDEO) {
            showVideo(item);
        } else {
            showImage(item);
        }
    }

    private void showImage(MediaItem item) {
        ZoomableImageView image = (ZoomableImageView) getLayoutInflater()
                .inflate(R.layout.item_viewer_image, container, false);
        image.setListener(new ZoomableImageView.Listener() {
            @Override public void onSingleTap() { toggleControls(); }
            @Override public void onSwipeLeft() { next(); }
            @Override public void onSwipeRight() { previous(); }
        });
        container.addView(image);
        String mime = item.mimeType == null ? "" : item.mimeType.toLowerCase(java.util.Locale.ROOT);
        if (mime.contains("gif") || mime.contains("webp")) loadAnimatedCapable(image, item);
        else loadStillCapable(image, item);
        executor.execute(() -> {
            long motionLength = MotionPhotoUtils.findVideoLength(this, item.uri);
            runOnUiThread(() -> {
                MediaItem now = current();
                if (now != null && now.uri.equals(item.uri)) currentMotionVideoLength = motionLength;
            });
        });
    }

    private void loadStillCapable(ZoomableImageView image, MediaItem item) {
        String key = item.uri.toString();
        image.setTag(key);
        final int maxW = Math.max(1, getResources().getDisplayMetrics().widthPixels * 2);
        final int maxH = Math.max(1, getResources().getDisplayMetrics().heightPixels * 2);
        executor.execute(() -> {
            try {
                android.graphics.Bitmap bitmap = BitmapDecodeUtils.decode(
                        getContentResolver(), item.uri, maxW, maxH);
                boolean ultraHdr = Build.VERSION.SDK_INT >= 34 && bitmap.hasGainmap();
                runOnUiThread(() -> {
                    if (isFinishing() || isDestroyed() || !key.equals(image.getTag())) {
                        if (!bitmap.isRecycled()) bitmap.recycle();
                        return;
                    }
                    image.setImageBitmap(bitmap);
                    getWindow().setColorMode(
                            ultraHdr ? ActivityInfo.COLOR_MODE_HDR : ActivityInfo.COLOR_MODE_DEFAULT);
                });
            } catch (Throwable failure) {
                Log.w(TAG, "Bitmap decode failed; trying drawable fallback " + item.uri, failure);
                try {
                    ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), item.uri);
                    Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
                        int w = Math.max(1, info.getSize().getWidth());
                        int h = Math.max(1, info.getSize().getHeight());
                        float scale = Math.min(1f, Math.min(maxW / (float) w, maxH / (float) h));
                        if (scale < 1f) {
                            decoder.setTargetSize(
                                    Math.max(1, Math.round(w * scale)),
                                    Math.max(1, Math.round(h * scale)));
                        }
                    });
                    runOnUiThread(() -> {
                        if (isFinishing() || isDestroyed() || !key.equals(image.getTag())) return;
                        image.setImageDrawable(drawable);
                        if (drawable instanceof AnimatedImageDrawable) {
                            ((AnimatedImageDrawable) drawable).start();
                        }
                    });
                } catch (Throwable fallbackFailure) {
                    Log.e(TAG, "Unable to decode still image " + item.uri, fallbackFailure);
                    runOnUiThread(() -> {
                        if (!isFinishing() && !isDestroyed() && key.equals(image.getTag())) {
                            showMediaError(R.string.viewer_unable_to_open_image);
                        }
                    });
                }
            }
        });
    }

    private void loadAnimatedCapable(ZoomableImageView image, MediaItem item) {
        String key = item.uri.toString();
        image.setTag(key);
        executor.execute(() -> {
            try {
                ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), item.uri);
                Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
                    int w = info.getSize().getWidth(), h = info.getSize().getHeight();
                    int maxW = getResources().getDisplayMetrics().widthPixels * 2;
                    int maxH = getResources().getDisplayMetrics().heightPixels * 2;
                    float scale = Math.min(1f, Math.min(maxW / (float)Math.max(1,w), maxH / (float)Math.max(1,h)));
                    if (scale < 1f) decoder.setTargetSize(Math.max(1, Math.round(w*scale)), Math.max(1, Math.round(h*scale)));
                });
                runOnUiThread(() -> {
                    if (isFinishing() || isDestroyed() || !key.equals(image.getTag())) return;
                    image.setImageDrawable(drawable);
                    if (drawable instanceof AnimatedImageDrawable) ((AnimatedImageDrawable) drawable).start();
                });
            } catch (Exception e) {
                Log.w(TAG, "Animated decode failed, falling back to still bitmap", e);
                final int maxW = Math.max(1, getResources().getDisplayMetrics().widthPixels * 2);
                final int maxH = Math.max(1, getResources().getDisplayMetrics().heightPixels * 2);
                try {
                    android.graphics.Bitmap bitmap = BitmapDecodeUtils.decode(
                            getContentResolver(), item.uri, maxW, maxH);
                    runOnUiThread(() -> {
                        if (isFinishing() || isDestroyed() || !key.equals(image.getTag())) {
                            if (!bitmap.isRecycled()) bitmap.recycle();
                            return;
                        }
                        image.setImageBitmap(bitmap);
                    });
                } catch (Throwable fallbackFailure) {
                    Log.e(TAG, "Unable to decode animated image as still bitmap " + item.uri,
                            fallbackFailure);
                    runOnUiThread(() -> {
                        if (!isFinishing() && !isDestroyed() && key.equals(image.getTag())) {
                            showMediaError(R.string.viewer_unable_to_open_image);
                        }
                    });
                }
            }
        });
    }

    private void showVideo(MediaItem item) {
        VideoPageView video = (VideoPageView) getLayoutInflater()
                .inflate(R.layout.item_viewer_video, container, false);
        activeVideo = video;
        container.addView(video);

        MediaController controller = new MediaController(this);
        controller.setMediaPlayer(video);
        controller.setAnchorView(video);
        video.setErrorListener((what, extra, failure) -> {
            Log.e(TAG, "Unable to play video " + item.uri
                    + " what=" + what + " extra=" + extra, failure);
            if (!isFinishing() && !isDestroyed() && activeVideo == video) {
                showMediaError(R.string.viewer_unable_to_open_video);
            }
        });
        video.setVideoURI(item.uri);

        GestureDetector gesture = new GestureDetector(this,
                new GestureDetector.SimpleOnGestureListener() {
                    @Override public boolean onDown(MotionEvent e) { return true; }
                    @Override public boolean onSingleTapConfirmed(MotionEvent e) {
                        toggleControls();
                        controller.show(2500);
                        return true;
                    }
                    @Override public boolean onFling(MotionEvent start, MotionEvent end,
                            float velocityX, float velocityY) {
                        float dx = end.getX() - start.getX();
                        float dy = end.getY() - start.getY();
                        if (Math.abs(dx) < 120f || Math.abs(dx) < Math.abs(dy) * 1.3f
                                || Math.abs(velocityX) < 500f) return false;
                        if (dx < 0) next(); else previous();
                        return true;
                    }
                });
        video.setOnTouchListener((v, event) -> gesture.onTouchEvent(event));
    }


    private void showMediaError(int messageRes) {
        if (activeVideo != null) {
            activeVideo.release();
            activeVideo = null;
        }
        container.removeAllViews();
        TextView error = new TextView(this);
        error.setText(messageRes);
        error.setTextColor(0xffffffff);
        error.setTextSize(16f);
        error.setGravity(android.view.Gravity.CENTER);
        int padding = Math.round(24f * getResources().getDisplayMetrics().density);
        error.setPadding(padding, padding, padding, padding);
        container.addView(error, new FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT));
    }

    private void toggleControls() {
        int visibility = topControls.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
        topControls.setVisibility(visibility);
        bottomControls.setVisibility(visibility);
    }

    private void showDetails() {
        MediaItem item = current();
        if (item != null) MediaDetailsBottomSheet.show(this, item);
    }

    private void edit() {
        MediaItem item = current();
        if (item == null) return;
        if (item.type == MediaType.IMAGE) startActivity(EditorActivity.intent(this, item.uri));
        else if (item.type == MediaType.VIDEO) startActivity(VideoEditorActivity.intent(this, item.uri));
    }

    private void showMore() {
        MediaItem item = current();
        if (item == null) return;
        final boolean motion = item.type == MediaType.IMAGE && currentMotionVideoLength > 0L;
        final boolean vault = VaultContract.isAvailable(this, item.uri, item.mimeType);
        final java.util.ArrayList<String> labels = new java.util.ArrayList<>();
        final java.util.ArrayList<Integer> actions = new java.util.ArrayList<>();
        final int ACTION_MOTION=1, ACTION_DETAILS=2, ACTION_SHARE=3, ACTION_PRIVATE=4, ACTION_VAULT=5;
        if (motion) { labels.add(getString(R.string.play_motion_photo)); actions.add(ACTION_MOTION); }
        labels.add(getString(R.string.media_details)); actions.add(ACTION_DETAILS);
        labels.add(getString(R.string.share_original)); actions.add(ACTION_SHARE);
        if (item.type == MediaType.IMAGE) { labels.add(getString(R.string.share_without_metadata)); actions.add(ACTION_PRIVATE); }
        if (vault) { labels.add(getString(R.string.move_to_vault)); actions.add(ACTION_VAULT); }
        new AlertDialog.Builder(this).setTitle(R.string.more).setItems(labels.toArray(new String[0]), (dialog, which) -> {
            int action = actions.get(which);
            if (action == ACTION_MOTION) playMotionPhoto(item, currentMotionVideoLength);
            else if (action == ACTION_DETAILS) showDetails();
            else if (action == ACTION_SHARE) IntentUtils.share(this, item.uri, item.mimeType);
            else if (action == ACTION_PRIVATE) privateShare(item);
            else if (action == ACTION_VAULT) handoffToVault(item);
        }).show();
    }

    private void handoffToVault(MediaItem item) {
        try { startActivity(VaultContract.importIntent(item.uri, item.mimeType)); }
        catch (Exception e) {
            Log.e(TAG, "Unable to hand media to Lemenzo Vault", e);
            android.widget.Toast.makeText(this, R.string.vault_handoff_failed, android.widget.Toast.LENGTH_LONG).show();
        }
    }

    private void playMotionPhoto(MediaItem item, long videoLength) {
        android.widget.Toast.makeText(this, R.string.preparing_motion_photo, android.widget.Toast.LENGTH_SHORT).show();
        executor.execute(() -> {
            java.io.File videoFile = null;
            try { videoFile = MotionPhotoUtils.extractVideo(this, item.uri, videoLength); }
            catch (Exception e) { Log.e(TAG, "Unable to extract Motion Photo video", e); }
            java.io.File result = videoFile;
            runOnUiThread(() -> {
                if (isFinishing() || isDestroyed()) return;
                if (result == null) { android.widget.Toast.makeText(this, R.string.motion_photo_failed, android.widget.Toast.LENGTH_LONG).show(); return; }
                if (activeVideo != null) activeVideo.release();
                VideoPageView video = (VideoPageView) getLayoutInflater()
                        .inflate(R.layout.item_viewer_video, container, false);
                activeVideo = video;
                container.removeAllViews();
                container.addView(video);
                MediaController controller = new MediaController(this);
                controller.setMediaPlayer(video);
                controller.setAnchorView(video);
                video.setErrorListener((what, extra, failure) -> {
                    Log.e(TAG, "Unable to play Motion Photo video", failure);
                    if (!isFinishing() && !isDestroyed() && activeVideo == video) {
                        showMediaError(R.string.viewer_unable_to_open_video);
                    }
                });
                video.setVideoURI(Uri.fromFile(result));
            });
        });
    }

    private void privateShare(MediaItem item) {
        android.widget.Toast.makeText(this, R.string.preparing_private_copy, android.widget.Toast.LENGTH_SHORT).show();
        executor.execute(() -> {
            Uri clean = null;
            try { clean = PrivateShare.create(this, item.uri, item.mimeType); }
            catch (Exception exception) { Log.e(TAG, "Unable to create metadata-free share copy", exception); }
            Uri result = clean;
            runOnUiThread(() -> {
                if (isFinishing() || isDestroyed()) return;
                if (result == null) android.widget.Toast.makeText(this, R.string.private_share_failed, android.widget.Toast.LENGTH_LONG).show();
                else IntentUtils.share(this, result, getContentResolver().getType(result));
            });
        });
    }

    private void setFavorite() {
        MediaItem item = current();
        if (item == null) return;
        try {
            PendingIntent request = MediaStore.createFavoriteRequest(
                    getContentResolver(), Collections.singletonList(item.uri), !item.favorite);
            startIntentSenderForResult(request.getIntentSender(), REQUEST_FAVORITE,
                    null, 0, 0, 0);
        } catch (Exception exception) {
            Log.e(TAG, "Unable to update favorite state", exception);
        }
    }

    private void trash() {
        MediaItem item = current();
        if (item == null) return;
        try {
            PendingIntent request = MediaStore.createTrashRequest(
                    getContentResolver(), Collections.singletonList(item.uri), true);
            startIntentSenderForResult(request.getIntentSender(), REQUEST_TRASH,
                    null, 0, 0, 0);
        } catch (Exception exception) {
            Log.e(TAG, "Unable to move media to trash", exception);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK) return;

        if (requestCode == REQUEST_TRASH) {
            items.remove(index);
            if (items.isEmpty()) {
                finish();
                return;
            }
            index = Math.min(index, items.size() - 1);
            showCurrent();
        } else if (requestCode == REQUEST_FAVORITE) {
            String target = current() == null ? null : current().uri.toString();
            executor.execute(() -> {
                ViewerViewModel.Window window = viewModel.loadWindow(target);
                int position = viewModel.find(window.items, target);
                runOnUiThread(() -> {
                    if (isFinishing()) return;
                    items.clear(); items.addAll(window.items); windowStartOffset = window.startOffset; viewerEndReached = window.endReached;
                    if (items.isEmpty()) { finish(); return; }
                    index = position >= 0 ? position : Math.min(index, items.size() - 1);
                    showCurrent(); prefetchViewerEdges();
                });
            });
        }
    }

    @Override
    protected void onPause() {
        if (activeVideo != null) activeVideo.pauseForLifecycle();
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (activeVideo != null) activeVideo.resumeForLifecycle();
    }

    @Override
    protected void onDestroy() {
        if (activeVideo != null) {
            activeVideo.release();
            activeVideo = null;
        }
        executor.shutdownNow();
        super.onDestroy();
    }
}
