/*
 * Copyright (c) 2020  airsquared
 *
 * This file is part of blobsaver.
 *
 * blobsaver is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * blobsaver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with blobsaver.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.airsquared.blobsaver;

import com.sun.javafx.PlatformUtil;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.ptr.PointerByReference;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;

import static com.airsquared.blobsaver.Utils.exceptionToString;
import static com.airsquared.blobsaver.Utils.newReportableError;
import static com.airsquared.blobsaver.Utils.newUnreportableError;
import static com.airsquared.blobsaver.Utils.openURL;
import static com.airsquared.blobsaver.Utils.runSafe;

/**
 * This class provides access to functions in the native library libimobiledevice.
 * <p>
 * See the libimobiledevice docs for
 * <a href="https://www.libimobiledevice.org/docs/html/lockdown_8h.html">lockdown.h</a>
 * and
 * <a href="https://www.libimobiledevice.org/docs/html/libimobiledevice_8h.html">libimobiledevice.h</a>
 * for information on the native functions. For information on the libplist functions,
 * look at the source code(files plist.c and xplist.c).
 *
 * @author airsquared
 */
@SuppressWarnings("WeakerAccess")
public class Libimobiledevice {

    public static long getEcid(boolean showErrorAlert) {
        return Long.parseLong(getKeyFromConnectedDevice("UniqueChipID", PlistType.INTEGER, showErrorAlert));
    }

    public static String getDeviceModelIdentifier(boolean showErrorAlert) {
        return getKeyFromConnectedDevice("ProductType", PlistType.STRING, showErrorAlert);
    }

    public static String getBoardConfig(boolean showErrorAlert) {
        return getKeyFromConnectedDevice("HardwareModel", PlistType.STRING, showErrorAlert);
    }

    public static String getKeyFromConnectedDevice(String key, PlistType plistType, boolean showErrorAlert) {
        if (plistType == null) {
            plistType = PlistType.STRING;
        }
        if (key == null) {
            key = "";
        }

        Pointer client = lockdowndClientFromConnectedDevice(showErrorAlert);
        PointerByReference plist_value = new PointerByReference();
        int lockdowndGetValueErrorCode = lockdownd_get_value(client, Pointer.NULL, key, plist_value);
        if (lockdowndGetValueErrorCode == -8) {
            // try again, and if it doesn't work, show an error to the user + throw an exception
            // it always works the second time
            lockdownd_client_free(client);
            client = lockdowndClientFromConnectedDevice(showErrorAlert);
            throwIfNeeded(lockdownd_get_value(client, Pointer.NULL, key, plist_value), showErrorAlert, ErrorCodeType.lockdownd_error_t);
        } else {
            throwIfNeeded(lockdowndGetValueErrorCode, showErrorAlert, ErrorCodeType.lockdownd_error_t);
        }
        lockdownd_client_free(client);
        if (plistType.equals(PlistType.INTEGER)) {
            PointerByReference xml_doc = new PointerByReference();
            Libplist.toXml(plist_value.getValue(), xml_doc, new PointerByReference());
            Libplist.free(plist_value.getValue());
            String toReturn = xml_doc.getValue().getString(0, "UTF-8");
            return toReturn.substring(toReturn.indexOf("<integer>") + "<integer>".length(), toReturn.indexOf("</integer>"));
        } else {
            PointerByReference toReturn = new PointerByReference();
            Libplist.getStringVal(plist_value.getValue(), toReturn);
            Libplist.free(plist_value.getValue());
            return toReturn.getValue().getString(0, "UTF-8");
        }
    }

    public static Pointer lockdowndClientFromConnectedDevice(boolean showErrorAlert) {
        PointerByReference device = new PointerByReference();
        throwIfNeeded(idevice_new(device, Pointer.NULL), showErrorAlert, ErrorCodeType.idevice_error_t);
        PointerByReference client = new PointerByReference();
        throwIfNeeded(lockdownd_client_new(device.getValue(), client, "blobsaver"), showErrorAlert, ErrorCodeType.lockdownd_error_t);
        if (lockdownd_pair(client.getValue(), Pointer.NULL) != 0) {
            // try again, and if it doesn't work, show an error to the user + throw an exception
            throwIfNeeded(lockdownd_pair(client.getValue(), Pointer.NULL), showErrorAlert, ErrorCodeType.lockdownd_error_t);
        }
        idevice_free(device.getValue());
        return client.getValue();
    }

    public static void enterRecovery(boolean showErrorAlert) {
        Pointer client = lockdowndClientFromConnectedDevice(true);
        throwIfNeeded(lockdownd_enter_recovery(client), showErrorAlert, ErrorCodeType.lockdownd_error_t);
        lockdownd_client_free(client);
    }

    public static void exitRecovery(Pointer irecvClient, boolean showErrorAlert) {
        throwIfNeeded(Libirecovery.irecv_setenv(irecvClient, "auto-boot", "true"), showErrorAlert, ErrorCodeType.irecv_error_t);
        throwIfNeeded(Libirecovery.irecv_saveenv(irecvClient), showErrorAlert, ErrorCodeType.irecv_error_t);
        throwIfNeeded(Libirecovery.irecv_reboot(irecvClient), showErrorAlert, ErrorCodeType.irecv_error_t);
        throwIfNeeded(Libirecovery.irecv_close(irecvClient), showErrorAlert, ErrorCodeType.irecv_error_t);
    }

    enum PlistType {
        STRING, INTEGER
    }

    enum ErrorCodeType {
        idevice_error_t, lockdownd_error_t, irecv_error_t
    }

    public static native int lockdownd_get_value(Pointer client, Pointer domain, String key, PointerByReference value);

    public static native int lockdownd_enter_recovery(Pointer client);

    public static native int idevice_new(PointerByReference device, Pointer udid);

    public static native int lockdownd_client_new(Pointer device, PointerByReference client, String label);

    public static native int lockdownd_pair(Pointer lockdownd_client, Pointer lockdownd_pair_record);

    public static native void lockdownd_client_free(Pointer client);

    public static native void idevice_free(Pointer idevice);

    public static class Libplist {

        public static void getStringVal(Pointer plist, PointerByReference value) {
            plist_get_string_val(plist, value);
        }

        public static void free(Pointer plist) {
            plist_free(plist);
        }

        public static void toXml(Pointer plist, PointerByReference plist_xml, PointerByReference length) {
            plist_to_xml(plist, plist_xml, length);
        }

        private static native void plist_get_string_val(Pointer plist, PointerByReference value);

        private static native void plist_free(Pointer plist);

        private static native void plist_to_xml(Pointer plist, PointerByReference plist_xml, PointerByReference length);

        static {
            Native.register("plist");
        }
    }

    public static class Libirecovery {

        public static native int irecv_open_with_ecid(PointerByReference irecv_client, long ecid);

        public static native int irecv_close(Pointer irecv_client);

        public static native int irecv_setenv(Pointer irecv_client, String variable, String value);

        public static native int irecv_saveenv(Pointer irecv_client);

        public static native int irecv_reboot(Pointer irecv_client);

        public static native irecv_device_info irecv_get_device_info(Pointer irecv_client);

        @SuppressWarnings({"unused", "SpellCheckingInspection"})
        @Structure.FieldOrder({"cpid", "cprv", "cpfm", "scep", "bdid", "ecid", "ibfl", "srnm", "imei", "srtg", "serial_string", "ap_nonce", "ap_nonce_size", "sep_nonce", "sep_nonce_size"})
        public static class irecv_device_info extends Structure {
            public int cpid, cprv, cpfm, scep, bdid;
            public long ecid;
            public int ibfl;
            public String srnm, imei, srtg, serial_string;
            public Pointer ap_nonce;
            public int ap_nonce_size;
            public Pointer sep_nonce;
            public int sep_nonce_size;
        }

        static {
            Native.register("irecovery");
        }
    }

    public static void throwIfNeeded(int errorCode, boolean showAlert, ErrorCodeType errorType) {
        if (errorCode == 0) {
            return;
        }
        if (errorType == null) {
            throw new IllegalArgumentException("errorType cannot be null");
        }

        String exceptionMessage = "";
        String alertMessage = "";
        boolean reportableError = false;
        if (errorType.equals(ErrorCodeType.idevice_error_t)) {
            if (errorCode == -3) { // IDEVICE_E_NO_DEVICE
                exceptionMessage = "idevice error: no device found/connected (IDEVICE_E_NO_DEVICE)";
                alertMessage = "Error: No devices found/connected. Make sure your device is connected via USB and unlocked.";
                if (PlatformUtil.isWindows()) {
                    alertMessage = alertMessage + "\n\nEnsure iTunes or Apple's iOS Drivers are installed.";
                }
                if (showAlert) {
                    ButtonType downloadItunes = new ButtonType("Download iTunes");
                    Alert alert = new Alert(Alert.AlertType.ERROR, alertMessage, downloadItunes, ButtonType.OK);
                    if (downloadItunes.equals(alert.showAndWait().orElse(null))) {
                        openURL("https://www.apple.com/itunes/download/win64");
                    }
                    showAlert = false;
                }
            } else {
                exceptionMessage = "idevice error: code=" + errorCode;
                alertMessage = exceptionMessage;
                reportableError = true;
            }
        } else if (errorType.equals(ErrorCodeType.lockdownd_error_t)) {
            switch (errorCode) {
                case -17:  // LOCKDOWN_E_PASSWORD_PROTECTED
                    exceptionMessage = "lockdownd error: LOCKDOWN_E_PASSWORD_PROTECTED (-17)";
                    alertMessage = "Error: The device is locked.\n\nPlease unlock your device and go to the homescreen then try again.";
                    break;
                case -18:  // LOCKDOWN_E_USER_DENIED_PAIRING
                    exceptionMessage = "lockdownd error: LOCKDOWN_E_USER_DENIED_PAIRING (-18)";
                    alertMessage = "Error: The user denied the trust/pair request on the device. Please unplug your device and accept the dialog next time.";
                    break;
                case -19:  // LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING
                    exceptionMessage = "lockdownd error: LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING (-19)";
                    alertMessage = "Error: Please accept the trust/pair dialog on the device and try again.";
                    break;
                case -8:  // LOCKDOWN_E_MUX_ERROR
                    exceptionMessage = "lockdownd error: LOCKDOWN_E_MUX_ERROR (-8)";
                    alertMessage = exceptionMessage;
                    reportableError = true;
                    break;
                default:
                    exceptionMessage = "lockdownd error: code=" + errorCode;
                    alertMessage = exceptionMessage;
                    reportableError = true;
                    break;
            }
        } else if (errorType.equals(ErrorCodeType.irecv_error_t)) {
            exceptionMessage = "irecovery error: code=" + errorCode;
            alertMessage = exceptionMessage + "\n\nIf your device is still in recovery mode, use the \"Exit Recovery Mode\" option from the help menu.";
            reportableError = true;
        }
        if (showAlert) {
            // temporary final variables are required because of the lambda
            final boolean finalReportableError = reportableError;
            final String finalAlertMessage = alertMessage;
            runSafe(() -> {
                if (finalReportableError) {
                    newReportableError(finalAlertMessage);
                } else {
                    newUnreportableError(finalAlertMessage);
                }
            });
        }
        throw new LibimobiledeviceException(exceptionMessage);
    }

    static {
        try {
            Native.register("imobiledevice");
        } catch (Throwable e) { // need to catch UnsatisfiedLinkError
            newReportableError("Error: unable to register native methods", exceptionToString(e));
            throw new LibimobiledeviceException("Unable to register native methods", e);
        }
    }

    @SuppressWarnings("unused")
    public static class LibimobiledeviceException extends RuntimeException {
        public LibimobiledeviceException() {
            super();
        }

        public LibimobiledeviceException(String message) { super(message); }

        public LibimobiledeviceException(String message, Throwable cause) {
            super(message, cause);
        }

        public LibimobiledeviceException(Throwable cause) {
            super(cause);
        }
    }

}