/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright (C) 2011 Colin Walters <walters@verbum.org>
 *
 * 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, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Author: Colin Walters <walters@verbum.org>
 */

#include "config.h"

#include "ostree-repo-private.h"
#include "otutil.h"

static gboolean
add_ref_to_set (const char       *remote,
                GFile            *base,
                GFile            *child,
                GHashTable       *refs,
                GCancellable     *cancellable,
                GError          **error)
{
  gboolean ret = FALSE;
  char *contents;
  char *relpath;
  gsize len;
  GString *refname;

  if (!g_file_load_contents (child, cancellable, &contents, &len, NULL, error))
    goto out;

  g_strchomp (contents);

  refname = g_string_new ("");
  if (remote)
    {
      g_string_append (refname, remote);
      g_string_append_c (refname, ':');
    }
  relpath = g_file_get_relative_path (base, child);
  g_string_append (refname, relpath);
  g_free (relpath);
          
  g_hash_table_insert (refs, g_string_free (refname, FALSE), contents);

  ret = TRUE;
 out:
  return ret;
}

static gboolean
write_checksum_file (GFile *parentdir,
                     const char *name,
                     const char *sha256,
                     GError **error)
{
  gboolean ret = FALSE;
  gsize bytes_written;
  int i;
  gs_unref_object GFile *parent = NULL;
  gs_unref_object GFile *child = NULL;
  gs_unref_object GOutputStream *out = NULL;
  gs_unref_ptrarray GPtrArray *components = NULL;

  if (!ostree_validate_checksum_string (sha256, error))
    goto out;

  if (ostree_validate_checksum_string (name, NULL))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Rev name '%s' looks like a checksum", name);
      goto out;
    }

  if (!ot_util_path_split_validate (name, &components, error))
    goto out;

  if (components->len == 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Invalid empty ref name");
      goto out;
    }

  parent = g_object_ref (parentdir);
  for (i = 0; i+1 < components->len; i++)
    {
      child = g_file_get_child (parent, (char*)components->pdata[i]);

      if (!gs_file_ensure_directory (child, FALSE, NULL, error))
        goto out;

      g_clear_object (&parent);
      parent = child;
      child = NULL;
    }

  child = g_file_get_child (parent, components->pdata[components->len - 1]);
  if ((out = (GOutputStream*)g_file_replace (child, NULL, FALSE, 0, NULL, error)) == NULL)
    goto out;
  if (!g_output_stream_write_all (out, sha256, strlen (sha256), &bytes_written, NULL, error))
    goto out;
  if (!g_output_stream_write_all (out, "\n", 1, &bytes_written, NULL, error))
    goto out;
  if (!g_output_stream_close (out, NULL, error))
    goto out;

  ret = TRUE;
 out:
  return ret;
}


static gboolean
parse_rev_file (OstreeRepo     *self,
                GFile          *f,
                char          **sha256,
                GError        **error) G_GNUC_UNUSED;

static gboolean
parse_rev_file (OstreeRepo     *self,
                GFile          *f,
                char          **sha256,
                GError        **error)
{
  gboolean ret = FALSE;
  GError *temp_error = NULL;
  gs_free char *rev = NULL;

  if ((rev = gs_file_load_contents_utf8 (f, NULL, &temp_error)) == NULL)
    goto out;

  if (rev == NULL)
    {
      if (g_error_matches (temp_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
        {
          g_clear_error (&temp_error);
        }
      else
        {
          g_propagate_error (error, temp_error);
          goto out;
        }
    }
  else
    {
      g_strchomp (rev);
    }

  if (g_str_has_prefix (rev, "ref: "))
    {
      gs_unref_object GFile *ref = NULL;
      char *ref_sha256;
      gboolean subret;

      ref = g_file_resolve_relative_path (self->local_heads_dir, rev + 5);
      subret = parse_rev_file (self, ref, &ref_sha256, error);
        
      if (!subret)
        {
          g_free (ref_sha256);
          goto out;
        }
      
      g_free (rev);
      rev = ref_sha256;
    }
  else 
    {
      if (!ostree_validate_checksum_string (rev, error))
        goto out;
    }

  ot_transfer_out_value(sha256, &rev);
  ret = TRUE;
 out:
  return ret;
}

static gboolean
find_ref_in_remotes (OstreeRepo         *self,
                     const char         *rev,
                     GFile             **out_file,
                     GError            **error)
{
  gboolean ret = FALSE;
  gs_unref_object GFileEnumerator *dir_enum = NULL;
  gs_unref_object GFile *ret_file = NULL;

  dir_enum = g_file_enumerate_children (self->remote_heads_dir, OSTREE_GIO_FAST_QUERYINFO,
                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                        NULL, error);
  if (!dir_enum)
    goto out;

  while (TRUE)
    {
      GFileInfo *file_info;
      GFile *child;
      if (!gs_file_enumerator_iterate (dir_enum, &file_info, &child,
                                       NULL, error))
        goto out;
      if (file_info == NULL)
        break;
      if (g_file_info_get_file_type (file_info) != G_FILE_TYPE_DIRECTORY)
        continue;

      g_clear_object (&ret_file);
      ret_file = g_file_resolve_relative_path (child, rev);
      if (!g_file_query_exists (ret_file, NULL))
        g_clear_object (&ret_file);
      else
        break;
    }

  ret = TRUE;
  ot_transfer_out_value (out_file, &ret_file);
 out:
  return ret;
}

static gboolean
resolve_refspec (OstreeRepo     *self,
                 const char     *remote,
                 const char     *ref,
                 gboolean        allow_noent,
                 char          **out_rev,
                 GError        **error);

static gboolean
resolve_refspec_fallback (OstreeRepo     *self,
                          const char     *remote,
                          const char     *ref,
                          gboolean        allow_noent,
                          char          **out_rev,
                          GCancellable   *cancellable,
                          GError        **error)
{
  gboolean ret = FALSE;
  gs_free char *ret_rev = NULL;

  if (self->parent_repo)
    {
      if (!resolve_refspec (self->parent_repo, remote, ref,
                            allow_noent, &ret_rev, error))
        goto out;
    }
  else if (!allow_noent)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Refspec '%s%s%s' not found",
                   remote ? remote : "",
                   remote ? ":" : "",
                   ref);
      goto out;
    }

  ret = TRUE;
  ot_transfer_out_value (out_rev, &ret_rev);
 out:
  return ret;
}

static gboolean
resolve_refspec (OstreeRepo     *self,
                 const char     *remote,
                 const char     *ref,
                 gboolean        allow_noent,
                 char          **out_rev,
                 GError        **error)
{
  gboolean ret = FALSE;
  __attribute__((unused)) GCancellable *cancellable = NULL;
  GError *temp_error = NULL;
  gs_free char *tmp = NULL;
  gs_free char *tmp2 = NULL;
  gs_free char *ret_rev = NULL;
  gs_unref_object GFile *child = NULL;
  gs_unref_object GFile *origindir = NULL;
  
  g_return_val_if_fail (ref != NULL, FALSE);

  /* We intentionally don't allow a ref that looks like a checksum */
  if (ostree_validate_checksum_string (ref, NULL))
    {
      ret_rev = g_strdup (ref);
    }
  else if (remote != NULL)
    {
      child = ot_gfile_resolve_path_printf (self->remote_heads_dir, "%s/%s",
                                            remote, ref);
      if (!g_file_query_exists (child, NULL))
        g_clear_object (&child);
    }
  else
    {
      child = g_file_resolve_relative_path (self->local_heads_dir, ref);

      if (!g_file_query_exists (child, NULL))
        {
          g_clear_object (&child);

          child = g_file_resolve_relative_path (self->remote_heads_dir, ref);

          if (!g_file_query_exists (child, NULL))
            {
              g_clear_object (&child);
              
              if (!find_ref_in_remotes (self, ref, &child, error))
                goto out;
            }
        }
    }

  if (child)
    {
      if ((ret_rev = gs_file_load_contents_utf8 (child, NULL, &temp_error)) == NULL)
        {
          g_propagate_error (error, temp_error);
          g_prefix_error (error, "Couldn't open ref '%s': ", gs_file_get_path_cached (child));
          goto out;
        }

      g_strchomp (ret_rev);
      if (!ostree_validate_checksum_string (ret_rev, error))
        goto out;
    }
  else
    {
      if (!resolve_refspec_fallback (self, remote, ref, allow_noent,
                                     &ret_rev, cancellable, error))
        goto out;
    }

  ot_transfer_out_value (out_rev, &ret_rev);
  ret = TRUE;
 out:
  return ret;
}

/**
 * ostree_repo_resolve_rev:
 * @self: Repo
 * @refspec: A refspec
 * @allow_noent: Do not throw an error if refspec does not exist
 * @out_rev: (out) (transfer full): A checksum,or %NULL if @allow_noent is true and it does not exist
 * @error: Error
 *
 * Look up the given refspec, returning the checksum it references in
 * the parameter @out_rev.
 */
gboolean
ostree_repo_resolve_rev (OstreeRepo     *self,
                         const char     *refspec,
                         gboolean        allow_noent,
                         char          **out_rev,
                         GError        **error)
{
  gboolean ret = FALSE;
  gs_free char *ret_rev = NULL;

  g_return_val_if_fail (refspec != NULL, FALSE);

  if (ostree_validate_checksum_string (refspec, NULL))
    {
      ret_rev = g_strdup (refspec);
    }
  else
    {
      if (g_str_has_suffix (refspec, "^"))
        {
          gs_free char *parent_refspec = NULL;
          gs_free char *parent_rev = NULL;
          gs_unref_variant GVariant *commit = NULL;
          gs_unref_variant GVariant *parent_csum_v = NULL;

          parent_refspec = g_strdup (refspec);
          parent_refspec[strlen(parent_refspec) - 1] = '\0';

          if (!ostree_repo_resolve_rev (self, parent_refspec, allow_noent, &parent_rev, error))
            goto out;
          
          if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_COMMIT, parent_rev,
                                         &commit, error))
            goto out;
      
          g_variant_get_child (commit, 1, "@ay", &parent_csum_v);
          if (g_variant_n_children (parent_csum_v) == 0)
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Commit %s has no parent", parent_rev);
              goto out;
            }
          ret_rev = ostree_checksum_from_bytes_v (parent_csum_v);
        }
      else
        {
          gs_free char *remote = NULL;
          gs_free char *ref = NULL;

          if (!ostree_parse_refspec (refspec, &remote, &ref, error))
            goto out;
          
          if (!resolve_refspec (self, remote, ref, allow_noent,
                                &ret_rev, error))
            goto out;
        }
    }

  ret = TRUE;
  ot_transfer_out_value (out_rev, &ret_rev);
 out:
  return ret;
}

static gboolean
enumerate_refs_recurse (OstreeRepo    *repo,
                        const char    *remote,
                        GFile         *base,
                        GFile         *dir,
                        GHashTable    *refs,
                        GCancellable  *cancellable,
                        GError       **error)
{
  gboolean ret = FALSE;
  gs_unref_object GFileEnumerator *enumerator = NULL;

  enumerator = g_file_enumerate_children (dir, OSTREE_GIO_FAST_QUERYINFO,
                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                          cancellable, error);
  if (!enumerator)
    goto out;

  while (TRUE)
    {
      GFileInfo *file_info = NULL;
      GFile *child = NULL;

      if (!gs_file_enumerator_iterate (enumerator, &file_info, &child,
                                       NULL, error))
        goto out;
      if (file_info == NULL)
        break;

      if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY)
        {
          if (!enumerate_refs_recurse (repo, remote, base, child, refs, cancellable, error))
            goto out;
        }
      else if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
        {
          if (!add_ref_to_set (remote, base, child, refs,
                               cancellable, error))
            goto out;
        }
    }

  ret = TRUE;
 out:
  return ret;
}

gboolean
ostree_repo_list_refs (OstreeRepo       *repo,
                       const char       *refspec_prefix,
                       GHashTable      **out_all_refs,
                       GCancellable     *cancellable,
                       GError          **error)
{
  gboolean ret = FALSE;
  gs_unref_hashtable GHashTable *ret_all_refs = NULL;
  gs_free char *remote = NULL;
  gs_free char *ref_prefix = NULL;

  ret_all_refs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

  if (refspec_prefix)
    {
      gs_unref_object GFile *dir = NULL;
      gs_unref_object GFile *child = NULL;
      gs_unref_object GFileInfo *info = NULL;

      if (!ostree_parse_refspec (refspec_prefix, &remote, &ref_prefix, error))
        goto out;

      if (remote)
        dir = g_file_get_child (repo->remote_heads_dir, remote);
      else
        dir = g_object_ref (repo->local_heads_dir);

      child = g_file_resolve_relative_path (dir, ref_prefix);
      if (!ot_gfile_query_info_allow_noent (child, OSTREE_GIO_FAST_QUERYINFO, 0,
                                            &info, cancellable, error))
        goto out;

      if (info)
        {
          if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
            {
              if (!enumerate_refs_recurse (repo, remote, child, child,
                                           ret_all_refs,
                                           cancellable, error))
                goto out;
            }
          else
            {
              if (!add_ref_to_set (remote, dir, child, ret_all_refs,
                                   cancellable, error))
                goto out;
            }
        }
    }
  else
    {
      gs_unref_object GFileEnumerator *remote_enumerator = NULL;

      if (!enumerate_refs_recurse (repo, NULL, repo->local_heads_dir, repo->local_heads_dir,
                                   ret_all_refs,
                                   cancellable, error))
        goto out;

      remote_enumerator = g_file_enumerate_children (repo->remote_heads_dir, OSTREE_GIO_FAST_QUERYINFO,
                                                     0,
                                                     cancellable, error);

      while (TRUE)
        {
          GFileInfo *info;
          GFile *child;
          const char *name;

          if (!gs_file_enumerator_iterate (remote_enumerator, &info, &child,
                                           cancellable, error))
            goto out;
          if (!info)
            break;

          name = g_file_info_get_name (info);
          if (!enumerate_refs_recurse (repo, name, child, child,
                                       ret_all_refs,
                                       cancellable, error))
            goto out;
        }
    }

  ret = TRUE;
  ot_transfer_out_value (out_all_refs, &ret_all_refs);
 out:
  return ret;
}

static gboolean
write_ref_summary (OstreeRepo      *self,
                   GCancellable    *cancellable,
                   GError         **error)
{
  gboolean ret = FALSE;
  GHashTableIter hash_iter;
  gpointer key, value;
  gsize bytes_written;
  gs_unref_hashtable GHashTable *all_refs = NULL;
  gs_unref_object GFile *summary_path = NULL;
  gs_unref_object GOutputStream *out = NULL;
  gs_free char *buf = NULL;

  if (!ostree_repo_list_refs (self, NULL, &all_refs, cancellable, error))
    goto out;

  summary_path = g_file_resolve_relative_path (ostree_repo_get_path (self),
                                               "refs/summary");

  out = (GOutputStream*) g_file_replace (summary_path, NULL, FALSE, 0, cancellable, error);
  if (!out)
    goto out;
  
  g_hash_table_iter_init (&hash_iter, all_refs);
  while (g_hash_table_iter_next (&hash_iter, &key, &value))
    {
      const char *name = key;
      const char *sha256 = value;

      g_free (buf);
      buf = g_strdup_printf ("%s %s\n", sha256, name);
      if (!g_output_stream_write_all (out, buf, strlen (buf), &bytes_written, cancellable, error))
        goto out;
    }

  if (!g_output_stream_close (out, cancellable, error))
    goto out;

  ret = TRUE;
 out:
  return ret;
}

gboolean      
ostree_repo_write_ref (OstreeRepo  *self,
                       const char  *remote,
                       const char  *name,
                       const char  *rev,
                       GError     **error)
{
  gboolean ret = FALSE;
  gs_unref_object GFile *dir = NULL;

  if (remote == NULL)
    dir = g_object_ref (self->local_heads_dir);
  else
    {
      dir = g_file_get_child (self->remote_heads_dir, remote);
      
      if (rev != NULL)
        {
          if (!gs_file_ensure_directory (dir, FALSE, NULL, error))
            goto out;
        }
    }

  if (rev == NULL)
    {
      gs_unref_object GFile *child = g_file_resolve_relative_path (dir, name);
      
      if (g_file_query_exists (child, NULL))
        {
          if (!gs_file_unlink (child, NULL, error))
            goto out;
        }
    }
  else
    {
      if (!write_checksum_file (dir, name, rev, error))
        goto out;
      
      if (rev != NULL)
        {
          if (self->mode == OSTREE_REPO_MODE_ARCHIVE_Z2)
            {
              if (!write_ref_summary (self, NULL, error))
                goto out;
            }
        }
    }

  ret = TRUE;
 out:
  return ret;
}

gboolean
ostree_repo_write_refspec (OstreeRepo  *self,
                           const char  *refspec,
                           const char  *rev,
                           GError     **error)
{
  gboolean ret = FALSE;
  gs_free char *remote = NULL;
  gs_free char *ref = NULL;

  if (!ostree_parse_refspec (refspec, &remote, &ref, error))
    goto out;

  if (!ostree_repo_write_ref (self, remote, ref, rev, error))
    goto out;

  ret = TRUE;
 out:
  return ret;
}
