#include <unistd.h>             /* getpid() */
#include <stdio.h>

#include "gskghelpers.h"
#include "gskerror.h"
#include "gskmodule.h"
#include "compile-info.h"

struct _GskCompileContext
{
  char *tmp_dir;
  char *cc, *ld;
  GString *cflags;
  GString *ldflags;
  GPtrArray *packages;
  char *package_cflags, *package_ldflags;
  gboolean gdb_support;
  gboolean verbose;
};


/**
 * gsk_compile_context_new:
 *
 * Create a new compilation context.
 *
 * returns: the new compilation-context.
 */
GskCompileContext *gsk_compile_context_new ()
{
  GskCompileContext *rv = g_new (GskCompileContext, 1);
  rv->tmp_dir = NULL;
  rv->cc = g_strdup (GSK_CC);
  rv->ld = g_strdup (GSK_LD_SHLIB);
  rv->cflags = g_string_new (GSK_CFLAGS " " GSK_COMPILE_ONLY_FLAG);
  rv->ldflags = g_string_new (GSK_LD_SHLIB_FLAGS);
  rv->packages = g_ptr_array_new ();
  rv->package_cflags = NULL;
  rv->package_ldflags = NULL;
  rv->gdb_support = FALSE;
  rv->verbose = FALSE;
  return rv;
}

/**
 * gsk_compile_context_add_cflags:
 * @context: the compilation context.
 * @flags: a space-separated list of compiler flags.
 * This will be passed through the shell.
 *
 * Add compiler flags that you want used in
 * this compilation-context.
 */
void 
gsk_compile_context_add_cflags (GskCompileContext *context,
                                const char        *flags)
{
  g_string_append_c (context->cflags, ' ');
  g_string_append (context->cflags, flags);
}

/**
 * gsk_compile_context_add_ldflags:
 * @context: the compilation context.
 * @flags: a space-separated list of linker flags.
 * This will be passed through the shell.
 *
 * Add linker flags that you want used in
 * this compilation-context.
 */
void               gsk_compile_context_add_ldflags(GskCompileContext*context,
                                                   const char *flags)
{
  g_string_append_c (context->ldflags, ' ');
  g_string_append (context->ldflags, flags);
}

/**
 * gsk_compile_context_add_pkg:
 * @context: the compilation context.
 * @pkg: the name of a library as known by pkgconfig(1).
 *
 * Add a package-dependency for this compilation context.
 *
 * TODO error-handling.
 */
void               gsk_compile_context_add_pkg   (GskCompileContext*context,
                                                  const char *pkg)
{
  g_ptr_array_add (context->packages, g_strdup (pkg));
  if (context->package_cflags)
    {
      g_free (context->package_cflags);
      context->package_cflags = NULL;
    }
  if (context->package_ldflags)
    {
      g_free (context->package_ldflags);
      context->package_ldflags = NULL;
    }
}

/**
 * gsk_compile_context_set_tmp_dir:
 * @context: the compilation context.
 * @tmp_dir: the temporary directory to use.
 *
 * Set the directory to use for temporary files.
 */
void               gsk_compile_context_set_tmp_dir(GskCompileContext*context,
                                                   const char *tmp_dir)
{
  char *t = g_strdup (tmp_dir);
  g_free (context->tmp_dir);
  context->tmp_dir = t;
}

/**
 * gsk_compile_context_set_gdb:
 * @context: the compilation context.
 * @support: whether to support gdb by not immediately deleting temporary files.
 *
 * Set whether gdb will be supported on shared-libraries
 * created with this context.
 * The default is FALSE.
 */
void               gsk_compile_context_set_gdb    (GskCompileContext *context,
                                                   gboolean           support)
{
  context->gdb_support = support;
}

/**
 * gsk_compile_context_set_verbose:
 * @context: the compilation context.
 * @support: whether to support be verbose.
 *
 * Set whether to be verbose.
 * When the context is verbose, compilation and link commands
 * are printed out to stderr, with "compiling: " and "linking: " prefixes.
 *
 * The default is FALSE.
 */
void               gsk_compile_context_set_verbose(GskCompileContext *context,
                                                   gboolean           support)
{
  context->verbose = support;
}

/**
 * gsk_compile_context_free:
 * @context: the compilation context to free.
 *
 * Free memory used by the compilation context.
 */
void
gsk_compile_context_free       (GskCompileContext *context)
{
  g_free (context->tmp_dir);
  g_free (context->cc);
  g_free (context->ld);
  g_string_free (context->cflags, TRUE);
  g_string_free (context->ldflags, TRUE);
  gsk_g_ptr_array_foreach (context->packages, (GFunc) g_free, NULL);
  g_ptr_array_free (context->packages, TRUE);
  g_free (context->package_cflags);
  g_free (context->package_ldflags);
  g_free (context);
}

struct _GskModule
{
  GModule *module;
  guint ref_count;
  char **files_to_kill;
};

static gboolean
run_pkg_config       (GskCompileContext *context,
                      const char        *prg_option,
                      char             **flags_out,
                      GError           **error)
{
  GString *cmd_str;
  GString *str;
  guint i;
  FILE *fp;
  char buf[4096];
  int pclose_rv;

  cmd_str = g_string_new (GSK_PKGCONFIG);
  g_string_append_printf (cmd_str, " --cflags");
  for (i = 0; i < context->packages->len; i++)
    g_string_append_printf (cmd_str, " %s",
                            (char*)(context->packages->pdata[i]));
  str = g_string_new ("");
  fp = popen (cmd_str->str, "r");
  while (fgets (buf, sizeof (buf), fp) != NULL)
    g_string_append (str, buf);
  pclose_rv = pclose (fp);
  if (pclose_rv < 0)
    g_error ("error running pkg-config");
  if (pclose_rv != 0)
    {
      if (pclose_rv < 255)
        g_set_error (error, GSK_G_ERROR_DOMAIN, GSK_ERROR_COMPILE,
                     "pkg-config died with signal %u", pclose_rv);
      else
        g_set_error (error, GSK_G_ERROR_DOMAIN, GSK_ERROR_COMPILE,
                     "pkg-config returned exit status %u", pclose_rv);
      return FALSE;
    }
  g_strstrip (str->str);
  *flags_out = g_strdup (str->str);
  g_string_free (str, TRUE);
  g_string_free (cmd_str, TRUE);
  return TRUE;
}

static gboolean
ensure_pkg_info_ok (GskCompileContext *context,
                    GError           **error)
{
  if (context->package_ldflags == NULL)
    {
      if (context->packages->len == 0)
        {
          context->package_cflags = g_strdup ("");
          context->package_ldflags = g_strdup ("");
        }
      else
        {
          if (!run_pkg_config (context, "--cflags", &context->package_cflags, error)
           || !run_pkg_config (context, "--libs", &context->package_ldflags, error))
            return FALSE;
        }
    }
  return TRUE;
}

/**
 * gsk_module_compile:
 * @context: the compilation context.
 * @n_sources: the number of source files to compile into the module.
 * @sources: the source files.
 * @flags: ...
 * @delete_sources: whether to delete the source files
 * as possible (depending on whether gdb support is enabled)
 * @program_output: the program's output.
 * @error: where to put an error if something goes wrong.
 *
 * Compile a collection of sources into a module.
 * This will invoke the compiler n_sources times,
 * and the linker once.
 *
 * If @delete_sources is set, then the source files
 * will be deleted, but when depends on whether gdb support is enabled.
 * If it is enabled, then they are deleted only once the module is closed.
 */

GskModule *
gsk_module_compile  (GskCompileContext *context,
                     guint              n_sources,
                     char             **sources,
                     GModuleFlags       flags,
                     gboolean           delete_sources,
                     char             **program_output,
                     GError           **error)
{
  GModule *module;
  GskModule *rv;
  GString *linker_cmd;
  char *output_fname;
  char **to_kill_files;
  guint i;
  GString *output;
  char buf[4096];

  /* pick unused filename for module */
  {
    static guint seq = 0;
    for (;;)
      {
        output_fname = g_strdup_printf ("%s/mod-%u-%u.so",
                                        context->tmp_dir ? context->tmp_dir
                                                         : g_get_tmp_dir (),
                                        (guint) getpid (),
                                        seq++);
        if (!g_file_test (output_fname, G_FILE_TEST_EXISTS))
          break;
        g_free (output_fname);
      }
  }

  if (!ensure_pkg_info_ok (context, error))
    {
      return NULL;              /* TODO: cleanup */
    }

  linker_cmd = g_string_new (context->ld);
  g_string_append_printf (linker_cmd, " %s %s -o '%s'",
                          context->ldflags->str,
                          context->package_ldflags,
                          output_fname);

  output = g_string_new ("");
  for (i = 0; i < n_sources; i++)
    {
      char *command = g_strdup_printf ("%s %s %s -o '%s.o' '%s' 2>&1",
                                       context->cc,
                                       context->cflags->str,
                                       context->package_cflags,
                                       sources[i], sources[i]);

      FILE *fp;
      int pclose_rv;
      if (context->verbose)
        g_printerr ("compiling: %s\n", command);
      fp = popen (command, "r");
      while (fgets (buf, sizeof (buf), fp) != NULL)
        g_string_append (output, buf);
      pclose_rv = pclose (fp);
      if (pclose_rv != 0)
        {
          g_set_error (error,
                       GSK_G_ERROR_DOMAIN,
                       GSK_ERROR_COMPILE,
                       "error compiling shlib");
          if (program_output)
            *program_output = g_string_free (output, FALSE);
          else
            g_string_free (output, TRUE);
          g_free (command);
          return NULL;
        }
      g_free (command);
      g_string_append_printf (linker_cmd, " '%s.o'", sources[i]);
    }

  /* assemble linker command */
  {
    FILE *fp;
    if (context->verbose)
      g_printerr ("linking: %s\n", linker_cmd->str);
    fp = popen (linker_cmd->str, "r");
    g_string_free (linker_cmd, TRUE);
    linker_cmd = NULL;
    while (fgets (buf, sizeof (buf), fp) != NULL)
      g_string_append (output, buf);

    if (pclose (fp) < 0)
      {
        g_set_error (error,
                     GSK_G_ERROR_DOMAIN,
                     GSK_ERROR_COMPILE,
                     "error linking shlib");
        if (program_output)
          *program_output = g_string_free (output, FALSE);
        else
          g_string_free (output, TRUE);
        return NULL;
      }
  }

  module = g_module_open (output_fname, flags);
  if (module == NULL)
    {
      g_set_error (error,
                   GSK_G_ERROR_DOMAIN,
                   GSK_ERROR_OPEN_MODULE,
                   "error opening creating module %s: %s",
                   output_fname, g_module_error ());
      return NULL;
    }

  rv = g_new (GskModule, 1);
  rv->module = module;
  rv->ref_count = 1;

  /* make the list of files to unlink. */
  {
    GPtrArray *to_kill = g_ptr_array_new ();
    if (delete_sources)
      {
        for (i = 0; i < n_sources; i++)
          g_ptr_array_add (to_kill, g_strdup (sources[i]));
      }
    for (i = 0; i < n_sources; i++)
      g_ptr_array_add (to_kill, g_strdup_printf ("%s.o", sources[i]));
    g_ptr_array_add (to_kill, output_fname);
    g_ptr_array_add (to_kill, NULL);
    to_kill_files = (char **) g_ptr_array_free (to_kill, FALSE);
  }

  if (context->gdb_support)
    {
      rv->files_to_kill = to_kill_files;
    }
  else
    {
      /* delete files immediately */
      char **at;
      for (at = to_kill_files; *at; at++)
        unlink (*at);
      g_strfreev (to_kill_files);
      rv->files_to_kill = NULL;
    }

  if (program_output)
    *program_output = g_string_free (output, FALSE);
  else
    g_string_free (output, TRUE);

  return rv;
}

GskModule *
gsk_module_open (const char *filename,
                 GModuleFlags flags,
                 GError    **error)
{
  GModule *module = g_module_open (filename, flags);
  GskModule *rv;
  if (module == NULL)
    {
      g_set_error (error,
                   GSK_G_ERROR_DOMAIN,
                   GSK_ERROR_OPEN_MODULE,
                   "error opening module %s: %s",
                   filename, g_module_error ());
      return NULL;
    }
  rv = g_new (GskModule, 1);
  rv->ref_count = 1;
  rv->files_to_kill = NULL;
  rv->module = module;
  return rv;
}

GskModule *
gsk_module_ref (GskModule *module)
{
  g_return_val_if_fail (module->ref_count > 0, module);
  ++(module->ref_count);
  return module;
}

void
gsk_module_unref (GskModule *module)
{
  g_return_if_fail (module->ref_count > 0);
  if (--(module->ref_count) == 0)
    {
      if (module->files_to_kill)
        {
          char **at;
          for (at = module->files_to_kill; *at; at++)
            unlink (*at);
          g_strfreev (module->files_to_kill);
        }
      g_module_close (module->module);
      g_free (module);
    }
}

gpointer
gsk_module_lookup (GskModule  *module,
                   const char *symbol_name)
{
  gpointer rv;
  if (g_module_symbol (module->module, symbol_name, &rv))
    return rv;
  else
    return NULL;
}


syntax highlighted by Code2HTML, v. 0.9.1