/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright (C) 2006-2007 Red Hat, Inc.
 * Copyright (C) 2008 Novell, 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.1 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/>.
 *
 * Author: Alexander Larsson <alexl@redhat.com>
 * Author: Tor Lillqvist <tml@novell.com>
 */

#include "config.h"

#include <wchar.h>

#include "gio/gioerror.h"
#include "gio/giomodule.h"
#include "gio/gvfs.h"

#include "gwinhttpfile.h"
#include "gwinhttpvfs.h"

static gboolean lookup_done = FALSE;
static gboolean funcs_found = FALSE;
static GWinHttpDllFuncs funcs;

static void
lookup_funcs (void)
{
  HMODULE winhttp = NULL;
  WCHAR winhttp_dll[MAX_PATH + 100];
  int n;

  if (lookup_done)
    return;

  n = GetSystemDirectoryW (winhttp_dll, MAX_PATH);
  if (n > 0 && n < MAX_PATH)
    {
        if (winhttp_dll[n-1] != L'\\' &&
            winhttp_dll[n-1] != L'/')
            wcscat (winhttp_dll, L"\\");
        wcscat (winhttp_dll, L"winhttp.dll");
        winhttp = LoadLibraryW (winhttp_dll);
    }

  if (winhttp != NULL)
    {
      funcs.pWinHttpCloseHandle = (BOOL (WINAPI *) (HINTERNET)) GetProcAddress (winhttp, "WinHttpCloseHandle");
      funcs.pWinHttpCrackUrl = (BOOL (WINAPI *) (LPCWSTR,DWORD,DWORD,LPURL_COMPONENTS)) GetProcAddress (winhttp, "WinHttpCrackUrl");
      funcs.pWinHttpConnect = (HINTERNET (WINAPI *) (HINTERNET,LPCWSTR,INTERNET_PORT,DWORD)) GetProcAddress (winhttp, "WinHttpConnect");
      funcs.pWinHttpCreateUrl = (BOOL (WINAPI *) (LPURL_COMPONENTS,DWORD,LPWSTR,LPDWORD)) GetProcAddress (winhttp, "WinHttpCreateUrl");
      funcs.pWinHttpOpen = (HINTERNET (WINAPI *) (LPCWSTR,DWORD,LPCWSTR,LPCWSTR,DWORD)) GetProcAddress (winhttp, "WinHttpOpen");
      funcs.pWinHttpOpenRequest = (HINTERNET (WINAPI *) (HINTERNET,LPCWSTR,LPCWSTR,LPCWSTR,LPCWSTR,LPCWSTR*,DWORD)) GetProcAddress (winhttp, "WinHttpOpenRequest");
      funcs.pWinHttpQueryDataAvailable = (BOOL (WINAPI *) (HINTERNET,LPDWORD)) GetProcAddress (winhttp, "WinHttpQueryDataAvailable");
      funcs.pWinHttpQueryHeaders = (BOOL (WINAPI *) (HINTERNET,DWORD,LPCWSTR,LPVOID,LPDWORD,LPDWORD)) GetProcAddress (winhttp, "WinHttpQueryHeaders");
      funcs.pWinHttpReadData = (BOOL (WINAPI *) (HINTERNET,LPVOID,DWORD,LPDWORD)) GetProcAddress (winhttp, "WinHttpReadData");
      funcs.pWinHttpReceiveResponse = (BOOL (WINAPI *) (HINTERNET,LPVOID)) GetProcAddress (winhttp, "WinHttpReceiveResponse");
      funcs.pWinHttpSendRequest = (BOOL (WINAPI *) (HINTERNET,LPCWSTR,DWORD,LPVOID,DWORD,DWORD,DWORD_PTR)) GetProcAddress (winhttp, "WinHttpSendRequest");
      funcs.pWinHttpWriteData = (BOOL (WINAPI *) (HINTERNET,LPCVOID,DWORD,LPDWORD)) GetProcAddress (winhttp, "WinHttpWriteData");

      if (funcs.pWinHttpCloseHandle &&
	  funcs.pWinHttpCrackUrl &&
	  funcs.pWinHttpConnect &&
	  funcs.pWinHttpCreateUrl &&
	  funcs.pWinHttpOpen &&
	  funcs.pWinHttpOpenRequest &&
	  funcs.pWinHttpQueryDataAvailable &&
	  funcs.pWinHttpQueryHeaders &&
	  funcs.pWinHttpReadData &&
	  funcs.pWinHttpReceiveResponse &&
	  funcs.pWinHttpSendRequest &&
	  funcs.pWinHttpWriteData)
	funcs_found = TRUE;
    }
  lookup_done = TRUE;
}

#define g_winhttp_vfs_get_type _g_winhttp_vfs_get_type
G_DEFINE_TYPE_WITH_CODE (GWinHttpVfs, g_winhttp_vfs, G_TYPE_VFS,
			 {
			   lookup_funcs ();
			   if (funcs_found)
			     g_io_extension_point_implement (G_VFS_EXTENSION_POINT_NAME,
							     g_define_type_id,
							     "winhttp",
							     10);
			 })

static const gchar *winhttp_uri_schemes[] = { "http", "https" };

static void
g_winhttp_vfs_finalize (GObject *object)
{
  GWinHttpVfs *vfs;

  vfs = G_WINHTTP_VFS (object);

  (G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpCloseHandle) (vfs->session);
  vfs->session = NULL;

  if (vfs->wrapped_vfs)
    g_object_unref (vfs->wrapped_vfs);
  vfs->wrapped_vfs = NULL;

  G_OBJECT_CLASS (g_winhttp_vfs_parent_class)->finalize (object);
}

static void
g_winhttp_vfs_init (GWinHttpVfs *vfs)
{
  wchar_t *wagent;
  const gchar *prgname = g_get_prgname ();

  vfs->wrapped_vfs = g_vfs_get_local ();

  if (prgname)
    wagent = g_utf8_to_utf16 (prgname, -1, NULL, NULL, NULL);
  else
    wagent = g_utf8_to_utf16 ("GWinHttpVfs", -1, NULL, NULL, NULL);

  vfs->session = (G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpOpen)
    (wagent,
     WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
     WINHTTP_NO_PROXY_NAME,
     WINHTTP_NO_PROXY_BYPASS,
     0);

  g_free (wagent);
}

/**
 * g_winhttp_vfs_new:
 *
 * Returns a new #GVfs handle for a WinHttp vfs.
 *
 * Returns: a new #GVfs handle.
 **/
GVfs *
_g_winhttp_vfs_new (void)
{
  return g_object_new (G_TYPE_WINHTTP_VFS, NULL);
}

static GFile *
g_winhttp_vfs_get_file_for_path (GVfs       *vfs,
                                 const char *path)
{
  return g_vfs_get_file_for_path (G_WINHTTP_VFS (vfs)->wrapped_vfs, path);
}

static GFile *
g_winhttp_vfs_get_file_for_uri (GVfs       *vfs,
                                const char *uri)
{
  GWinHttpVfs *winhttp_vfs = G_WINHTTP_VFS (vfs);
  gsize i;
  GFile *ret = NULL;

  /* If it matches one of "our" schemes, handle it */
  for (i = 0; i < G_N_ELEMENTS (winhttp_uri_schemes); i++)
    {
      if (g_ascii_strncasecmp (uri, winhttp_uri_schemes[i], strlen (winhttp_uri_schemes[i])) == 0 &&
          uri[strlen (winhttp_uri_schemes[i])] == ':')
        {
          ret = _g_winhttp_file_new (winhttp_vfs, uri);
        }
    }

  /* For other URIs fallback to the wrapped GVfs */
  if (ret == NULL)
    ret = g_vfs_get_file_for_uri (winhttp_vfs->wrapped_vfs, uri);

  g_assert (ret != NULL);

  return g_steal_pointer (&ret);
}

static const gchar * const *
g_winhttp_vfs_get_supported_uri_schemes (GVfs *vfs)
{
  GWinHttpVfs *winhttp_vfs = G_WINHTTP_VFS (vfs);
  const gchar * const *wrapped_vfs_uri_schemes = g_vfs_get_supported_uri_schemes (winhttp_vfs->wrapped_vfs);
  gsize i, n;
  const gchar **retval;

  n = 0;
  while (wrapped_vfs_uri_schemes[n] != NULL)
    n++;

  retval = g_new (const gchar *, n + G_N_ELEMENTS (winhttp_uri_schemes) + 1);
  n = 0;
  while (wrapped_vfs_uri_schemes[n] != NULL)
    {
      retval[n] = wrapped_vfs_uri_schemes[n];
      n++;
    }

  for (i = 0; i < G_N_ELEMENTS (winhttp_uri_schemes); i++)
    {
      retval[n] = winhttp_uri_schemes[i];
      n++;
    }

  retval[n] = NULL;

  return retval;
}

static GFile *
g_winhttp_vfs_parse_name (GVfs       *vfs,
                          const char *parse_name)
{
  GWinHttpVfs *winhttp_vfs = G_WINHTTP_VFS (vfs);

  g_return_val_if_fail (G_IS_VFS (vfs), NULL);
  g_return_val_if_fail (parse_name != NULL, NULL);

  /* For plain file paths fallback to the wrapped GVfs */
  if (g_path_is_absolute (parse_name))
    return g_vfs_parse_name (winhttp_vfs->wrapped_vfs, parse_name);

  /* Otherwise assume it is an URI, so pass on to
   * g_winhttp_vfs_get_file_for_uri().
   */
  return g_winhttp_vfs_get_file_for_uri (vfs, parse_name);
}

static gboolean
g_winhttp_vfs_is_active (GVfs *vfs)
{
  return TRUE;
}

static void
g_winhttp_vfs_class_init (GWinHttpVfsClass *class)
{
  GObjectClass *object_class;
  GVfsClass *vfs_class;

  object_class = (GObjectClass *) class;

  object_class->finalize = g_winhttp_vfs_finalize;

  vfs_class = G_VFS_CLASS (class);

  vfs_class->is_active = g_winhttp_vfs_is_active;
  vfs_class->get_file_for_path = g_winhttp_vfs_get_file_for_path;
  vfs_class->get_file_for_uri = g_winhttp_vfs_get_file_for_uri;
  vfs_class->get_supported_uri_schemes = g_winhttp_vfs_get_supported_uri_schemes;
  vfs_class->parse_name = g_winhttp_vfs_parse_name;

  lookup_funcs ();
  if (funcs_found)
    class->funcs = &funcs;
  else
    class->funcs = NULL;
}

char *
_g_winhttp_error_message (DWORD error_code)
{
  /* The FormatMessage() API that g_win32_error_message() uses doesn't
   * seem to know about WinHttp errors, unfortunately.
   */
  if (error_code >= WINHTTP_ERROR_BASE && error_code < WINHTTP_ERROR_BASE + 200)
    {
      switch (error_code)
        {
          /* FIXME: Use meaningful error messages */
#define CASE(x) case ERROR_WINHTTP_##x: return g_strdup ("WinHttp error: " #x);
          CASE (AUTO_PROXY_SERVICE_ERROR);
          CASE (AUTODETECTION_FAILED);
          CASE (BAD_AUTO_PROXY_SCRIPT);
          CASE (CANNOT_CALL_AFTER_OPEN);
          CASE (CANNOT_CALL_AFTER_SEND);
          CASE (CANNOT_CALL_BEFORE_OPEN);
          CASE (CANNOT_CALL_BEFORE_SEND);
          CASE (CANNOT_CONNECT);
          CASE (CHUNKED_ENCODING_HEADER_SIZE_OVERFLOW);
          CASE (CLIENT_AUTH_CERT_NEEDED);
          CASE (CONNECTION_ERROR);
          CASE (HEADER_ALREADY_EXISTS);
          CASE (HEADER_COUNT_EXCEEDED);
          CASE (HEADER_NOT_FOUND);
          CASE (HEADER_SIZE_OVERFLOW);
          CASE (INCORRECT_HANDLE_STATE);
          CASE (INCORRECT_HANDLE_TYPE);
          CASE (INTERNAL_ERROR);
          CASE (INVALID_OPTION);
          CASE (INVALID_QUERY_REQUEST);
          CASE (INVALID_SERVER_RESPONSE);
          CASE (INVALID_URL);
          CASE (LOGIN_FAILURE);
          CASE (NAME_NOT_RESOLVED);
          CASE (NOT_INITIALIZED);
          CASE (OPERATION_CANCELLED);
          CASE (OPTION_NOT_SETTABLE);
          CASE (OUT_OF_HANDLES);
          CASE (REDIRECT_FAILED);
          CASE (RESEND_REQUEST);
          CASE (RESPONSE_DRAIN_OVERFLOW);
          CASE (SECURE_CERT_CN_INVALID);
          CASE (SECURE_CERT_DATE_INVALID);
          CASE (SECURE_CERT_REV_FAILED);
          CASE (SECURE_CERT_REVOKED);
          CASE (SECURE_CERT_WRONG_USAGE);
          CASE (SECURE_CHANNEL_ERROR);
          CASE (SECURE_FAILURE);
          CASE (SECURE_INVALID_CA);
          CASE (SECURE_INVALID_CERT);
          CASE (SHUTDOWN);
          CASE (TIMEOUT);
          CASE (UNABLE_TO_DOWNLOAD_SCRIPT);
          CASE (UNRECOGNIZED_SCHEME);
          #undef CASE
        default:
          return g_strdup_printf ("WinHttp error %ld", error_code);
        }
    }
  else
    return g_win32_error_message (error_code);
}

void
_g_winhttp_set_error (GError     **error,
                      DWORD        error_code,
                      const char  *what)
{
  char *emsg = _g_winhttp_error_message (error_code);

  g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
               "%s failed: %s", what, emsg);
  g_free (emsg);
}

gboolean
_g_winhttp_response (GWinHttpVfs *vfs,
                     HINTERNET    request,
                     GError     **error,
                     const char  *what)
{
  wchar_t *status_code;
  DWORD status_code_len;

  if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpReceiveResponse (request, NULL))
    {
      _g_winhttp_set_error (error, GetLastError (), what);

      return FALSE;
    }

  status_code_len = 0;
  if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpQueryHeaders
      (request,
       WINHTTP_QUERY_STATUS_CODE,
       NULL,
       NULL,
       &status_code_len,
       NULL) &&
      GetLastError () != ERROR_INSUFFICIENT_BUFFER)
    {
      _g_winhttp_set_error (error, GetLastError (), what);

      return FALSE;
    }

  status_code = g_malloc (status_code_len);

  if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpQueryHeaders
      (request,
       WINHTTP_QUERY_STATUS_CODE,
       NULL,
       status_code,
       &status_code_len,
       NULL))
    {
      _g_winhttp_set_error (error, GetLastError (), what);
      g_free (status_code);

      return FALSE;
    }

  if (status_code[0] != L'2')
    {
      wchar_t *status_text = NULL;
      DWORD status_text_len;

      if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpQueryHeaders
          (request,
           WINHTTP_QUERY_STATUS_TEXT,
           NULL,
           NULL,
           &status_text_len,
           NULL) &&
          GetLastError () == ERROR_INSUFFICIENT_BUFFER)
        {
          status_text = g_malloc (status_text_len);

          if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpQueryHeaders
              (request,
               WINHTTP_QUERY_STATUS_TEXT,
               NULL,
               status_text,
               &status_text_len,
               NULL))
            {
              g_free (status_text);
              status_text = NULL;
            }
        }

      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "%s failed: %S %S",
                   what, status_code, status_text ? status_text : L"");
      g_free (status_code);
      g_free (status_text);

      return FALSE;
    }

  g_free (status_code);

  return TRUE;
}

gboolean
_g_winhttp_query_header (GWinHttpVfs *vfs,
                         HINTERNET    request,
                         const char  *request_description,
                         DWORD        which_header,
                         wchar_t    **header,
                         GError     **error)
{
  DWORD header_len = 0;

  if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpQueryHeaders
      (request,
       which_header,
       NULL,
       NULL,
       &header_len,
       NULL) &&
      GetLastError () != ERROR_INSUFFICIENT_BUFFER)
    {
      _g_winhttp_set_error (error, GetLastError (), request_description);

      return FALSE;
    }

  *header = g_malloc (header_len);
  if (!G_WINHTTP_VFS_GET_CLASS (vfs)->funcs->pWinHttpQueryHeaders
      (request,
       which_header,
       NULL,
       *header,
       &header_len,
       NULL))
    {
      _g_winhttp_set_error (error, GetLastError (), request_description);
      g_free (*header);
      *header = NULL;

      return FALSE;
    }

  return TRUE;
}
