package net.typeblog.shelter.services;

import android.app.Service;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;

import androidx.annotation.Nullable;

import net.typeblog.shelter.ShelterApplication;
import net.typeblog.shelter.util.CrossProfileDocumentsProvider;
import net.typeblog.shelter.util.Utility;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// A service to forward file information across the profile boundary
public class FileShuttleService extends Service {
    public static final long TIMEOUT = 10000;
    // Periodic task to stop the service when idle.
    // This service does not need to persist.
    private Runnable mSuicideTask = this::suicide;
    private Handler mHandler = new Handler(Looper.getMainLooper());
    private IFileShuttleService.Stub mStub = new IFileShuttleService.Stub() {
        @Override
        public void ping() {
            // Dummy method
            resetSuicideTask();
        }

        @Override
        public List<Map<String, Serializable>> loadFiles(String path) {
            resetSuicideTask();
            ArrayList<Map<String, Serializable>> ret = new ArrayList<>();
            File f = new File(resolvePath(path));
            if (f.listFiles() != null) {
                for (File child : f.listFiles()) {
                    ret.add(loadFileMeta(child.getPath()));
                }
            }
            return ret;
        }

        @Override
        public Map<String, Serializable> loadFileMeta(String path) {
            resetSuicideTask();
            File f = new File(resolvePath(path));
            HashMap<String, Serializable> map = new HashMap<>();
            map.put(DocumentsContract.Document.COLUMN_DOCUMENT_ID, f.getAbsolutePath());
            map.put(DocumentsContract.Document.COLUMN_DISPLAY_NAME, f.getName());
            map.put(DocumentsContract.Document.COLUMN_SIZE, f.length());
            map.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, f.lastModified());

            if (f.isDirectory()) {
                map.put(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR);
                map.put(DocumentsContract.Document.COLUMN_FLAGS,
                        DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE |
                                DocumentsContract.Document.FLAG_SUPPORTS_DELETE);
            } else {
                String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
                        Utility.getFileExtension(f.getAbsolutePath()));
                int flags = DocumentsContract.Document.FLAG_SUPPORTS_DELETE;
                if (mime != null && (mime.startsWith("image/") || mime.startsWith("video/"))) {
                    flags |= DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL;
                }
                if (mime == null) {
                    mime = "application/unknown";
                }
                map.put(DocumentsContract.Document.COLUMN_MIME_TYPE, mime);
                map.put(DocumentsContract.Document.COLUMN_FLAGS, flags);
            }
            return map;
        }

        @Override
        public ParcelFileDescriptor openFile(String path, String mode) {
            resetSuicideTask();
            File f = new File(resolvePath(path));

            try {
                return ParcelFileDescriptor.open(f, ParcelFileDescriptor.parseMode(mode));
            } catch (FileNotFoundException e) {
                return null;
            }
        }

        @Override
        public ParcelFileDescriptor openThumbnail(String path, Point sizeHint) {
            resetSuicideTask();
            String fullPath = resolvePath(path);
            String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
                    Utility.getFileExtension(fullPath));
            if (mime == null) {
                return null;
            }
            if (mime.startsWith("image/")) {
                // Image thumbnail
                return loadImageThumbnail(fullPath, sizeHint);
            } else if (mime.startsWith("video/")) {
                // Video thumbnail
                return loadVideoThumbnail(fullPath);
            } else {
                return null;
            }
        }

        @Override
        public String createFile(String path, String mimeType, String displayName) {
            resetSuicideTask();
            File f;
            if (!DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
                String fullPath = path + "/" + displayName;
                String extensionPart = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
                if (!fullPath.endsWith(extensionPart)) {
                    fullPath += extensionPart;
                }
                f = new File(resolvePath(fullPath));

                if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
                    // Notify the media scanner to scan the file
                    Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                    intent.setData(Uri.fromFile(f));
                    sendBroadcast(intent);
                }

                try {
                    if (!f.createNewFile()) {
                        return null;
                    }
                } catch (IOException e) {
                    return null;
                }

            } else {
                String fullPath = path + "/" + displayName;
                f = new File(resolvePath(fullPath));
                if (!f.mkdir()) {
                    return null;
                }
            }
            return f.getAbsolutePath();
        }

        @Override
        public String deleteFile(String path) {
            resetSuicideTask();
            File f = new File(resolvePath(path));
            f.delete();
            return f.getParentFile().getAbsolutePath();
        }

        @Override
        public boolean isChildOf(String parent, String child) {
            File parentFile = new File(resolvePath(parent));
            File childFile = new File(resolvePath(child));
            String parentPath = parentFile.getAbsolutePath();
            if (parentPath.charAt(parentPath.length() - 1) != '/') {
                parentPath += "/"; // Make sure it ends with '/'
            }
            return parentFile.exists() && parentFile.isDirectory()
                    && childFile.exists()
                    && childFile.getAbsolutePath().startsWith(parentPath);
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        resetSuicideTask();
        return mStub;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        android.util.Log.d("FileShuttleService", "being destroyed");
    }

    private String resolvePath(String path) {
        if (path.startsWith(CrossProfileDocumentsProvider.DUMMY_ROOT)) {
            return path.replaceFirst(CrossProfileDocumentsProvider.DUMMY_ROOT,
                    Environment.getExternalStorageDirectory().getAbsolutePath());
        } else {
            return path;
        }
    }

    private void resetSuicideTask() {
        mHandler.removeCallbacks(mSuicideTask);
        mHandler.postDelayed(mSuicideTask, TIMEOUT);
    }

    private void suicide() {
        mHandler.removeCallbacks(mSuicideTask);
        ((ShelterApplication) getApplication()).unbindFileShuttleService();
        stopSelf();
    }

    private ParcelFileDescriptor loadImageThumbnail(String fullPath, Point sizeHint) {
        int id = Utility.getMediaStoreId(FileShuttleService.this, fullPath);
        if (id == -1) {
            // Fallback to directly loading thumbnail from file
            return loadBitmapThumbnail(fullPath, sizeHint);
        }
        Cursor result = MediaStore.Images.Thumbnails.queryMiniThumbnail(
                getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
        if (result.getCount() == 0) {
            // If no thumbnail is found, we try to request one first
            MediaStore.Images.Thumbnails.getThumbnail(
                    getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
            result = MediaStore.Images.Thumbnails.queryMiniThumbnail(
                    getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
        }
        if (result.getCount() == 0) {
            // Fallback to directly loading thumbnail from file
            return loadBitmapThumbnail(fullPath, sizeHint);
        } else {
            result.moveToFirst();
            try {
                int index = result.getColumnIndex(MediaStore.Images.Thumbnails.DATA);
                return getContentResolver().openFileDescriptor(
                        Uri.fromFile(new File(result.getString(index))), "r");
            } catch (FileNotFoundException e) {
                return null;
            }
        }
    }

    private ParcelFileDescriptor loadVideoThumbnail(String fullPath) {
        // The MediaStore interface for video thumbnails just do not work at all
        // It can't even retrieve video IDs from the database
        // Anyway, use this as a temporary fix.
        // TODO: Figure out how to use the MediaStore interface with videos
        Bitmap bmp = ThumbnailUtils.createVideoThumbnail(fullPath, MediaStore.Video.Thumbnails.MINI_KIND);
        return bitmapToFd(bmp);
    }

    // Fallback method for thumbnail loading: just load from disk, but load a scaled down version
    private ParcelFileDescriptor loadBitmapThumbnail(String path, Point sizeHint) {
        Bitmap bmp = Utility.decodeSampledBitmap(path, sizeHint.x, sizeHint.y);

        if (bmp == null) {
            return null;
        }

        return bitmapToFd(bmp);
    }

    private ParcelFileDescriptor bitmapToFd(Bitmap bmp) {
        ParcelFileDescriptor[] pair;
        try {
            // Use a pipe as a virtual in-memory ParcelFileDescriptor
            pair = ParcelFileDescriptor.createPipe();
        } catch (IOException e) {
            return null;
        }

        FileOutputStream os = new FileOutputStream(pair[1].getFileDescriptor());
        // Send the bitmap into the pipe in another thread, so that we can return the
        // reading fd to the Documents UI before we finish sending the Bitmap.
        new Thread(() -> {
            bmp.compress(Bitmap.CompressFormat.PNG, 100, os);
            try {
                os.flush();
                os.close();
            } catch (IOException e) {
                // ...
            }
            bmp.recycle();
        }).start();

        return pair[0];
    }
}