package com.eveningoutpost.dexdrip.utils.usb;

import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.mtp.MtpConstants;
import android.mtp.MtpDevice;
import android.mtp.MtpObjectInfo;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.support.annotation.RequiresApi;

import com.eveningoutpost.dexdrip.Models.JoH;
import com.eveningoutpost.dexdrip.Models.UserError.Log;
import com.eveningoutpost.dexdrip.UtilityModels.Inevitable;
import com.eveningoutpost.dexdrip.utils.CipherUtils;
import com.eveningoutpost.dexdrip.xdrip;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;

import lombok.Getter;

// jamorham

@RequiresApi(api = Build.VERSION_CODES.N)
public class MtpTools {

    private static final String TAG = "Usbtools-MTP";

    static synchronized MtpDevice openMTP(final UsbDevice device) {

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

        final UsbManager usbManager = (UsbManager) xdrip.getAppContext().getSystemService(Context.USB_SERVICE);

        if (usbManager == null) {
            Log.d(TAG, "usbmanager is null in openMTP");
            return null;
        }

        final MtpDevice mtpDevice = new MtpDevice(device);

        final UsbDeviceConnection usbDeviceConnection = usbManager.openDevice(device);
        try {
            if (!mtpDevice.open(usbDeviceConnection)) {
                return null;
            }
        } catch (Exception e) {
            JoH.static_toast_long("Exception opening USB: " + e);
            return null;
        }

        return mtpDevice;
    }

    private static synchronized int createDocument(final MtpDevice device, final MtpObjectInfo objectInfo,
                                                   final ParcelFileDescriptor source) {

        final MtpObjectInfo sendObjectInfoResult = device.sendObjectInfo(objectInfo);
        if (sendObjectInfoResult == null) {
            Log.e(TAG, "Null sendObjectInfoResult in create document :(");
            return -1;
        }
        Log.d(TAG, "Send object info result: " + sendObjectInfoResult.getName());

        // Association is what passes for a folder within mtp
        if (objectInfo.getFormat() != MtpConstants.FORMAT_ASSOCIATION) {
            if (!device.sendObject(sendObjectInfoResult.getObjectHandle(),
                    sendObjectInfoResult.getCompressedSize(), source)) {
                return -1;
            }
        }
        Log.d(TAG, "Success indicated with handle: " + sendObjectInfoResult.getObjectHandle());
        return sendObjectInfoResult.getObjectHandle();

    }

    public static boolean deleteIfExistsInRoot(final MtpDevice mtpDevice, final int storageId, final String filename) {
        final int handle = existsInRoot(mtpDevice, storageId, filename);
        if (handle != -1) {
            Log.d(TAG, "Deleting: " + filename + " at " + handle);
            return mtpDevice.deleteObject(handle);
        }
        return false;
    }

    public static int existsInTopLevelFolder(final MtpDevice mtpDevice, final int storageId, final String folder, final String filename) {
        final HashMap<String, Integer> folders = getTopLevelFolders(mtpDevice, storageId);
        if (folders == null || folders.get(folder) == null) {
            return -1;
        }
        return existsInFolderHandle(mtpDevice, storageId, filename, folders.get(folder));
    }

    // -1 = not found
    public static int existsInRoot(final MtpDevice mtpDevice, final int storageId, final String filename) {
        return existsInFolderHandle(mtpDevice, storageId, filename, -1);
    }

    // -1 = not found
    public static int existsInFolderHandle(final MtpDevice mtpDevice, final int storageId, final String filename, final int handle) {
        final int[] objectHandles = mtpDevice.getObjectHandles(storageId, 0, handle);
        if (objectHandles == null) {
            return -1;
        }

        if (objectHandles.length > 20 || objectHandles.length < 1) {
            Log.d(TAG, "existsInRoot() Got object handles count: " + objectHandles.length);
        }
        for (int objectHandle : objectHandles) {

            final MtpObjectInfo mtpObjectInfo = mtpDevice.getObjectInfo(objectHandle);
            if (mtpObjectInfo == null) {
                continue;
            }

            if (mtpObjectInfo.getParent() != 0) {
                continue;
            }

            if (mtpObjectInfo.getName().equalsIgnoreCase(filename)) {
                return mtpObjectInfo.getObjectHandle();
            }
        }
        return -1;
    }

    public static HashMap<String, Integer> getTopLevelFolders(final MtpDevice mtpDevice, final int storageId) {
        final int[] objectHandles = mtpDevice.getObjectHandles(storageId, MtpConstants.FORMAT_ASSOCIATION, -1);
        if (objectHandles == null) {
            return null;
        }

        Log.d(TAG, "FoldersInRoot() Got object handles count: " + objectHandles.length);

        final HashMap<String, Integer> results = new HashMap<>();

        for (int objectHandle : objectHandles) {

            final MtpObjectInfo mtpObjectInfo = mtpDevice.getObjectInfo(objectHandle);
            if (mtpObjectInfo == null) {
                continue;
            }
            if (mtpObjectInfo.getParent() != 0) {
                continue;
            }

            if (mtpObjectInfo.getFormat() == MtpConstants.FORMAT_ASSOCIATION) {
                results.put(mtpObjectInfo.getName(), mtpObjectInfo.getObjectHandle());
            }
        }
        return results;
    }

    public static int recreateFile(final String fileName, final byte[] outputBytes, final MtpDevice mtpDevice, final int storage_id, final int parent_id) {
        MtpTools.deleteIfExistsInRoot(mtpDevice, storage_id, fileName);
        return MtpTools.createFile(fileName, outputBytes, mtpDevice, storage_id, parent_id);
    }

    public static int createFile(final String fileName, final byte[] outputBytes, final MtpDevice mtpDevice, final int storage_id, final int parent_id) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            Log.e(TAG, "createFile cannot work below Android 7");
            return -1;
        }

        if (outputBytes != null) {
            ParcelFileDescriptor[] pipe = null;
            try {
                pipe = ParcelFileDescriptor.createReliablePipe();

                final FileOutputStream out = new FileOutputStream(pipe[1].getFileDescriptor());
                Inevitable.stackableTask("write-mtp-bytes", 200, () -> {
                    try {
                        Log.d(TAG, "Attempting to write: " + outputBytes.length + " bytes to: " + fileName);
                        out.write(outputBytes);
                        out.flush();

                    } catch (IOException e) {
                        Log.e(TAG, "Got io exception in writing thread");
                    } finally {
                        try {
                            out.close();
                        } catch (IOException e) {
                            Log.e(TAG, "got io exception closing in writing thread");
                        }
                    }
                });
            } catch (NullPointerException | IOException e) {
                Log.e(TAG, "IO exception or null in pipe creation: " + e);
            }

            if (pipe != null) {
                final MtpObjectInfo fileInfo = new MtpObjectInfo.Builder()
                        .setName(fileName)
                        .setFormat(MtpConstants.FORMAT_UNDEFINED)
                        .setStorageId(storage_id)
                        .setParent(parent_id)
                        .setCompressedSize(outputBytes.length).build();
                try {
                    return createDocument(mtpDevice, fileInfo, pipe[0]);
                } finally {
                    try {
                        pipe[1].close();
                    } catch (NullPointerException | IOException e) {
                        Log.d(TAG, "Exception closing pipe 1: " + e);
                    }
                    try {
                        pipe[0].close();
                    } catch (NullPointerException | IOException e) {
                        Log.d(TAG, "Exception closing pipe 0: " + e);
                    }
                }
            }
        } else {
            Log.e(TAG,"Output bytes null");
        }
        return -1;
    }

    public static int createFolder(final String fileName, final MtpDevice mtpDevice, final int storage_id) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            Log.e(TAG, "createFolder cannot work below Android 7");
            return -1;
        }

        ParcelFileDescriptor[] pipe = null;
        try {
            pipe = ParcelFileDescriptor.createReliablePipe();
        } catch (NullPointerException | IOException e) {
            Log.e(TAG, "IO exception or null in pipe creation: " + e);
        }

        if (pipe != null) {
            final MtpObjectInfo fileInfo = new MtpObjectInfo.Builder()
                    .setName(fileName)
                    .setFormat(MtpConstants.FORMAT_ASSOCIATION)
                    .setStorageId(storage_id)
                    .setCompressedSize(0).build();
            try {
                return createDocument(mtpDevice, fileInfo, pipe[0]);
            } finally {
                try {
                    pipe[1].close();
                } catch (NullPointerException | IOException e) {
                    Log.e(TAG, "Exception closing pipe 1: " + e);
                }
                try {
                    pipe[0].close();
                } catch (NullPointerException | IOException e) {
                    Log.e(TAG, "Exception closing pipe 0: " + e);
                }
            }
        }

        return -1;
    }

    public static class MtpDeviceHelper {

        @Getter
        final MtpDevice device;

        @Getter
        int[] storageVolumeIds;

        public MtpDeviceHelper(final UsbDevice usbDevice) {
            this.device = openMTP(usbDevice);
            if (this.device != null) {
                try {
                    this.storageVolumeIds = this.device.getStorageIds();
                } catch (Exception e) {
                    Log.e(TAG, "Got exception in MtpDeviceHelper constructor: " + e);
                }
            } else {
                Log.e(TAG, "Mtp device null in MtpDeviceHelper constructor");
            }
        }

        public int getFirstStorageId() {
            try {
                return storageVolumeIds[0];
            } catch (Exception e) {
                return -1;
            }
        }

        public int numberOfStorageIds() {
            try {
                return storageVolumeIds.length;
            } catch (Exception e) {
                return -1;
            }
        }

        public boolean ok() {
            return device != null;
        }

        public String name() {
            try {
                return device.getDeviceInfo().getModel();
            } catch (Exception e) {
                return "<unknown>";
            }
        }

        public String manufacturer() {
            try {
                return device.getDeviceInfo().getManufacturer();
            } catch (Exception e) {
                return "<unknown>";
            }
        }

        public String hash() {
            return CipherUtils.getSHA256(manufacturer());
        }

        public int recreateRootFile(final String filename, final byte[] data) {
            return MtpTools.recreateFile(filename, data, device, getFirstStorageId(), 0);
        }

        public boolean existsInRoot(final String filename) {
            return MtpTools.existsInRoot(device, getFirstStorageId(), filename) != -1;
        }

        public boolean existsInFolder(final String folder, final String filename) {
            return MtpTools.existsInTopLevelFolder(device, getFirstStorageId(), folder, filename) != -1;
        }

        public void close() {
            device.close();
        }
    }

}