/*
 * Copyright (c) 2002, The EROS Group, LLC and Johns Hopkins
 * University. All rights reserved.
 * 
 * This software was developed to support the EROS secure operating
 * system project (http://www.eros-os.org). The latest version of
 * the OpenCM software can be found at http://www.opencm.org.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 * 
 * 3. Neither the name of the The EROS Group, LLC nor the name of
 *    Johns Hopkins University, nor the names of its contributors
 *    may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <opencm.h>
#include <repos/Repository.h>
#include "opencmclient.h"

static OC_bool
isNumber(const char *s)
{
  for (;*s; s++) {
    if (!isdigit(*s))
      return FALSE;
  }

  return TRUE;
}

ObVec *
resolve(Repository *repos, const char *arg, unsigned flags)
{
#define URITAG "opencm://"

  ObVec *vec = obvec_create();
  const char *rest = arg;
  unsigned u, w, z;
  char *pos;
  Resolution *r;
  OC_bool progress;
  OC_bool no_matches = TRUE; /* until proven otherwise */

  r = (Resolution *)GC_MALLOC(sizeof(Resolution));
  r->rest = xstrdup(arg);

  log_trace(DBG_RESOLVER, "ARG is: %s\n", arg);

  /* No embedded uris are allowed.  If input string begins
   * with 'opencm://' treat it as beginning with a uri.
   * Parse/process the uri part and set any residual for later
   * processing. */
  if (strncmp(arg, URITAG, strlen(URITAG)) == 0) {
    OC_bool ok = FALSE;

    URI *tmp_uri = uri_create(arg);

    if (tmp_uri->path == NULL)
      THROW(ExNoObject, format("Could not resolve %s (bad URI?)", arg));

    TRY {
      /* Need to take just the path_car of tmp_uri->path
       * in case it's something like 'mumble/foobar' */
      if (path_cdr(tmp_uri->path + 1) == NULL) {
	r->m = repos_GetMutable(repos, tmp_uri->URI);
	r->s = repos_GetMutableContent(repos, r->m);
	r->rest = 0;
	r->fullPath = tmp_uri->URI;
	r->fp_frag = 0;
      } 
      else {
	char *just_uri = NULL;

	/* Store rest of path for later processing */
	r->rest = path_cdr(tmp_uri->path + 1);

	/* For now, resolve just the mutable uri */
	just_uri = xstrndup(tmp_uri->URI, strlen(tmp_uri->URI) -
			    strlen(r->rest) - 1);
	r->m = repos_GetMutable(repos, just_uri);
	r->s = repos_GetMutableContent(repos, r->m);
	r->fullPath = just_uri;
	r->fp_frag = 0;

        /* One last minor check: */
	if (strcmp(r->rest, "") == 0)
	  r->rest = 0;

      }

      ok = TRUE;
    } 
    DEFAULT(AnyException) {
      /* What's specified in 'path' part of uri was NOT a mutable
       * and thus (currently) is not supported */
      ok = FALSE;
    }
    END_CATCH;
    if (ok == FALSE)
      THROW(ExNoObject, format("Could not resolve %s", arg));

  }
  else if (strlen(arg) >= 1 && (*arg == '~' || *arg == '@')) {
    /* Leading ~ means home dir */
    arg++;
    while (*arg == '/')
      arg++;
    if (arg && *arg == '\0')
      arg = 0;

    r->rest = arg;
  }

  if (r->m == 0) {
    OC_bool ok = FALSE;
    TRY {
      r->m = repos_GetMutable(repos, repos->authUser->dirURI);
      r->s = repos_GetMutableContent(repos, r->m);
      ok = TRUE;
    }
    DEFAULT(AnyException) {
    }
    END_CATCH;
    if (ok == FALSE)
      THROW(ExNoObject, format("Could not resolve %s", arg));

    r->fullPath = "@";
    r->fp_frag = 0;
  }

  r->tail = r->s;

  obvec_append(vec, r);

  for(progress = TRUE; progress; ) {
    ObVec *outvec = obvec_create();
    progress = FALSE;

    for (u = 0; u < vec_size(vec); u++) {
      const char *fragment;

      r = vec_fetch(vec, u, Resolution *);
      fragment = r->rest;

      if (fragment == 0 || r->tail == 0) {
	/* This one is done. */
	obvec_append(outvec, r);
	no_matches = FALSE;
	continue;
      }

      pos = strchr(r->rest, '/');

      if (pos) {
	fragment = xstrndup(r->rest, pos - r->rest);
	while (*pos == '/')
	  pos++;
	if (*pos == '\0')
	  pos = 0;
      }

      log_trace(DBG_RESOLVER, "Fragment: %s %s\n", r->fullPath, fragment);

      /* Expansion strategy depends on current entity type */ 

      switch(GETTYPE(r->tail)) {
      case TY_Change:
	{
	  /* Branches generally expand by matching the sequence
	     number, but the project member is matched as a special
	     case. */
	  Revision *rev = NULL;
	  ObVec *revs = NULL;

	  no_matches = TRUE;

	  /* Get all the Revision records with one call */
	  revs = NULL;

	  TRY {
	    uint64_t first = 0;
	    uint64_t last = 0xFFFFFFFFFFFFFFFFll;
	    
	    if (isNumber(fragment))
	      first = last = atollu(fragment);
	    else if (nmequal(fragment, "top")) {
	      first = last = r->m->nRevisions - 1;
	      /* hack alert */
	      fragment = xunsigned64_str(first);
	    }

	    revs = repos_GetRevisions(repos, r->m->uri, first, last);
	  }
	  DEFAULT(AnyException) {
	  }
	  END_CATCH;
	  if (revs == NULL)
	    continue;

	  for (w = 0; w < vec_size(revs); w++) {
	    const char *revno;
	    Resolution *nr;

	    rev = vec_fetch(revs, w, Revision *);

	    if (rev->newObject == 0)
	      continue;

	    revno = xunsigned64_str(rev->seq_number);

	    if (!glob_match(revno, fragment, 0))
	      continue;

	    nr = (Resolution *)GC_MALLOC(sizeof(Resolution));

	    nr->fullPath = path_join(r->fullPath, revno);
	    nr->fp_frag = revno;
	    nr->rest = pos;
	    nr->m = r->m;
	    nr->s = r->s;
		
	    nr->tail = (Serializable *)version_create(rev->newObject, 
						      rev->seq_number);
	    obvec_append(outvec, nr);

	    progress = TRUE;

	    no_matches = FALSE;
	  }

	  break;
	}
      case TY_Version:
	{
	  /* Version is a slightly strange case, because at this point
	     we switch into the fsName realm and the pattern matching
	     strategy alters accordingly. The catch is that we only
	     want to do that if we are really doing name expansion
	     within the fsName space. That is: 

	     Typing
                 somedir/mybranch/5   -> Version

             But somedir/mybranch/5/xx proceeds through

                 somedir/mybranch/5   -> Version
                 somedir/mybranch/5    -> FsDir (because there was more)
                 somedir/mybranch/5/xx -> FsDir|Entity
	  */

	  Version *v = (Version *) r->tail;

	  TRY {
	    Change *chg;
	    assert(v->cfgName);
	    chg = (Change *) repos_GetEntity(repos, r->m->uri, v->cfgName);

	    assert(GETTYPE(chg) == TY_Change);
	    assert(fragment);

	    /* Reuse the existing resolver entry, but with a type
	       change: */
	    /* (Instead of using './', just use '.', since the path_join() call
	     * will append the trailing '/'.) */
	    r->tail = (Serializable*) fsdir_create(v->rev, chg, ".");
	    obvec_append(outvec, r);

	    progress = TRUE;
	  }
	  DEFAULT(AnyException) {
	  }
	  END_CATCH;

	  break;
	}

      case TY_FsDir:
	{
	  FsDir *fsdir = (FsDir*) r->tail;
 	  Change *chg = fsdir->cfg;
	  StrVec *dirlist = strvec_create();

	  const char *glob_pattern; 

	  assert (chg != NULL);

	  assert(GETTYPE(chg) == TY_Change);

	  log_trace(DBG_RESOLVER, "path: %s, frag: %s\n", fsdir->path, fragment);
	  glob_pattern = path_join(fsdir->path, fragment);
	  log_trace(DBG_RESOLVER, "glob: %s\n", glob_pattern);

	  no_matches = TRUE;

#if 0
	  /* Get all the needed Entity objects with one call */
	  TRY {
	    ents = repos_GetEntityVec(repos, r->m->uri, chg->entNames);
	  }
	  DEFAULT(AnyException) {
	  }
	  END_CATCH;
#endif

	  for (z = 0; z < vec_size(CMGET(chg,entities)); z++) {
	    const char *match;
	    Entity *e = vec_fetch(CMGET(chg,entities), z, Entity *);

	    assert (e != NULL);

	    assert(GETTYPE(e) == TY_Entity);

#if 0
            report(3, "compare: %s, %s\n", e->fsName, glob_pattern);
#endif
	    rest = glob_match(e->fsName, glob_pattern, GM_FS_COMPONENT);

	    if (rest == 0)
	      continue;

	    no_matches = FALSE;

	    if (*rest) {
	      do {
		--rest;
	      } while (*rest == '/');
	      rest++;

	      match = xstrndup(e->fsName, rest - e->fsName);
	    }
	    else
	      match = xstrdup(e->fsName);

	    /* This is mildly tricky. Entities really are leaves, but
	       directories can end up appearing multiple times and in
	       some cases may collide with directory entities. Deal
	       with entities immediately, handle directories in a
	       second pass. */
	    if (*rest == 0) {
	      Resolution *nr = 
		(Resolution *)GC_MALLOC(sizeof(Resolution));
	      const char *newPath = path_join(r->fullPath, path_tail(match));

	      log_trace(DBG_RESOLVER, "New ent path is %s\n", newPath);
	      nr->fullPath = newPath;
	      nr->fp_frag = path_tail(match);
	      nr->m = r->m;
	      nr->s = r->s;
	      nr->tail = (Serializable *)e;
	      obvec_append(outvec, nr);

	      progress = TRUE;
	    }
	    else {
	      log_trace(DBG_RESOLVER, "Add match %s\n", match);
	      strvec_append(dirlist, match);
	    }
	  }

	  if (vec_size(dirlist) > 0) {
	    /* Do a separate pass to filter the directories down to
	       unique elements. */
            strvec_sort(dirlist);

	    {
	      const char *last = 0;
	      int i;
	  
	      for(i = 0; i < vec_size(dirlist); i++) {
		if (last && strcmp(vec_fetch(dirlist, i, const char *), last) == 0) 
		  continue;

		last = vec_fetch(dirlist, i, const char *);

		{	      
		  Resolution *nr = 
		    (Resolution *)GC_MALLOC(sizeof(Resolution));
		  const char *newPath;

		  newPath = last;
		  log_trace(DBG_RESOLVER, "New dir path is %s\n", last);
		  log_trace(DBG_RESOLVER, "Rest is %s\n", pos);

		  nr->fullPath = path_join(r->fullPath, path_tail(last));
		  nr->fp_frag = path_tail(last);
		  nr->m = r->m;
		  nr->s = r->s;
		  nr->rest = pos;
		  nr->tail = 
		    (Serializable *)fsdir_create(fsdir->rev, chg, newPath);

		  obvec_append(outvec, nr);

		  progress = TRUE;
		}
	      }
	    }
	  }

	  if (progress)
	    log_trace(DBG_RESOLVER, "Should continue\n");
	  break;
	}

      case TY_User:
	{
	  User *u = (User *)r->tail;
	  Mutable *dirM = NULL;
	  Serializable *dir = NULL;
	  Resolution *nr;

	  if (!glob_match("home", fragment, 0)) {
	      no_matches = TRUE;
	      break;
	  }

	  TRY {
	    dirM = repos_GetMutable(repos, u->dirURI);
	    dir = repos_GetMutableContent(repos, dirM);
	  }
	  DEFAULT(AnyException) {
	  }
	  END_CATCH;

	  if (dirM == NULL || dir == NULL) {
	    no_matches = TRUE;
	    break;
	  }

	  nr = (Resolution *)GC_MALLOC(sizeof(Resolution));

	  nr->fullPath = path_join(r->fullPath, "home");
	  nr->fp_frag = xstrdup("home");
	  nr->rest = pos;
	  nr->m = dirM;
	  nr->s = dir; 
	  nr->tail = dir;
	  obvec_append(outvec, nr);
	  progress = TRUE;

	  break;
	}

      case TY_Directory:
	{
	  Directory *dir = (Directory *) r->tail;
	  unsigned ndx;

	  no_matches = TRUE;

	  for(ndx = 0; ndx < vec_size(dir->entries); ndx++) {
	    Dirent *de = vec_fetch(dir->entries, ndx, Dirent *);


	    if (glob_match(de->key, fragment, 0)) {
	      Resolution *nr = (Resolution *)GC_MALLOC(sizeof(Resolution));

	      /* FIX: This is SO broken!! */
	      URI *uri = uri_create(de->value);

	      nr->fullPath = path_join(r->fullPath, de->key);
	      nr->fp_frag = de->key;
	      nr->rest = pos;

	      TRY {
	        nr->m = repos_GetMutable(repos, uri->URI);
	        nr->s = repos_GetMutableContent(repos, nr->m);
	      }
	      DEFAULT(AnyException) {
	      }
	      END_CATCH;
	      if (nr->m == 0 || nr->s == 0)
		continue;

	      nr->tail = nr->s;

	      obvec_append(outvec, nr);

	      progress = TRUE;

	      no_matches = FALSE;
	    }
	  }

	  break;
	}

      case TY_Group:
	{
	  Group *g = (Group *) r->tail;
	  unsigned ndx;

	  no_matches = TRUE;

	  for(ndx = 0; ndx < vec_size(g->members); ndx++) {
	    Mutable *m = 0;
	    Resolution *nr;

	    const char *uri = vec_fetch(g->members, ndx, const char *);

#if 0
	    report(0, "tn: %s\n", tn);
	    report(0, "fragment: %s\n", fragment);
#endif

	    TRY {
	      m = repos_GetMutable(repos, uri);
	    }
	    DEFAULT(ex) {
	    }
	    END_CATCH;
	    if (m == 0)
	      continue;

	    if (!glob_match(m->name, fragment, 0))
	      continue;

	    nr = (Resolution *)GC_MALLOC(sizeof(Resolution));

	    nr->fullPath = path_join(r->fullPath, m->name);
	    nr->fp_frag = m->name;
	    nr->rest = pos;
	    nr->m = m;

	    TRY {
	      nr->s = repos_GetMutableContent(repos, nr->m);
	      nr->tail = nr->s;
	    }
	    DEFAULT(AnyException) {
	    }
	    END_CATCH;
	    if (nr->m == NULL || nr->s == NULL)
	      continue;

	    obvec_append(outvec, nr);

	    progress = TRUE;

	    no_matches = FALSE;

#if 0
	    /* I am leaving this code present here because we may want
	       to resurrect the glob match against the mutable name
	       for various reasons.. */
	    if (glob_match(uri, fragment, 0)) {
	      Resolution *nr = (Resolution *)GC_MALLOC(sizeof(Resolution));

	      nr->fullPath = path_join(r->fullPath, fragment);
	      nr->fp_frag = uri;
	      nr->rest = pos;

	      TRY {
	        nr->m = repos_GetMutable(repos, tn);
	        nr->s = repos_GetMutableContent(repos, nr->m);
		nr->tail = nr->s;
	      }
	      DEFAULT(AnyException) {
	      }
	      END_CATCH;
	      if (nr->m == NULL || nr->s == NULL)
	        continue;

	      obvec_append(outvec, nr);

	      progress = TRUE;

	      no_matches = FALSE;
	    }
#endif
	  }

	  break;
	}
      default:
	break;
      }

      /* Don't keep partial expansions for now: */
    }
    vec = outvec;
  }

  /* We distinguish no matches at all from an empty container by returning
   * NULL versus an empty set.  In other words, if you 'ls' and empty 
   * directory we return an empty set.  If you 'ls' something that can't
   * be resolved, we return NULL. */

  /* Update on previous comment, JL 7/3/2002
   * Throw ExNoObject if the object can't be found, never return NULL
   */
  if(no_matches)
    THROW(ExNoObject, format("Could not resolve %s", arg));

  xassert(vec != NULL);

  if(((flags & RESOLVER_OPT_SINGLE_RESULT) || (flags & RESOLVER_OPT_MANY_RESULTS)) &&
     (vec_size(vec) == 0))
    THROW(ExNoObject, format("Could not resolve %s", arg));

  if((flags & RESOLVER_OPT_NO_RESULT) && (vec_size(vec) > 0))
    THROW(ExNoObject,
          format("Successfully resolved %s; did not want any resolutions", arg));

  if((flags & RESOLVER_OPT_SINGLE_RESULT) && (vec_size(vec) > 1))
    THROW(ExNoObject, format("Could not uniquely resolve %s", arg));

  return vec;
}
