package com.lemenzo.gallery.data;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;

import com.lemenzo.gallery.model.MediaItem;
import com.lemenzo.gallery.model.MediaType;
import com.lemenzo.gallery.util.PermissionUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class MediaStoreDataSource {
    private static final String TAG = "LemenzoGalleryMedia";
    private static final long NO_BUCKET = Long.MIN_VALUE;

    private static final String[] FILES_PROJECTION = {
            MediaStore.Files.FileColumns._ID,
            MediaStore.MediaColumns.DISPLAY_NAME,
            MediaStore.MediaColumns.MIME_TYPE,
            MediaStore.MediaColumns.DATE_TAKEN,
            MediaStore.MediaColumns.DATE_ADDED,
            MediaStore.MediaColumns.WIDTH,
            MediaStore.MediaColumns.HEIGHT,
            MediaStore.MediaColumns.SIZE,
            MediaStore.Images.ImageColumns.BUCKET_ID,
            MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
            MediaStore.Video.VideoColumns.DURATION,
            MediaStore.MediaColumns.IS_FAVORITE,
            MediaStore.MediaColumns.IS_TRASHED,
            MediaStore.Files.FileColumns.MEDIA_TYPE,
            MediaStore.MediaColumns.INFERRED_DATE
    };

    private final Context context;
    private final ContentResolver resolver;

    public MediaStoreDataSource(Context context) {
        this.context = context.getApplicationContext();
        resolver = this.context.getContentResolver();
    }

    public List<MediaItem> load(int mode) {
        ArrayList<MediaItem> out = new ArrayList<>();
        if (PermissionUtils.canReadImages(context)) queryImages(out, mode);
        if (PermissionUtils.canReadVideos(context)) queryVideos(out, mode);
        Collections.sort(out, (a, b) -> {
            int date = Long.compare(b.sortDate(), a.sortDate());
            return date != 0 ? date : Long.compare(b.id, a.id);
        });
        return out;
    }

    /** Compatibility offset paging, retained for viewer anchor/window operations. */
    public List<MediaItem> loadPage(int mode, int offset, int limit) {
        List<MediaItem> page = loadFilesPage(mode, offset, limit, NO_BUCKET);
        if (!page.isEmpty() || offset > 0) return page;
        List<MediaItem> all = load(mode);
        int from = Math.min(Math.max(0, offset), all.size());
        int to = Math.min(all.size(), from + Math.max(0, limit));
        return new ArrayList<>(all.subList(from, to));
    }

    /**
     * Seek/keyset paging for large libraries.
     *
     * Unlike SQL OFFSET, query cost does not grow with every page. The anchor is the last
     * item already displayed in the globally sorted Files cursor.
     */
    public List<MediaItem> loadPageAfter(int mode, MediaItem anchor, int limit) {
        return loadFilesPageAfter(mode, anchor, limit, NO_BUCKET);
    }

    public List<MediaItem> loadAlbumPage(long bucketId, int offset, int limit) {
        return loadFilesPage(MediaQuery.ALL, offset, limit, bucketId);
    }

    public List<MediaItem> loadAlbumPageAfter(long bucketId, MediaItem anchor, int limit) {
        return loadFilesPageAfter(MediaQuery.ALL, anchor, limit, bucketId);
    }

    public int findOffsetForUri(String targetUri) {
        if (targetUri == null || targetUri.trim().isEmpty()) return -1;
        Uri target;
        try { target = Uri.parse(targetUri); } catch (RuntimeException e) { return -1; }
        long targetId;
        try { targetId = ContentUris.parseId(target); } catch (RuntimeException e) { return -1; }
        long targetDate = -1L;
        String[] targetProjection = { MediaStore.MediaColumns.INFERRED_DATE };
        try (Cursor c = resolver.query(target, targetProjection, null, null, null)) {
            if (c != null && c.moveToFirst()) targetDate = c.getLong(0);
        } catch (RuntimeException e) {
            Log.w(TAG, "Unable to resolve viewer anchor date", e);
            return -1;
        }
        if (targetDate < 0L) return -1;

        SelectionBuilder sb = baseSelection(NO_BUCKET);
        if (sb == null) return -1;
        sb.selection.append(" AND (")
                .append(MediaStore.MediaColumns.INFERRED_DATE).append(">? OR (")
                .append(MediaStore.MediaColumns.INFERRED_DATE).append("=? AND ")
                .append(MediaStore.Files.FileColumns._ID).append(">?))");
        sb.args.add(Long.toString(targetDate));
        sb.args.add(Long.toString(targetDate));
        sb.args.add(Long.toString(targetId));

        Bundle q = args(MediaQuery.ALL, sb.selection.toString(), sb.args.toArray(new String[0]));
        q.remove(ContentResolver.QUERY_ARG_SQL_SORT_ORDER);
        try (Cursor c = resolver.query(
                MediaStore.Files.getContentUri("external"),
                new String[] { MediaStore.Files.FileColumns._ID },
                q,
                null)) {
            return c == null ? -1 : c.getCount();
        } catch (RuntimeException e) {
            Log.w(TAG, "Unable to calculate viewer anchor offset", e);
            return -1;
        }
    }

    private List<MediaItem> loadFilesPage(int mode, int offset, int limit, long bucketId) {
        ArrayList<MediaItem> out = new ArrayList<>();
        if (limit <= 0) return out;
        SelectionBuilder sb = baseSelection(bucketId);
        if (sb == null) return out;

        Bundle query = args(mode, sb.selection.toString(), sb.args.toArray(new String[0]));
        query.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
        query.putInt(ContentResolver.QUERY_ARG_OFFSET, Math.max(0, offset));
        return executeFilesQuery(query, out);
    }

    private List<MediaItem> loadFilesPageAfter(int mode, MediaItem anchor, int limit, long bucketId) {
        ArrayList<MediaItem> out = new ArrayList<>();
        if (limit <= 0) return out;
        SelectionBuilder sb = baseSelection(bucketId);
        if (sb == null) return out;

        if (anchor != null) {
            sb.selection.append(" AND (")
                    .append(MediaStore.MediaColumns.INFERRED_DATE).append("<? OR (")
                    .append(MediaStore.MediaColumns.INFERRED_DATE).append("=? AND ")
                    .append(MediaStore.Files.FileColumns._ID).append("<?))");
            sb.args.add(Long.toString(anchor.sortDate()));
            sb.args.add(Long.toString(anchor.sortDate()));
            sb.args.add(Long.toString(anchor.id));
        }

        Bundle query = args(mode, sb.selection.toString(), sb.args.toArray(new String[0]));
        query.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
        return executeFilesQuery(query, out);
    }

    private List<MediaItem> executeFilesQuery(Bundle query, ArrayList<MediaItem> out) {
        try (Cursor cursor = resolver.query(
                MediaStore.Files.getContentUri("external"),
                FILES_PROJECTION,
                query,
                null)) {
            if (cursor == null) return out;
            while (cursor.moveToNext()) {
                long id = cursor.getLong(0);
                int mediaType = cursor.getInt(13);
                MediaType type = mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
                        ? MediaType.VIDEO : MediaType.IMAGE;
                Uri uri = ContentUris.withAppendedId(
                        type == MediaType.VIDEO
                                ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                                : MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        id);
                out.add(new MediaItem(
                        id,
                        uri,
                        cursor.getString(1),
                        cursor.getString(2),
                        cursor.getLong(3),
                        cursor.getLong(4),
                        cursor.getInt(5),
                        cursor.getInt(6),
                        cursor.getLong(7),
                        cursor.getLong(8),
                        safe(cursor.getString(9)),
                        type == MediaType.VIDEO ? cursor.getLong(10) : 0L,
                        cursor.getInt(11) != 0,
                        cursor.getInt(12) != 0,
                        type,
                        cursor.getLong(14)));
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Files query failed", e);
        }
        return out;
    }

    private SelectionBuilder baseSelection(long bucketId) {
        boolean images = PermissionUtils.canReadImages(context);
        boolean videos = PermissionUtils.canReadVideos(context);
        if (!images && !videos) return null;

        SelectionBuilder sb = new SelectionBuilder();
        sb.selection.append("(");
        if (images) {
            sb.selection.append(MediaStore.Files.FileColumns.MEDIA_TYPE).append("=?");
            sb.args.add(Integer.toString(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE));
        }
        if (videos) {
            if (!sb.args.isEmpty()) sb.selection.append(" OR ");
            sb.selection.append(MediaStore.Files.FileColumns.MEDIA_TYPE).append("=?");
            sb.args.add(Integer.toString(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO));
        }
        sb.selection.append(")");
        if (bucketId != NO_BUCKET) {
            sb.selection.append(" AND ")
                    .append(MediaStore.Images.ImageColumns.BUCKET_ID)
                    .append("=?");
            sb.args.add(Long.toString(bucketId));
        }
        return sb;
    }

    private static final class SelectionBuilder {
        final StringBuilder selection = new StringBuilder();
        final ArrayList<String> args = new ArrayList<>();
    }

    public List<MediaItem> loadAlbum(long bucketId) {
        ArrayList<MediaItem> out = new ArrayList<>();
        if (PermissionUtils.canReadImages(context)) queryImages(out, MediaQuery.ALL, bucketId);
        if (PermissionUtils.canReadVideos(context)) queryVideos(out, MediaQuery.ALL, bucketId);
        Collections.sort(out, (a, b) -> {
            int date = Long.compare(b.sortDate(), a.sortDate());
            return date != 0 ? date : Long.compare(b.id, a.id);
        });
        return out;
    }

    private Bundle args(int mode, String selection, String[] selectionArgs) {
        Bundle args = new Bundle();
        if (selection != null) {
            args.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
            args.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
        }
        args.putString(
                ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
                MediaStore.MediaColumns.INFERRED_DATE + " DESC, "
                        + MediaStore.Files.FileColumns._ID + " DESC");
        args.putInt(
                MediaStore.QUERY_ARG_MATCH_TRASHED,
                mode == MediaQuery.TRASH ? MediaStore.MATCH_ONLY : MediaStore.MATCH_EXCLUDE);
        if (mode == MediaQuery.FAVORITES) {
            args.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_ONLY);
        }
        return args;
    }

    private void queryImages(List<MediaItem> out, int mode) {
        queryImages(out, mode, NO_BUCKET);
    }

    private void queryVideos(List<MediaItem> out, int mode) {
        queryVideos(out, mode, NO_BUCKET);
    }

    private void queryImages(List<MediaItem> out, int mode, long bucket) {
        String selection = bucket == NO_BUCKET
                ? null : MediaStore.Images.ImageColumns.BUCKET_ID + "=?";
        String[] selectionArgs = selection == null ? null : new String[] { Long.toString(bucket) };
        String[] projection = {
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.MIME_TYPE,
                MediaStore.Images.Media.DATE_TAKEN,
                MediaStore.Images.Media.DATE_ADDED,
                MediaStore.Images.Media.WIDTH,
                MediaStore.Images.Media.HEIGHT,
                MediaStore.Images.Media.SIZE,
                MediaStore.Images.ImageColumns.BUCKET_ID,
                MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
                MediaStore.MediaColumns.IS_FAVORITE,
                MediaStore.MediaColumns.IS_TRASHED
        };
        try (Cursor cursor = resolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                projection,
                args(mode, selection, selectionArgs),
                null)) {
            if (cursor == null) return;
            while (cursor.moveToNext()) {
                long id = cursor.getLong(0);
                Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
                out.add(new MediaItem(
                        id, uri, cursor.getString(1), cursor.getString(2),
                        cursor.getLong(3), cursor.getLong(4), cursor.getInt(5), cursor.getInt(6),
                        cursor.getLong(7), cursor.getLong(8), safe(cursor.getString(9)), 0L,
                        cursor.getInt(10) != 0, cursor.getInt(11) != 0, MediaType.IMAGE));
            }
        } catch (SecurityException e) {
            Log.w(TAG, "Image access is not currently granted", e);
        } catch (RuntimeException e) {
            Log.e(TAG, "Unable to query images", e);
        }
    }

    private void queryVideos(List<MediaItem> out, int mode, long bucket) {
        String selection = bucket == NO_BUCKET
                ? null : MediaStore.Video.VideoColumns.BUCKET_ID + "=?";
        String[] selectionArgs = selection == null ? null : new String[] { Long.toString(bucket) };
        String[] projection = {
                MediaStore.Video.Media._ID,
                MediaStore.Video.Media.DISPLAY_NAME,
                MediaStore.Video.Media.MIME_TYPE,
                MediaStore.Video.VideoColumns.DATE_TAKEN,
                MediaStore.Video.Media.DATE_ADDED,
                MediaStore.Video.Media.WIDTH,
                MediaStore.Video.Media.HEIGHT,
                MediaStore.Video.Media.SIZE,
                MediaStore.Video.VideoColumns.BUCKET_ID,
                MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME,
                MediaStore.Video.VideoColumns.DURATION,
                MediaStore.MediaColumns.IS_FAVORITE,
                MediaStore.MediaColumns.IS_TRASHED
        };
        try (Cursor cursor = resolver.query(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                projection,
                args(mode, selection, selectionArgs),
                null)) {
            if (cursor == null) return;
            while (cursor.moveToNext()) {
                long id = cursor.getLong(0);
                Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
                out.add(new MediaItem(
                        id, uri, cursor.getString(1), cursor.getString(2),
                        cursor.getLong(3), cursor.getLong(4), cursor.getInt(5), cursor.getInt(6),
                        cursor.getLong(7), cursor.getLong(8), safe(cursor.getString(9)), cursor.getLong(10),
                        cursor.getInt(11) != 0, cursor.getInt(12) != 0, MediaType.VIDEO));
            }
        } catch (SecurityException e) {
            Log.w(TAG, "Video access is not currently granted", e);
        } catch (RuntimeException e) {
            Log.e(TAG, "Unable to query videos", e);
        }
    }

    private static String safe(String value) {
        return value == null || value.trim().isEmpty() ? "Other" : value;
    }
}
