/**
 * List archived applications. This function runs synchronously.
 *
 * @see instproxy_archive
 *
 * @param client The connected installation_proxy client
 * @param client_options The client options to use, as PLIST_DICT, or NULL.
 *        Currently there are no known client options, so pass NULL here.
 * @param result Pointer that will be set to a plist containing a PLIST_DICT
 *        holding information about the archived applications found.
 *
 * @return INSTPROXY_E_SUCCESS on success or an INSTPROXY_E_* error value if
 *     an error occured.
 */
instproxy_error_t instproxy_lookup_archives(instproxy_client_t client, plist_t client_options, plist_t *result)
{
	if (!client || !client->parent || !result)
		return INSTPROXY_E_INVALID_ARG;

	instproxy_lock(client);
	instproxy_error_t res = instproxy_send_command(client, "LookupArchives", client_options, NULL, NULL);

	if (res != INSTPROXY_E_SUCCESS) {
		debug_info("could not send plist, error %d", res);
		goto leave_unlock;
	}

	res = instproxy_error(property_list_service_receive_plist(client->parent, result));
	if (res != INSTPROXY_E_SUCCESS) {
		debug_info("could not receive plist, error %d", res);
		goto leave_unlock;
	}

	res = INSTPROXY_E_SUCCESS;

leave_unlock:
	instproxy_unlock(client);
	return res;
}
/**
 * Sends a command to the device.
 * Only used internally.
 *
 * @param client The connected installation_proxy client.
 * @param command The command to execute. Required.
 * @param client_options The client options to use, as PLIST_DICT, or NULL.
 * @param appid The ApplicationIdentifier to add or NULL if not required.
 * @param package_path The installation package path or NULL if not required.
 *
 * @return INSTPROXY_E_SUCCESS on success or an INSTPROXY_E_* error value if
 *     an error occured.
 */
static instproxy_error_t instproxy_send_command(instproxy_client_t client, plist_t command)
{
	if (!client || !command)
		return INSTPROXY_E_INVALID_ARG;

	instproxy_error_t res = instproxy_error(property_list_service_send_xml_plist(client->parent, command));

	if (res != INSTPROXY_E_SUCCESS) {
		debug_info("could not send command plist, error %d", res);
		return res;
	}

	return res;
}
/**
 * Connects to the installation_proxy service on the specified device.
 *
 * @param device The device to connect to
 * @param service The service descriptor returned by lockdownd_start_service.
 * @param client Pointer that will be set to a newly allocated
 *     instproxy_client_t upon successful return.
 *
 * @return INSTPROXY_E_SUCCESS on success, or an INSTPROXY_E_* error value
 *     when an error occured.
 */
instproxy_error_t instproxy_client_new(idevice_t device, lockdownd_service_descriptor_t service, instproxy_client_t *client)
{
	property_list_service_client_t plistclient = NULL;
	instproxy_error_t err = instproxy_error(property_list_service_client_new(device, service, &plistclient));
	if (err != INSTPROXY_E_SUCCESS) {
		return err;
	}

	instproxy_client_t client_loc = (instproxy_client_t) malloc(sizeof(struct instproxy_client_private));
	client_loc->parent = plistclient;
	mutex_init(&client_loc->mutex);
	client_loc->status_updater = (thread_t)NULL;

	*client = client_loc;
	return INSTPROXY_E_SUCCESS;
}
/**
 * Send a command with specified options to the device.
 * Only used internally.
 *
 * @param client The connected installation_proxy client.
 * @param command The command to execute. Required.
 * @param client_options The client options to use, as PLIST_DICT, or NULL.
 * @param appid The ApplicationIdentifier to add or NULL if not required.
 * @param package_path The installation package path or NULL if not required.
 *
 * @return INSTPROXY_E_SUCCESS on success or an INSTPROXY_E_* error value if
 *     an error occured.
 */
static instproxy_error_t instproxy_send_command(instproxy_client_t client, const char *command, plist_t client_options, const char *appid, const char *package_path)
{
	if (!client || !command || (client_options && (plist_get_node_type(client_options) != PLIST_DICT)))
		return INSTPROXY_E_INVALID_ARG;

	plist_t dict = plist_new_dict();
	if (appid) {
		plist_dict_insert_item(dict, "ApplicationIdentifier", plist_new_string(appid));
	}
	if (client_options && (plist_dict_get_size(client_options) > 0)) {
		plist_dict_insert_item(dict, "ClientOptions", plist_copy(client_options));
	}
	plist_dict_insert_item(dict, "Command", plist_new_string(command));
	if (package_path) {
		plist_dict_insert_item(dict, "PackagePath", plist_new_string(package_path));
	}

	instproxy_error_t err = instproxy_error(property_list_service_send_xml_plist(client->parent, dict));
	plist_free(dict);
	return err;
}
/**
 * Internally used function that will synchronously receive messages from
 * the specified installation_proxy until it completes or an error occurs.
 *
 * If status_cb is not NULL, the callback function will be called each time
 * a status update or error message is received.
 *
 * @param client The connected installation proxy client
 * @param status_cb Pointer to a callback function or NULL
 * @param operation Operation name. Will be passed to the callback function
 *        in async mode or shown in debug messages in sync mode. 
 * @param user_data Callback data passed to status_cb.
 */
static instproxy_error_t instproxy_perform_operation(instproxy_client_t client, instproxy_status_cb_t status_cb, const char *operation, void *user_data)
{
	instproxy_error_t res = INSTPROXY_E_UNKNOWN_ERROR;
	int ok = 1;
	plist_t dict = NULL;

	do {
		instproxy_lock(client);
		res = instproxy_error(property_list_service_receive_plist_with_timeout(client->parent, &dict, 30000));
		instproxy_unlock(client);
		if (res != INSTPROXY_E_SUCCESS) {
			debug_info("could not receive plist, error %d", res);
			break;
		}
		if (dict) {
			/* invoke callback function */
			if (status_cb) {
				status_cb(operation, dict, user_data);
			}
			/* check for 'Error', so we can abort cleanly */
			plist_t err = plist_dict_get_item(dict, "Error");
			if (err) {
#ifndef STRIP_DEBUG_CODE
				char *err_msg = NULL;
				plist_get_string_val(err, &err_msg);
				if (err_msg) {
					debug_info("(%s): ERROR: %s", operation, err_msg);
					free(err_msg);
				}
#endif
				ok = 0;
				res = INSTPROXY_E_OP_FAILED;
			}
			/* get 'Status' */
			plist_t status = plist_dict_get_item(dict, "Status");
			if (status) {
				char *status_msg = NULL;
				plist_get_string_val(status, &status_msg);
				if (status_msg) {
					if (!strcmp(status_msg, "Complete")) {
						ok = 0;
						res = INSTPROXY_E_SUCCESS;
					}
#ifndef STRIP_DEBUG_CODE
					plist_t npercent = plist_dict_get_item(dict, "PercentComplete");
					if (npercent) {
						uint64_t val = 0;
						int percent;
						plist_get_uint_val(npercent, &val);
						percent = val;
						debug_info("(%s): %s (%d%%)", operation, status_msg, percent);
					} else {
						debug_info("(%s): %s", operation, status_msg);
					}
#endif
					free(status_msg);
				}
			}
			plist_free(dict);
			dict = NULL;
		}
	} while (ok && client->parent);

	return res;
}
/**
 * List installed applications. This function runs synchronously.
 *
 * @param client The connected installation_proxy client
 * @param client_options The client options to use, as PLIST_DICT, or NULL.
 *        Valid client options include:
 *          "ApplicationType" -> "User"
 *          "ApplicationType" -> "System"
 * @param result Pointer that will be set to a plist that will hold an array
 *        of PLIST_DICT holding information about the applications found.
 *
 * @return INSTPROXY_E_SUCCESS on success or an INSTPROXY_E_* error value if
 *     an error occured.
 */
instproxy_error_t instproxy_browse(instproxy_client_t client, plist_t client_options, plist_t *result)
{
	if (!client || !client->parent || !result)
		return INSTPROXY_E_INVALID_ARG;

	instproxy_error_t res = INSTPROXY_E_UNKNOWN_ERROR;

	instproxy_lock(client);
	res = instproxy_send_command(client, "Browse", client_options, NULL, NULL);
	if (res != INSTPROXY_E_SUCCESS) {
		debug_info("could not send plist");
		goto leave_unlock;
	}

	int browsing = 0;
	plist_t apps_array = plist_new_array();
	plist_t dict = NULL;

	do {
		browsing = 0;
		dict = NULL;
		res = instproxy_error(property_list_service_receive_plist(client->parent, &dict));
		if (res != INSTPROXY_E_SUCCESS) {
			break;
		}
		if (dict) {
			uint64_t i;
			uint64_t current_amount = 0;
			char *status = NULL;
			plist_t camount = plist_dict_get_item(dict, "CurrentAmount");
			plist_t pstatus = plist_dict_get_item(dict, "Status");
			if (camount) {
				plist_get_uint_val(camount, &current_amount);
			}
			if (current_amount > 0) {
				plist_t current_list = plist_dict_get_item(dict, "CurrentList");
				for (i = 0; current_list && (i < current_amount); i++) {
					plist_t item = plist_array_get_item(current_list, i);
					plist_array_append_item(apps_array, plist_copy(item));
				}
			}
			if (pstatus) {
				plist_get_string_val(pstatus, &status);
			}
			if (status) {
				if (!strcmp(status, "BrowsingApplications")) {
					browsing = 1;
				} else if (!strcmp(status, "Complete")) {
					debug_info("Browsing applications completed");
					res = INSTPROXY_E_SUCCESS;
				}
				free(status);
			}
			plist_free(dict);
		}
	} while (browsing);

	if (res == INSTPROXY_E_SUCCESS) {
		*result = apps_array;
	} else {
		plist_free(apps_array);
	}

leave_unlock:
	instproxy_unlock(client);
	return res;
}
/**
 * Internally used function that will synchronously receive messages from
 * the specified installation_proxy until it completes or an error occurs.
 *
 * If status_cb is not NULL, the callback function will be called each time
 * a status update or error message is received.
 *
 * @param client The connected installation proxy client
 * @param status_cb Pointer to a callback function or NULL
 * @param command Operation specificiation in plist. Will be passed to the
 *        status_cb callback.
 * @param user_data Callback data passed to status_cb.
 */
static instproxy_error_t instproxy_receive_status_loop(instproxy_client_t client, plist_t command, instproxy_status_cb_t status_cb, void *user_data)
{
	instproxy_error_t res = INSTPROXY_E_UNKNOWN_ERROR;
	int complete = 0;
	plist_t node = NULL;
	char* command_name = NULL;
	char* status_name = NULL;
	char* error_name = NULL;
	char* error_description = NULL;
	uint64_t error_code = 0;
#ifndef STRIP_DEBUG_CODE
	int percent_complete = 0;
#endif

	instproxy_command_get_name(command, &command_name);

	do {
		/* receive status response */
		instproxy_lock(client);
		res = instproxy_error(property_list_service_receive_plist_with_timeout(client->parent, &node, 1000));
		instproxy_unlock(client);

		/* break out if we have a communication problem */
		if (res != INSTPROXY_E_SUCCESS && res != INSTPROXY_E_RECEIVE_TIMEOUT) {
			debug_info("could not receive plist, error %d", res);
			break;
		}

		/* parse status response */
		if (node) {
			/* check status for possible error to allow reporting it and aborting it gracefully */
			res = instproxy_status_get_error(node, &error_name, &error_description, &error_code);
			if (res != INSTPROXY_E_SUCCESS) {
				debug_info("command: %s, error %d, code 0x%08"PRIx64", name: %s, description: \"%s\"", command_name, res, error_code, error_name, error_description ? error_description: "N/A");
				complete = 1;
			}

			if (error_name) {
				free(error_name);
				error_name = NULL;
			}

			if (error_description) {
				free(error_description);
				error_description = NULL;
			}

			/* check status from response */
			instproxy_status_get_name(node, &status_name);
			if (!status_name) {
				debug_info("failed to retrieve name from status response with error %d.", res);
				complete = 1;
			}

			if (status_name) {
				if (!strcmp(status_name, "Complete")) {
					complete = 1;
				} else {
					res = INSTPROXY_E_OP_IN_PROGRESS;
				}

#ifndef STRIP_DEBUG_CODE
				percent_complete = -1;
				instproxy_status_get_percent_complete(node, &percent_complete);
				if (percent_complete >= 0) {
					debug_info("command: %s, status: %s, percent (%d%%)", command_name, status_name, percent_complete);
				} else {
					debug_info("command: %s, status: %s", command_name, status_name);
				}
#endif
				free(status_name);
				status_name = NULL;
			}

			/* invoke status callback function */
			if (status_cb) {
				status_cb(command, node, user_data);
			}

			plist_free(node);
			node = NULL;
		}
	} while (!complete && client->parent);

	if (command_name)
		free(command_name);

	return res;
}