/* GTK - The GIMP Toolkit
 * gtkcupsutils.h: Statemachine implementation of POST and GET 
 * cups calls which can be used to create a non-blocking cups API
 * Copyright (C) 2006, 2007 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"
#include <gtk/gtk.h>
#include "gtkcupsutils.h"

#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <sys/socket.h>

typedef void (*GtkCupsRequestStateFunc) (GtkCupsRequest *request);

static void _connect            (GtkCupsRequest *request);
static void _post_send          (GtkCupsRequest *request);
static void _post_write_request (GtkCupsRequest *request);
static void _post_write_data    (GtkCupsRequest *request);
static void _post_check         (GtkCupsRequest *request);
static void _post_auth          (GtkCupsRequest *request);
static void _post_read_response (GtkCupsRequest *request);

static void _get_send           (GtkCupsRequest *request);
static void _get_check          (GtkCupsRequest *request);
static void _get_auth           (GtkCupsRequest *request);
static void _get_read_data      (GtkCupsRequest *request);

struct _GtkCupsResult
{
  gchar *error_msg;
  ipp_t *ipp_response;
  GtkCupsErrorType error_type;

  /* some error types like HTTP_ERROR have a status and a code */
  int error_status;            
  int error_code;

  guint is_error : 1;
  guint is_ipp_response : 1;
};


#define _GTK_CUPS_MAX_ATTEMPTS 10 
#define _GTK_CUPS_MAX_CHUNK_SIZE 8192

#if CUPS_VERSION_MAJOR < 3
#define cupsGetEncryption() cupsEncryption()
#define cupsGetError() cupsLastError()
#define cupsGetServer() cupsServer()
#define cupsGetUser() cupsUser()
#define cupsLangGetName(lang) (lang)->language

#define httpAddrGetLength(http_address) httpAddrLength(http_address)
#define _httpConnect(host, port, addrlist, family, encryption, blocking, msec, cancel) httpConnect2(host, port, addrlist, family, encryption, blocking, msec, cancel)
#define httpConnectAgain(http_connection, connection_timeout, cancel_variable) (httpReconnect2(http_connection, connection_timeout, cancel_variable) == 0)
#define httpGetError(http_connection) httpError(http_connection)
#define httpGetLength(http_connection) httpGetLength2(http_connection)
#define httpRead(http_connection, data_buffer, maximal_length) httpRead2(http_connection, data_buffer, maximal_length)
#define httpSetBlocking(http_connection, blocking) httpBlocking(http_connection, blocking)
#define httpSetEncryption(http_connection, encryption_preference) httpEncryption(http_connection, encryption_preference)
#define httpWrite(http_connection, data_buffer, maximal_length) httpWrite2(http_connection, data_buffer, maximal_length)

#define ippGetLength(ipp_message) ippLength(ipp_message)
#define ippGetPort() ippPort()

#define HTTP_STATUS_ERROR HTTP_ERROR
#define HTTP_STATUS_CONTINUE HTTP_CONTINUE
#define HTTP_STATUS_OK HTTP_OK
#define HTTP_STATUS_UNAUTHORIZED HTTP_UNAUTHORIZED
#define HTTP_STATUS_UPGRADE_REQUIRED HTTP_UPGRADE_REQUIRED

#define HTTP_ENCRYPTION_REQUIRED HTTP_ENCRYPT_REQUIRED

#define IPP_STATE_ERROR IPP_ERROR
#define IPP_STATE_IDLE IPP_IDLE
#define IPP_STATE_DATA IPP_DATA
#else
#define _httpConnect(host, port, addrlist, family, encryption, blocking, msec, cancel) httpConnect(host, port, addrlist, family, encryption, blocking, msec, cancel)
#endif

static GtkCupsRequestStateFunc post_states[] = {
  _connect,
  _post_send,
  _post_write_request,
  _post_write_data,
  _post_check,
  _post_auth,
  _post_read_response
};

static GtkCupsRequestStateFunc get_states[] = {
  _connect,
  _get_send,
  _get_check,
  _get_auth,
  _get_read_data
};

static void
gtk_cups_result_set_error (GtkCupsResult    *result,
                           GtkCupsErrorType  error_type,
                           int               error_status,
                           int               error_code, 
                           const char       *error_msg,
			   ...)
{
  va_list args;

  result->is_ipp_response = FALSE;
  result->is_error = TRUE;
  result->error_type = error_type;
  result->error_status = error_status;
  result->error_code = error_code;

  va_start (args, error_msg);
  result->error_msg = g_strdup_vprintf (error_msg, args);
  va_end (args);
}

GtkCupsRequest *
gtk_cups_request_new_with_username (http_t             *connection,
                                    GtkCupsRequestType  req_type, 
                                    gint                operation_id,
                                    GIOChannel         *data_io,
                                    const char         *server,
                                    const char         *resource,
                                    const char         *username)
{
  GtkCupsRequest *request;
  cups_lang_t *language;
  
  request = g_new0 (GtkCupsRequest, 1);
  request->result = g_new0 (GtkCupsResult, 1);

  request->result->error_msg = NULL;
  request->result->ipp_response = NULL;

  request->result->is_error = FALSE;
  request->result->is_ipp_response = FALSE;

  request->type = req_type;
  request->state = GTK_CUPS_REQUEST_START;

  request->password_state = GTK_CUPS_PASSWORD_NONE;

   if (server)
    request->server = g_strdup (server);
  else
    request->server = g_strdup (cupsGetServer ());

  if (resource)
    request->resource = g_strdup (resource);
  else
    request->resource = g_strdup ("/");
 
  if (connection != NULL)
    {
      request->http = connection;
      request->own_http = FALSE;
    }
  else
    {
      request->http = _httpConnect (request->server, ippGetPort (),
                                    NULL, AF_UNSPEC,
                                    cupsGetEncryption (),
                                    1, 30000, NULL);

      if (request->http)
        httpSetBlocking (request->http, 0);
        
      request->own_http = TRUE;
    }

  request->last_status = HTTP_STATUS_CONTINUE;

  request->attempts = 0;
  request->data_io = data_io;

  request->ipp_request = ippNew ();
  ippSetOperation (request->ipp_request, operation_id);
  ippSetRequestId (request->ipp_request, 1);

  language = cupsLangDefault ();

  gtk_cups_request_ipp_add_string (request, IPP_TAG_OPERATION, IPP_TAG_CHARSET,
                                   "attributes-charset", 
                                   NULL, "utf-8");
	
  gtk_cups_request_ipp_add_string (request, IPP_TAG_OPERATION, IPP_TAG_LANGUAGE,
                                   "attributes-natural-language", 
                                   NULL, cupsLangGetName (language));

  if (username != NULL)
    gtk_cups_request_ipp_add_string (request, IPP_TAG_OPERATION, IPP_TAG_NAME,
                                     "requesting-user-name",
                                     NULL, username);
  else
    gtk_cups_request_ipp_add_string (request, IPP_TAG_OPERATION, IPP_TAG_NAME,
                                     "requesting-user-name",
                                     NULL, cupsGetUser ());

  request->auth_info_required = NULL;
  request->auth_info = NULL;
  request->need_auth_info = FALSE;

#if CUPS_VERSION_MAJOR < 3
  cupsLangFree (language);
#endif

  return request;
}

GtkCupsRequest *
gtk_cups_request_new (http_t             *connection,
                      GtkCupsRequestType  req_type, 
                      gint                operation_id,
                      GIOChannel         *data_io,
                      const char         *server,
                      const char         *resource)
{
  return gtk_cups_request_new_with_username (connection,
                                             req_type,
                                             operation_id,
                                             data_io,
                                             server,
                                             resource,
                                             NULL);
}

static void
gtk_cups_result_free (GtkCupsResult *result)
{
  g_free (result->error_msg);

  if (result->ipp_response)
    ippDelete (result->ipp_response);

  g_free (result);
}

void
gtk_cups_request_free (GtkCupsRequest *request)
{
  if (request->own_http)
    {
      if (request->http)
        httpClose (request->http);
    }
  
  if (request->ipp_request)
    ippDelete (request->ipp_request);

  g_free (request->server);
  g_free (request->resource);
  if (request->password != NULL)
    {
      memset (request->password, 0, strlen (request->password));
      g_free (request->password);
    }

  g_free (request->username);
  g_strfreev (request->auth_info_required);

  gtk_cups_result_free (request->result);

  g_free (request);
}

gboolean 
gtk_cups_request_read_write (GtkCupsRequest *request, gboolean connect_only)
{
  if (connect_only && request->state != GTK_CUPS_REQUEST_START)
    return FALSE;

  do
    {
      if (request->type == GTK_CUPS_POST)
        post_states[request->state] (request);
      else if (request->type == GTK_CUPS_GET)
        get_states[request->state] (request);

      if (gtk_cups_result_is_error (request->result))
        request->state = GTK_CUPS_REQUEST_DONE;

      if (request->attempts > _GTK_CUPS_MAX_ATTEMPTS &&
          request->state != GTK_CUPS_REQUEST_DONE)
        {
          /* TODO: should add a status or error code for too many failed attempts */
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_GENERAL,
                                     0,
                                     0,
                                     "Too many failed attempts");

          request->state = GTK_CUPS_REQUEST_DONE;
        }

      if (request->state == GTK_CUPS_REQUEST_DONE)
        {
          request->poll_state = GTK_CUPS_HTTP_IDLE;
          return TRUE;
        }
    }
  /* We need to recheck using httpCheck if the poll_state is read, because
   * Cups has an internal read buffer. And if this buffer is filled, we may
   * never get a poll event again. */
  while (request->poll_state == GTK_CUPS_HTTP_READ && request->http && httpWait(request->http, 0));

  return FALSE;
}

GtkCupsPollState 
gtk_cups_request_get_poll_state (GtkCupsRequest *request)
{
  return request->poll_state;
}



GtkCupsResult *
gtk_cups_request_get_result (GtkCupsRequest *request)
{
  return request->result;
}

void            
gtk_cups_request_ipp_add_string (GtkCupsRequest *request,
                                 ipp_tag_t       group,
                                 ipp_tag_t       tag,
                                 const char     *name,
                                 const char     *charset,
                                 const char     *value)
{
  ippAddString (request->ipp_request,
                group,
                tag,
                name,
                charset,
                value);
}

void            
gtk_cups_request_ipp_add_strings (GtkCupsRequest    *request,
				  ipp_tag_t          group,
				  ipp_tag_t          tag,
				  const char        *name,
				  int                num_values,
				  const char        *charset,
				  const char *const *values)
{
  ippAddStrings (request->ipp_request,
		 group,
		 tag,
		 name,
		 num_values,
		 charset,
		 values);
}

const char *
gtk_cups_request_ipp_get_string (GtkCupsRequest *request,
                                 ipp_tag_t       tag,
                                 const char     *name)
{
  ipp_attribute_t *attribute = NULL;

  if (request != NULL && request->ipp_request != NULL)
    attribute = ippFindAttribute (request->ipp_request,
                                  name,
                                  tag);

  if (attribute != NULL && ippGetCount (attribute) > 0)
      return ippGetString (attribute, 0, NULL);
  else
    return NULL;
}


typedef struct
{
  const char	*name;
  ipp_tag_t	value_tag;
} ipp_option_t;

static const ipp_option_t ipp_options[] = {
  { "blackplot",		IPP_TAG_BOOLEAN },
  { "brightness",		IPP_TAG_INTEGER },
  { "columns",			IPP_TAG_INTEGER },
  { "copies",			IPP_TAG_INTEGER },
  { "finishings",		IPP_TAG_ENUM },
  { "fitplot",			IPP_TAG_BOOLEAN },
  { "gamma",			IPP_TAG_INTEGER },
  { "hue",			IPP_TAG_INTEGER },
  { "job-k-limit",		IPP_TAG_INTEGER },
  { "job-page-limit",		IPP_TAG_INTEGER },
  { "job-priority",		IPP_TAG_INTEGER },
  { "job-quota-period",		IPP_TAG_INTEGER },
  { "landscape",		IPP_TAG_BOOLEAN },
  { "media",			IPP_TAG_KEYWORD },
  { "mirror",			IPP_TAG_BOOLEAN },
  { "natural-scaling",		IPP_TAG_INTEGER },
  { "number-up",		IPP_TAG_INTEGER },
  { "orientation-requested",	IPP_TAG_ENUM },
  { "page-bottom",		IPP_TAG_INTEGER },
  { "page-left",		IPP_TAG_INTEGER },
  { "page-ranges",		IPP_TAG_RANGE },
  { "page-right",		IPP_TAG_INTEGER },
  { "page-top",			IPP_TAG_INTEGER },
  { "penwidth",			IPP_TAG_INTEGER },
  { "ppi",			IPP_TAG_INTEGER },
  { "prettyprint",		IPP_TAG_BOOLEAN },
  { "printer-resolution",	IPP_TAG_RESOLUTION },
  { "print-quality",		IPP_TAG_ENUM },
  { "saturation",		IPP_TAG_INTEGER },
  { "scaling",			IPP_TAG_INTEGER },
  { "sides",			IPP_TAG_KEYWORD },
  { "wrap",			IPP_TAG_BOOLEAN },
  { "number-up-layout",		IPP_TAG_INTEGER }
};


static ipp_tag_t
_find_option_tag (const gchar *option)
{
  int lower_bound, upper_bound, num_options;
  int current_option;
  ipp_tag_t result;

  result = IPP_TAG_ZERO;

  lower_bound = 0;
  upper_bound = num_options = (int) G_N_ELEMENTS (ipp_options) - 1;
  
  while (1)
    {
      int match;
      current_option = (int) (((upper_bound - lower_bound) / 2) + lower_bound);

      match = strcasecmp (option, ipp_options[current_option].name);
      if (match == 0)
        {
	  result = ipp_options[current_option].value_tag;
	  return result;
	}
      else if (match < 0)
        {
          upper_bound = current_option - 1;
	}
      else
        {
          lower_bound = current_option + 1;
	}

      if (upper_bound == lower_bound && upper_bound == current_option)
        return result;

      if (upper_bound < 0)
        return result;

      if (lower_bound > num_options)
        return result;

      if (upper_bound < lower_bound)
        return result;
    }
}

/*
 * Note that this function uses IPP_TAG_JOB, so it is
 * only suitable for IPP Group 2 attributes.
 * See RFC 2911.
 */
void
gtk_cups_request_encode_option (GtkCupsRequest *request,
                                const gchar    *option,
			        const gchar    *value)
{
  ipp_tag_t option_tag;

  g_return_if_fail (option != NULL);
  g_return_if_fail (value != NULL);

  option_tag = _find_option_tag (option);

  if (option_tag == IPP_TAG_ZERO)
    {
      option_tag = IPP_TAG_NAME;
      if (strcasecmp (value, "true") == 0 ||
          strcasecmp (value, "false") == 0)
        {
          option_tag = IPP_TAG_BOOLEAN;
        }
    }
        
  switch (option_tag)
    {
      case IPP_TAG_INTEGER:
      case IPP_TAG_ENUM:
        ippAddInteger (request->ipp_request,
                       IPP_TAG_JOB,
                       option_tag,
                       option,
                       strtol (value, NULL, 0));
        break;

      case IPP_TAG_BOOLEAN:
        {
          char b;
          
          if (strcasecmp (value, "true") == 0 ||
	      strcasecmp (value, "on") == 0 ||
	      strcasecmp (value, "yes") == 0) 
	    b = 1;
	  else
            b = 0;

          ippAddBoolean (request->ipp_request,
                         IPP_TAG_JOB,
                         option,
                         b);
        
          break;
        }
        
      case IPP_TAG_RANGE:
        {
          char	*s;
          int lower;
          int upper;
          
          if (*value == '-')
	    {
	      lower = 1;
	      s = (char *)value;
	    }
	  else
	    lower = strtol (value, &s, 0);

	  if (*s == '-')
	    {
	      if (s[1])
		upper = strtol (s + 1, NULL, 0);
	      else
		upper = 2147483647;
            }
	  else
	    upper = lower;
         
          ippAddRange (request->ipp_request,
                       IPP_TAG_JOB,
                       option,
                       lower,
                       upper);

          break;
        }

      case IPP_TAG_RESOLUTION:
        {
          char *s;
          int xres;
          int yres;
          ipp_res_t units;
          
          xres = strtol (value, &s, 0);

	  if (*s == 'x')
	    yres = strtol (s + 1, &s, 0);
	  else
	    yres = xres;

	  if (strcasecmp (s, "dpc") == 0)
            units = IPP_RES_PER_CM;
          else
            units = IPP_RES_PER_INCH;
          
          ippAddResolution (request->ipp_request,
                            IPP_TAG_JOB,
                            option,
                            units,
                            xres,
                            yres);

          break;
        }

      default:
        {
          char *values;
          char *s;
          int in_quotes;
          char *next;
          GPtrArray *strings;
          
          values = g_strdup (value);
          strings = NULL;
	  in_quotes = 0;

          for (s = values, next = s; *s != '\0'; s++)
            {
              if (in_quotes != 2 && *s == '\'')
                {
                  /* skip quoted value */
                  if (in_quotes == 0)
                    in_quotes = 1;
                  else
                    in_quotes = 0;
                }
              else if (in_quotes != 1 && *s == '\"')
                {
                  /* skip quoted value */
                  if (in_quotes == 0)
                    in_quotes = 2;
                  else
                    in_quotes = 0;
                }
              else if (in_quotes == 0 && *s == ',')
                {
                  /* found delimiter, add to value array */
                  *s = '\0';
                  if (strings == NULL)
                    strings = g_ptr_array_new ();
                  g_ptr_array_add (strings, next);
                  next = s + 1;
                }
              else if (in_quotes == 0 && *s == '\\' && s[1] != '\0')
                {
                  /* skip escaped character */
                  s++;
                }
            }
          
          if (strings == NULL)
            {
              /* single value */
              ippAddString (request->ipp_request,
                            IPP_TAG_JOB,
                            option_tag,
                            option,
                            NULL,
                            value);
            }
          else
            {
              /* multiple values */
              
              /* add last value */
              g_ptr_array_add (strings, next);
              
              ippAddStrings (request->ipp_request,
                             IPP_TAG_JOB,
                             option_tag,
                             option,
                             strings->len,
                             NULL,
                             (const char **) strings->pdata);
              g_ptr_array_free (strings, TRUE);
            }

          g_free (values);
        }

        break;
    }
}
				
void
gtk_cups_request_set_ipp_version (GtkCupsRequest     *request,
				  gint                major,
				  gint                minor)
{
  ippSetVersion (request->ipp_request, major, minor);
}

static void
_connect (GtkCupsRequest *request)
{
  request->poll_state = GTK_CUPS_HTTP_IDLE;
  request->bytes_received = 0;

  if (request->http == NULL)
    {
      request->http = _httpConnect (request->server, ippGetPort (),
                                    NULL, AF_UNSPEC,
                                    cupsGetEncryption (),
                                    1, 30000, NULL);
      if (request->http == NULL)
        request->attempts++;

      if (request->http)
        httpSetBlocking (request->http, 0);
        
      request->own_http = TRUE;
    }
  else
    {
      request->attempts = 0;
      request->state++;

      /* we always write to the socket after we get
         the connection */
      request->poll_state = GTK_CUPS_HTTP_WRITE;
    }
}

static void 
_post_send (GtkCupsRequest *request)
{
  gchar length[255];
  struct stat data_info;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  request->poll_state = GTK_CUPS_HTTP_WRITE;

  if (request->data_io != NULL)
    {
      fstat (g_io_channel_unix_get_fd (request->data_io), &data_info);
      sprintf (length, "%lu", (unsigned long) (ippGetLength (request->ipp_request) + data_info.st_size));
    }
  else
    sprintf (length, "%lu", (unsigned long) ippGetLength (request->ipp_request));
	

  httpClearFields (request->http);
  httpSetField (request->http, HTTP_FIELD_CONTENT_LENGTH, length);
  httpSetField (request->http, HTTP_FIELD_CONTENT_TYPE, "application/ipp");
  httpSetField (request->http, HTTP_FIELD_AUTHORIZATION, httpGetAuthString (request->http));

#if CUPS_VERSION_MAJOR < 3
  if (httpPost (request->http, request->resource))
#else
  if (!httpWriteRequest (request->http, "POST", request->resource))
#endif
    {
      if (!httpConnectAgain (request->http, 30000, NULL))
        {
          request->state = GTK_CUPS_POST_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;

          /* TODO: should add a status or error code for failed post */
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_GENERAL,
                                     0,
                                     0,
                                     "Failed Post");
        }

      request->attempts++;
      return;    
    }
        
    request->attempts = 0;

    request->state = GTK_CUPS_POST_WRITE_REQUEST;
    ippSetState (request->ipp_request, IPP_STATE_IDLE);
}

static void 
_post_write_request (GtkCupsRequest *request)
{
  ipp_state_t ipp_status;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  request->poll_state = GTK_CUPS_HTTP_WRITE;
  
  ipp_status = ippWrite (request->http, request->ipp_request);

  if (ipp_status == IPP_STATE_ERROR)
    {
      int cups_error = cupsGetError ();
      request->state = GTK_CUPS_POST_DONE;
      request->poll_state = GTK_CUPS_HTTP_IDLE;
 
      gtk_cups_result_set_error (request->result, 
                                 GTK_CUPS_ERROR_IPP,
                                 ipp_status,
                                 cups_error,
                                 "%s", 
                                 ippErrorString (cups_error));
      return;
    }

  if (ipp_status == IPP_STATE_DATA)
    {
      if (request->data_io != NULL)
        request->state = GTK_CUPS_POST_WRITE_DATA;
      else
        {
          request->state = GTK_CUPS_POST_CHECK;
          request->poll_state = GTK_CUPS_HTTP_READ;
	}
    }
}

static void 
_post_write_data (GtkCupsRequest *request)
{
  gsize bytes;
  char buffer[_GTK_CUPS_MAX_CHUNK_SIZE];
  http_status_t http_status;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  request->poll_state = GTK_CUPS_HTTP_WRITE;
  
  if (httpWait (request->http, 0))
    http_status = httpUpdate (request->http);
  else
    http_status = request->last_status;

  request->last_status = http_status;


  if (http_status == HTTP_STATUS_CONTINUE || http_status == HTTP_STATUS_OK)
    {
      GIOStatus io_status;
      GError *error;

      error = NULL;

      /* send data */
      io_status =
        g_io_channel_read_chars (request->data_io, 
	                         buffer, 
				 _GTK_CUPS_MAX_CHUNK_SIZE,
				 &bytes,
				 &error);

      if (io_status == G_IO_STATUS_ERROR)
        {
          request->state = GTK_CUPS_POST_DONE;
	  request->poll_state = GTK_CUPS_HTTP_IDLE;
     
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_IO,
                                     io_status,
                                     error->code, 
                                     "Error reading from cache file: %s",
                                     error->message);

	  g_error_free (error);
          return;
	}
      else if (bytes == 0 && io_status == G_IO_STATUS_EOF)
        {
          request->state = GTK_CUPS_POST_CHECK;
	  request->poll_state = GTK_CUPS_HTTP_READ;

          request->attempts = 0;
          return;
        }


      if (httpWrite (request->http, buffer, bytes) < bytes)
        {
          int http_errno;

          http_errno = httpGetError (request->http);

          request->state = GTK_CUPS_POST_DONE;
	  request->poll_state = GTK_CUPS_HTTP_IDLE;
     
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_HTTP,
                                     http_status,
                                     http_errno, 
                                     "Error writing to socket in Post %s", 
                                     g_strerror (http_errno));
          return;
        }
    }
  else if (http_status == HTTP_STATUS_UNAUTHORIZED)
    {
      request->state = GTK_CUPS_POST_CHECK;
      request->poll_state = GTK_CUPS_HTTP_READ;

      request->attempts = 0;
      return;
    }
  else
    {
      request->attempts++;
    }
}

static void
_post_auth (GtkCupsRequest *request)
{
  if (request->password_state == GTK_CUPS_PASSWORD_HAS)
    {
      if (request->password == NULL)
        {
          request->state = GTK_CUPS_POST_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;

          gtk_cups_result_set_error (request->result, 
                                     GTK_CUPS_ERROR_AUTH,
                                     0,
                                     1,
                                     "Canceled by user");
        }
      else
        request->state = GTK_CUPS_POST_CHECK;
    }
}

static void
_get_auth (GtkCupsRequest *request)
{
  if (request->password_state == GTK_CUPS_PASSWORD_HAS)
    {
      if (request->password == NULL)
        {
          request->state = GTK_CUPS_GET_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;

          gtk_cups_result_set_error (request->result, 
                                     GTK_CUPS_ERROR_AUTH,
                                     0,
                                     1,
                                     "Canceled by user");
        }
      else
        request->state = GTK_CUPS_GET_CHECK;
    }
}

/* Very ugly hack: cups has a stupid synchronous password callback 
 * that doesn't even take the request or user data parameters, so 
 * we have to use a static variable to pass the password to it.
 * Not threadsafe !
 * The callback sets cups_password to NULL to signal that the 
 * password has been used.
 */
static char *cups_password = NULL;
static char *cups_username = NULL;

static const char *
#if CUPS_VERSION_MAJOR < 3
passwordCB (const char *prompt)
#else
passwordCB (const char *prompt,
            http_t     *http,
            const char *method,
            const char *resource,
            void       *user_data)
#endif
{
  char *pwd = cups_password;
  cups_password = NULL;

  cupsSetUser (cups_username);

  return pwd;
}

static void 
_post_check (GtkCupsRequest *request)
{
  http_status_t http_status;

  http_status = request->last_status;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s - status %i\n", G_STRFUNC, http_status));

  request->poll_state = GTK_CUPS_HTTP_READ;

  if (http_status == HTTP_STATUS_CONTINUE)
    {
      goto again; 
    }
  else if (http_status == HTTP_STATUS_UNAUTHORIZED)
    {
      int auth_result = -1;
      httpFlush (request->http);

      if (request->password_state == GTK_CUPS_PASSWORD_APPLIED)
        {
          request->poll_state = GTK_CUPS_HTTP_IDLE;
          request->password_state = GTK_CUPS_PASSWORD_NOT_VALID;
          request->state = GTK_CUPS_POST_AUTH;
          request->need_password = TRUE;

          return;
        }

      /* Negotiate */
      if (strncmp (httpGetField (request->http, HTTP_FIELD_WWW_AUTHENTICATE), "Negotiate", 9) == 0)
        {
          auth_result = cupsDoAuthentication (request->http, "POST", request->resource);
        }
      /* Basic, BasicDigest, Digest and PeerCred */
      else
        {
          if (request->password_state == GTK_CUPS_PASSWORD_NONE)
            {
              cups_username = request->username;
#if CUPS_VERSION_MAJOR < 3
              cupsSetPasswordCB (passwordCB);
#else
              cupsSetPasswordCB (passwordCB, NULL);
#endif

              /* This call success for PeerCred authentication */
              auth_result = cupsDoAuthentication (request->http, "POST", request->resource);

              if (auth_result != 0)
                {
                  /* move to AUTH state to let the backend 
                   * ask for a password
                   */ 
                  request->poll_state = GTK_CUPS_HTTP_IDLE;
                  request->state = GTK_CUPS_POST_AUTH;
                  request->need_password = TRUE;

                  return;
                }
            }
          else
            {
              cups_password = request->password;
              cups_username = request->username;

              auth_result = cupsDoAuthentication (request->http, "POST", request->resource);

              if (cups_password != NULL)
                return;

              if (request->password != NULL)
                {
                  memset (request->password, 0, strlen (request->password));
                  g_free (request->password);
                  request->password = NULL;
                }

              request->password_state = GTK_CUPS_PASSWORD_APPLIED;
            }
        }

      if (auth_result ||
          !httpConnectAgain (request->http, 30000, NULL))
        {
          /* if the password has been used, reset password_state
           * so that we ask for a new one next time around
           */ 
          if (cups_password == NULL)
            request->password_state = GTK_CUPS_PASSWORD_NONE;

          request->state = GTK_CUPS_POST_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;
          gtk_cups_result_set_error (request->result, 
                                     GTK_CUPS_ERROR_AUTH,
                                     0,
                                     0,
                                     "Not authorized");
          return;
        }
      
      if (request->data_io != NULL)
        g_io_channel_seek_position (request->data_io, 0, G_SEEK_SET, NULL);

      request->state = GTK_CUPS_POST_CONNECT;
      request->poll_state = GTK_CUPS_HTTP_WRITE;
    }
  else if (http_status == HTTP_STATUS_ERROR)
    {
      int error = httpGetError (request->http);
#ifdef G_OS_WIN32
      if (error != WSAENETDOWN && error != WSAENETUNREACH)
#else
      if (error != ENETDOWN && error != ENETUNREACH)	  
#endif /* G_OS_WIN32 */
        {
          request->attempts++;
          goto again;
        }
      else
        {
          request->state = GTK_CUPS_POST_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;
     
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_HTTP,
                                     http_status,
                                     error, 
                                     "Unknown HTTP error");

          return;
        }
    }
  else if (http_status == HTTP_STATUS_UPGRADE_REQUIRED)
    {
      /* Flush any error message... */
      httpFlush (request->http);

      cupsSetEncryption (HTTP_ENCRYPTION_REQUIRED);
      request->state = GTK_CUPS_POST_CONNECT;

      /* Reconnect... */
      if (httpConnectAgain (request->http, 30000, NULL))
        {
          /* Upgrade with encryption... */
          httpSetEncryption (request->http, HTTP_ENCRYPTION_REQUIRED);
        }

      request->attempts++;
      goto again;
    }
  else if (http_status != HTTP_STATUS_OK)
    {
      int http_errno;

      http_errno = httpGetError (request->http);

      if (http_errno == EPIPE)
        request->state = GTK_CUPS_POST_CONNECT;
      else
        {
          request->state = GTK_CUPS_POST_DONE;
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_HTTP,
                                     http_status,
                                     http_errno, 
                                     "HTTP Error in POST %s", 
                                     g_strerror (http_errno));
          request->poll_state = GTK_CUPS_HTTP_IDLE;
 
          httpFlush (request->http); 
          return;
        }

      request->poll_state = GTK_CUPS_HTTP_IDLE;
      request->last_status = HTTP_STATUS_CONTINUE;

      httpFlush (request->http);
      if (request->own_http)
        httpClose (request->http);
      request->http = NULL;

      return;
    }
  else
    {
      request->state = GTK_CUPS_POST_READ_RESPONSE;
      return;
    }

 again:
  http_status = HTTP_STATUS_CONTINUE;

  if (httpWait (request->http, 0))
    http_status = httpUpdate (request->http);

  request->last_status = http_status;
}

static void 
_post_read_response (GtkCupsRequest *request)
{
  ipp_state_t ipp_status;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  request->poll_state = GTK_CUPS_HTTP_READ;

  if (request->result->ipp_response == NULL)
    request->result->ipp_response = ippNew();

  ipp_status = ippRead (request->http, 
                        request->result->ipp_response);

  if (ipp_status == IPP_STATE_ERROR)
    {
      int ipp_error = cupsGetError ();
      gtk_cups_result_set_error (request->result,  
                                 GTK_CUPS_ERROR_IPP,
                                 ipp_status,
                                 ipp_error,
                                 "%s",
                                 ippErrorString (ipp_error));
      
      ippDelete (request->result->ipp_response);
      request->result->ipp_response = NULL;

      request->state = GTK_CUPS_POST_DONE;
      request->poll_state = GTK_CUPS_HTTP_IDLE;
    }
  else if (ipp_status == IPP_STATE_DATA)
    {
      request->state = GTK_CUPS_POST_DONE;
      request->poll_state = GTK_CUPS_HTTP_IDLE;
    }
}

static void 
_get_send (GtkCupsRequest *request)
{
  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  request->poll_state = GTK_CUPS_HTTP_WRITE;

  if (request->data_io == NULL)
    {
      gtk_cups_result_set_error (request->result,
                                 GTK_CUPS_ERROR_IO,
                                 G_IO_STATUS_ERROR,
                                 G_IO_CHANNEL_ERROR_FAILED, 
                                 "Get requires an open io channel");

      request->state = GTK_CUPS_GET_DONE;
      request->poll_state = GTK_CUPS_HTTP_IDLE;

      return;
    }

  httpClearFields (request->http);
  httpSetField (request->http, HTTP_FIELD_AUTHORIZATION, httpGetAuthString (request->http));

#if CUPS_VERSION_MAJOR < 3
  if (httpGet (request->http, request->resource))
#else
  if (!httpWriteRequest (request->http, "GET", request->resource))
#endif
    {
      if (!httpConnectAgain (request->http, 30000, NULL))
        {
          request->state = GTK_CUPS_GET_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;
	 
          /* TODO: should add a status or error code for failed GET */ 
          gtk_cups_result_set_error (request->result, 
                                     GTK_CUPS_ERROR_GENERAL,
                                     0,
                                     0,
                                     "Failed Get");
        }

      request->attempts++;
      return;    
    }

  if (httpWait (request->http, 0))
    request->last_status = httpUpdate (request->http);
        
  request->attempts = 0;

  request->state = GTK_CUPS_GET_CHECK;
  request->poll_state = GTK_CUPS_HTTP_READ;
  
  ippSetState (request->ipp_request, IPP_STATE_IDLE);
}

static void 
_get_check (GtkCupsRequest *request)
{
  http_status_t http_status;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  http_status = request->last_status;

  request->poll_state = GTK_CUPS_HTTP_READ;

  if (http_status == HTTP_STATUS_CONTINUE)
    {
      goto again; 
    }
  else if (http_status == HTTP_STATUS_UNAUTHORIZED)
    {
      int auth_result = -1;
      httpFlush (request->http);

      if (request->password_state == GTK_CUPS_PASSWORD_APPLIED)
        {
          request->poll_state = GTK_CUPS_HTTP_IDLE;
          request->password_state = GTK_CUPS_PASSWORD_NOT_VALID;
          request->state = GTK_CUPS_GET_AUTH;
          request->need_password = TRUE;

          return;
        }

      /* Negotiate */
      if (strncmp (httpGetField (request->http, HTTP_FIELD_WWW_AUTHENTICATE), "Negotiate", 9) == 0)
        {
          auth_result = cupsDoAuthentication (request->http, "GET", request->resource);
        }
      /* Basic, BasicDigest, Digest and PeerCred */
      else
        {
          if (request->password_state == GTK_CUPS_PASSWORD_NONE)
            {
              cups_username = request->username;
#if CUPS_VERSION_MAJOR < 3
              cupsSetPasswordCB (passwordCB);
#else
              cupsSetPasswordCB (passwordCB, NULL);
#endif

              /* This call success for PeerCred authentication */
              auth_result = cupsDoAuthentication (request->http, "GET", request->resource);

              if (auth_result != 0)
                {
                  /* move to AUTH state to let the backend
                   * ask for a password
                   */
                  request->poll_state = GTK_CUPS_HTTP_IDLE;
                  request->state = GTK_CUPS_GET_AUTH;
                  request->need_password = TRUE;

                  return;
                }
            }
          else
            {
              cups_password = request->password;
              cups_username = request->username;

              auth_result = cupsDoAuthentication (request->http, "GET", request->resource);

              if (cups_password != NULL)
                return;

              if (request->password != NULL)
                {
                  memset (request->password, 0, strlen (request->password));
                  g_free (request->password);
                  request->password = NULL;
                }

              request->password_state = GTK_CUPS_PASSWORD_APPLIED;
            }
        }

      if (auth_result ||
          !httpConnectAgain (request->http, 30000, NULL))
        {
          /* if the password has been used, reset password_state
           * so that we ask for a new one next time around
           */
          if (cups_password == NULL)
            request->password_state = GTK_CUPS_PASSWORD_NONE;

          request->state = GTK_CUPS_GET_DONE;
          request->poll_state = GTK_CUPS_HTTP_IDLE;
          gtk_cups_result_set_error (request->result, 
                                     GTK_CUPS_ERROR_AUTH,
                                     0,
                                     0,
                                     "Not authorized");
          return;
        }

      request->state = GTK_CUPS_GET_CONNECT;
      request->last_status = HTTP_STATUS_CONTINUE;

     return;
    }
  else if (http_status == HTTP_STATUS_UPGRADE_REQUIRED)
    {
      /* Flush any error message... */
      httpFlush (request->http);

      cupsSetEncryption (HTTP_ENCRYPTION_REQUIRED);
      request->state = GTK_CUPS_GET_CONNECT;

      /* Reconnect... */
      if (httpConnectAgain (request->http, 30000, NULL))
        {
          /* Upgrade with encryption... */
          httpSetEncryption (request->http, HTTP_ENCRYPTION_REQUIRED);
        }

      request->attempts++;
      goto again;
    }
  else if (http_status != HTTP_STATUS_OK)
    {
      int http_errno;

      http_errno = httpGetError (request->http);

      if (http_errno == EPIPE)
        request->state = GTK_CUPS_GET_CONNECT;
      else
        {
          request->state = GTK_CUPS_GET_DONE;
          gtk_cups_result_set_error (request->result,
                                     GTK_CUPS_ERROR_HTTP,
                                     http_status,
                                     http_errno, 
                                     "HTTP Error in GET %s", 
                                     g_strerror (http_errno));
          request->poll_state = GTK_CUPS_HTTP_IDLE;
          httpFlush (request->http);

          return;
        }

      request->poll_state = GTK_CUPS_HTTP_IDLE;
      request->last_status = HTTP_STATUS_CONTINUE;

      httpFlush (request->http);
      if (request->own_http)
        httpClose (request->http);
      request->http = NULL;

      return;
    }
  else
    {
      request->state = GTK_CUPS_GET_READ_DATA;
      return;
    }

 again:
  http_status = HTTP_STATUS_CONTINUE;

  if (httpWait (request->http, 0))
    http_status = httpUpdate (request->http);

  request->last_status = http_status;

}

static void 
_get_read_data (GtkCupsRequest *request)
{
  char buffer[_GTK_CUPS_MAX_CHUNK_SIZE];
  gsize bytes;
  gsize bytes_written;
  GIOStatus io_status;
  GError *error;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %s\n", G_STRFUNC));

  error = NULL;

  request->poll_state = GTK_CUPS_HTTP_READ;

  bytes = httpRead (request->http, buffer, sizeof (buffer));
  request->bytes_received += bytes;

  GTK_NOTE (PRINTING,
            g_print ("CUPS Backend: %"G_GSIZE_FORMAT" bytes read\n", bytes));

  io_status =
    g_io_channel_write_chars (request->data_io, 
                              buffer, 
			      bytes, 
			      &bytes_written,
			      &error);

  if (io_status == G_IO_STATUS_ERROR)
    {
      request->state = GTK_CUPS_GET_DONE;
      request->poll_state = GTK_CUPS_HTTP_IDLE;
    
      gtk_cups_result_set_error (request->result,
                                 GTK_CUPS_ERROR_IO,
                                 io_status,
                                 error->code, 
                                 error->message);
      g_error_free (error);
    }

  /* Stop if we do not expect any more data or EOF was received. */
  if (httpGetLength (request->http) <= request->bytes_received || bytes == 0)
    {
      request->state = GTK_CUPS_GET_DONE;
      request->poll_state = GTK_CUPS_HTTP_IDLE;

      return;
    }
}

gboolean
gtk_cups_request_is_done (GtkCupsRequest *request)
{
  return (request->state == GTK_CUPS_REQUEST_DONE);
}

gboolean
gtk_cups_result_is_error (GtkCupsResult *result)
{
  return result->is_error;
}

ipp_t *
gtk_cups_result_get_response (GtkCupsResult *result)
{
  return result->ipp_response;
}

GtkCupsErrorType
gtk_cups_result_get_error_type (GtkCupsResult *result)
{
  return result->error_type;
}

int
gtk_cups_result_get_error_status (GtkCupsResult *result)
{
  return result->error_status;
}

int
gtk_cups_result_get_error_code (GtkCupsResult *result)
{
  return result->error_code;
}

const char *
gtk_cups_result_get_error_string (GtkCupsResult *result)
{
  return result->error_msg; 
}

/* This function allocates new instance of GtkCupsConnectionTest() and creates
 * a socket for communication with a CUPS server 'server'.
 */
GtkCupsConnectionTest *
gtk_cups_connection_test_new (const char *server,
                              const int   port)
{
  GtkCupsConnectionTest *result = NULL;
  gchar                 *port_str = NULL;

  result = g_new (GtkCupsConnectionTest, 1);

  if (port >= 0)
    port_str = g_strdup_printf ("%d", port);
  else
    port_str = g_strdup_printf ("%d", ippGetPort ());

  if (server != NULL)
    result->addrlist = httpAddrGetList (server, AF_UNSPEC, port_str);
  else
    result->addrlist = httpAddrGetList (cupsGetServer (), AF_UNSPEC, port_str);

  g_free (port_str);

  result->socket = -1;
  result->current_addr = NULL;
  result->last_wrong_addr = NULL;
  result->at_init = GTK_CUPS_CONNECTION_NOT_AVAILABLE;

  result->at_init = gtk_cups_connection_test_get_state (result);

  return result;
}


/* A non-blocking test whether it is possible to connect to a CUPS server specified
 * inside of GtkCupsConnectionTest structure.
 *  - you need to check it more then once.
 * The connection is closed after a successful connection.
 */
GtkCupsConnectionState 
gtk_cups_connection_test_get_state (GtkCupsConnectionTest *test)
{
  GtkCupsConnectionState result = GTK_CUPS_CONNECTION_NOT_AVAILABLE;
  http_addrlist_t       *iter;
  gint                   error_code;
  gint                   flags;
  gint                   code;

  if (test == NULL)
    return GTK_CUPS_CONNECTION_NOT_AVAILABLE;

  if (test->at_init == GTK_CUPS_CONNECTION_AVAILABLE)
    {
      test->at_init = GTK_CUPS_CONNECTION_NOT_AVAILABLE;
      return GTK_CUPS_CONNECTION_AVAILABLE;
    }
  else
    {
      if (test->socket == -1)
        {
          if (test->last_wrong_addr != NULL && test->last_wrong_addr->next != NULL)
            iter = test->last_wrong_addr->next;
          else
            {
              test->last_wrong_addr = NULL;
              iter = test->addrlist;
            }

          while (iter)
            {
              test->socket = socket (iter->addr.addr.sa_family,
                                     SOCK_STREAM,
                                     0);

              if (test->socket >= 0)
                {
                  flags = fcntl (test->socket, F_GETFL);

                  if (flags != -1)
                    flags |= O_NONBLOCK;

                  fcntl (test->socket, F_SETFL, flags);
              
                  test->current_addr = iter;
              
                  break;
                }
               iter = iter->next;
            }
        }

      if (test->socket >= 0)
        {
          code = connect (test->socket,
                          &test->current_addr->addr.addr,
                          httpAddrGetLength (&test->current_addr->addr));

          error_code = errno;

          if (code == 0 || error_code == EISCONN)
            {
              close (test->socket);
              test->socket = -1;
              test->current_addr = NULL;
              result = GTK_CUPS_CONNECTION_AVAILABLE;
            }
          else
            {
              if (error_code == EALREADY || error_code == EINPROGRESS)
                result = GTK_CUPS_CONNECTION_IN_PROGRESS;
              else
                {
                  close (test->socket);
                  test->socket = -1;
                  test->last_wrong_addr = test->current_addr;
                  result = GTK_CUPS_CONNECTION_NOT_AVAILABLE;
                }
            }
         }

      return result;
    }
}

/* This function frees memory used by the GtkCupsConnectionTest structure.
 */
void 
gtk_cups_connection_test_free (GtkCupsConnectionTest *test)
{
  if (test == NULL)
    return;

  test->current_addr = NULL;
  test->last_wrong_addr = NULL;
  httpAddrFreeList (test->addrlist);
  if (test->socket != -1)
    {
      close (test->socket);
      test->socket = -1;
    }
  g_free (test);
}
