/*
 * 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"
#include "../libgdiff/diffrun.h"

/**
 * The WsEntity object manages the state of an entity version in the
 * workspace. Because the entity in the workspace can be unchanged,
 * deleted, newly added, renamed, updated, or modified, it is less
 * than obvious how to go about this.
 *
 * At bottom, the question amounts to: should the WsEntity reflect
 *
 *   1. The state of the entity that you originally checked out,
 *   2. The state of some hypothetical future entity that will be
 *      created by a subsequent commit, or
 *   3. Something in between?
 *
 * We would very much like all of the objects in the workspace to
 * correspond to some kind of Entity object, because it greatly
 * simplifies the merge logic.
 *
 * If we go with option (1), the problem becomes: how do we represent
 * merged objects?  The problem is that we always want the entity
 * ancestor state to correctly reflect the ancestry trail, so after a
 * merge we would need to replace the WsEntity with some new,
 * synthetic entity that is not yet checked in, and we no longer have
 * something that looks like the entity you originally checked out.
 *
 * If we go with option (2), ALL of the WsEntity structures reflect
 * the state of some hypothetical future entity. When the time comes
 * to commit, we will either check in this new hypothetical entity or
 * discard it depending on whether it has changed. The main
 * complication is this is figuring out how to *undo* changes when the
 * user reverts (e.g. rename to something else and then back). We need
 * a way to reliably detect that the state of the entity in the
 * workspace once again matches the state of the original.
 *
 * Option (3) seems to inherit all of the problems of option (1) with
 * no advantages that I can see.
 *
 * I have gone with option (1). In order to detect changes that are
 * undone, the WsEntity records separately the original fsName and the
 * truename of the original EntityBits for each object. Separately
 * from this, it records a flags field that tracks the following kinds
 * of changes:
 *
 *      NEF_RENAME     object has been renamed (*)
 *      NEF_MODIFIED   object has been modified (*)
 *      NEF_MERGED     a merge into the object has occurred (*)
 *      NEF_ADD        this is a new object
 *      NEF_DELETED    object has been deleted
 *
 * The items marked (*) can be detected by other means (comparing old
 * and new names, comparing the original and the current entitybits
 * truename, checking if the mergeParent is non-null). Encoding
 * everything in one place makes it fairly easy to decide which
 * entities require careful treatment during the merge and commit
 * operations.
 *
 * Note that if an update operation is performed into an object whose
 * content is unmodified, we do NOT set the NEF_MERGED flag. Instead,
 * we simply pretend that the original checkout was done against the
 * newer baseline entity.
 */

/**
 * The WsEntity object manages the change from one version of an
 * entity to the next.
 */

typedef struct TypeSuffix {
  unsigned char eType;
  const char *glob_pattern;
} TypeSuffix;

/* NOTE: the LAST match wins! */
/* FIX: The fact that I stuck an EROS expedient in here suggests that
 * a general command line option for conversion purposes would be
 * helpful, and also perhaps that the .opencm-ignore file is
 * misnamed, and should be able to contain information like
 * this. Perhaps '.opencmrc'? :-) 
 *
 * No. This should be integrated into the ".opencm-rules" and
 * filterset processing by having the enumerator return a vector of
 * (name, type) pairs rather than a vector of names. */
static TypeSuffix global_suffix_list[] = {
  { 'T', "*" },			/* until otherwise proven */
  { 'B', "core" },		/* core files */
  { 'B', "core.*" },		/* core files */
  { 'B', "*.a" },		/* archive files */
  { 'B', "*.o" },		/* object files */
  { 'B', "*.exe" },		/* Windows executables */
  { 'B', "a.out" },		/* UNIX default executables */
  { 'B', "*.dvi" },		/* TeX DVI files */
  { 'B', "*.png" },		/* PNG images */
  { 'B', "*.gif" },		/* GIF images */
  { 'B', "*.jpg" },		/* JPEG images */
  { 'B', "*.gz" },		/* gzip */
  { 'B', "*.tar" },		/* tar files */
  { 'B', "*.tgz" },		/* gzipped tar files */
  { 'B', "*.Z" },		/* compress files */
  { 'B', "*.zip" },		/* zip files */

  { 'B', "tgz-*" },		/* Temporary EROS conversion expedient */

  { '?', 0 }			/* END MARKER */
};

static unsigned char
guess_etype(const char *fileName)
{
  TypeSuffix *list = global_suffix_list;
  unsigned char eType = 'T';

  const char *basenm = path_tail(fileName);

  while (list->glob_pattern) {
    if (glob_match(basenm, list->glob_pattern, GM_EXACT))
      eType = list->eType;
    list++;
  }

  return eType;
}

/* When adding new files: */
WsEntity *
wsentity_addFromFile(PendingChange *pc,
		     const char *fsName,
		     unsigned char eType)
{
  portstat_t ps;

  WsEntity *wse = (WsEntity *) GC_MALLOC(sizeof(WsEntity));
  Buffer *content;

  if (eType == '?')
    eType = guess_etype(fsName);

  content = buffer_FromFile(fsName, eType);

  ser_init(wse, &WsEntity_SerType, WsEntity_SerType.ver);
  SER_MODIFIED(wse);

  wse->old = 0;

  wse->flags = NEF_ADDED;
  wse->cur_fsName = fsName;
  wse->cur_entityType = eType;
  wse->cur_entityPerms = 0;
  wse->cur_mergeParent = 0;

  if (path_isexecutable(wse->cur_fsName))
    wse->cur_entityPerms |= EPRM_EXEC;

  wse->lk_contentTrueName = ser_getTrueName(content);
  wse->lk_length = buffer_length(content);
  
  path_portstat(fsName, &ps);

  wse->lk_modTime = ps.modTime;

  /* Assign a new FamNID for internal use: */
  wse->familyNID = entity_CreateFamNID();
  wse->famNidHash = truename_hash(wse->familyNID);

  return wse;
}

WsEntity *
wsentity_fromEntity(const Entity *e)
{
  WsEntity *wse = (WsEntity *) GC_MALLOC(sizeof(WsEntity));

  ser_init(wse, &WsEntity_SerType, WsEntity_SerType.ver);
  SER_MODIFIED(wse);

  wse->old = e;

  wse->cur_fsName = e->fsName;
  wse->cur_entityType = e->entityType;
  wse->cur_entityPerms = e->entityPerms;
  wse->cur_mergeParent = NULL;
  wse->cur_mergeParentChange = NULL;

  /* Following items are only valid if computed by
     wsentity_RecomputeStatus() */
  wse->lk_contentTrueName = e->contentTrueName;
  wse->lk_length = 0;

  /* We won't have a valid modTime until we actually check this out
     into the workspace. Set the modTime to "beginning of time", which
     will not match any valid modTime in RecomputeStatus and will
     therefore trigger a check. */
  wse->lk_modTime = 0;

  wse->flags = 0;

  wse->familyNID = e->familyNID;
  wse->famNidHash = e->famNidHash;

  return wse;
}

static const char *
wsentity_status(WsEntity *wse)
{
  if (wse->flags == 0)
    return "[same     ]";
  else if (wse->flags & NEF_MISSING)
    return "[missing  ]";
  else
    return format("[%c%c%c%c%c%c%c%c%c]", 
		  (wse->flags & NEF_RENAMEALERT) ? '!' : ' ',
		  ((wse->flags & NEF_ADDED) ||
                   (wse->flags & NEF_JADD)) ? 'A' : ' ',
		  (wse->flags & NEF_MODIFIED) ? 'M' : ' ',
		  (wse->flags & NEF_MERGED) ? 'J' : ' ',
		  (wse->flags & NEF_RENAMED) ? 'N' : ' ',
		  (wse->flags & NEF_DELETED) ? 'D' : ' ',
		  (wse->flags & NEF_CONDDEL) ? '-' : ' ',
		  (wse->flags & NEF_PERMS) ? 'P' : ' ',
		  (wse->flags & NEF_MODTIME) ? 'T' : ' ');
}

void 
wsentity_ReportStatus(WsEntity *wse_u)
{
  report(0, "%s %10d %s\n",
	  wsentity_status(wse_u),
	  wse_u->lk_length,
	  wse_u->cur_fsName);

  /* Display old name */
  if ((wse_u->flags & NEF_RENAMED) && opt_Verbosity >= 1)
    report(0, "        Name was: \"%s\"\n", wse_u->old->fsName);

  /* Display status of executable bit */
  if ((wse_u->flags & NEF_PERMS) && opt_Verbosity >= 1) {
    if (wse_u->cur_entityPerms & EPRM_EXEC)
      report(0, "        File is executable.\n");
    else
      report(0, "        File is no longer executable.\n");
  }

  /* Depending on verbose level, display message concerning CONDDEL
     and MERGED flags */
  if (opt_Verbosity > 0) {
    if (wse_u->flags & NEF_CONDDEL)
      report(0, "        *Conditionally removed: Use 'add' or "
	      "'rm' before committing.\n");
    if (wse_u->flags & NEF_MERGED)
      report(0, "        Merge from: \"%s\"\n", wse_u->cur_mergeParent);
  }

  /* Display message about obsolete flags: */
  if (wse_u->flags & NEF_CONDADD)
    report(0, "          *CONDADD flag is set but it's now obsolete.\n");

  if (wse_u->flags & NEF_UPDATED)
    report(0, "          *UPDATED flag is set but it's now obsolete.\n");

}

OC_bool
wsentity_check(const void *v)
{
  return TRUE;
}

void
wsentity_show(const void *ob)
{
  const WsEntity *wse = ob;
  
  report(0, "====> WsEntity (begin)\n");

  sdr_show(wse->old);
  report(0, "flags: %d\n", wse->flags);
  report(0, "cur_fsName: %s\n", wse->cur_fsName);
  report(0, "cur_entityType: %c\n", wse->cur_entityType);
  report(0, "cur_entityPerms: 0x%x\n", wse->cur_entityPerms);
  report(0, "cur_mergeParent: %s\n", wse->cur_mergeParent);
  report(0, "cur_mergeParentChange: %s\n", wse->cur_mergeParentChange);

  report(0, "lk_length: %d\n", wse->lk_length);
  report(0, "lk_modTime: %d\n", wse->lk_modTime);
  report(0, "lk_contentTrueName: %s\n", wse->lk_contentTrueName);

  report(0, "====> WsEntity (end)\n");
}

void
wsentity_serialize(SDR_stream *strm, const void *ob)
{
  const WsEntity *wse = ob;

  /* First do the entity part: */
  sdr_write("ent", strm, wse->old);

  /* Next do the wsentity part: */
  sdr_w_u32("flags", strm, wse->flags);
  sdr_w_string("cur_fsName", strm, wse->cur_fsName);
  sdr_w_u8("cur_entityType", strm, wse->cur_entityType);
  sdr_w_u32("cur_entityPerms", strm, wse->cur_entityPerms);
  sdr_w_obname("cur_mergeParent", strm, wse->cur_mergeParent);
  sdr_w_obname("cur_mergeParentChg", strm, wse->cur_mergeParentChange);
  sdr_w_u32("lk_length", strm, wse->lk_length);
  sdr_w_u32("lk_modTime", strm, wse->lk_modTime);
  sdr_w_obname("lk_contentTrueName", strm, wse->lk_contentTrueName);
}

void *
wsentity_deserialize(const DeserializeInfo *di, SDR_stream *strm)
{
  WsEntity *wse          = (WsEntity *) GC_MALLOC(sizeof(WsEntity));

  ser_init(wse, &WsEntity_SerType, di->ver);
  SER_MODIFIED(wse);

  wse->old               = sdr_read("ent", strm);

  wse->flags             = sdr_r_u32("flags", strm);
  wse->cur_fsName        = sdr_r_string("cur_fsName", strm);
  wse->cur_entityType    = sdr_r_u8("cur_entityType", strm);
  wse->cur_entityPerms   = sdr_r_u32("cur_entityPerms", strm);
  wse->cur_mergeParent   = sdr_r_obname("cur_mergeParent", strm);
  wse->cur_mergeParentChange   = sdr_r_obname("cur_mergeParentChg", strm);
  wse->lk_length         = sdr_r_u32("lk_length", strm);
  wse->lk_modTime        = sdr_r_u32("lk_modTime", strm);
  wse->lk_contentTrueName   = sdr_r_obname("lk_contentTrueName", strm);

  wse->familyNID = wse->old ? wse->old->familyNID : entity_CreateFamNID();
  wse->famNidHash = truename_hash(wse->familyNID);

  /* We need to upgrade old workspaces to use the new NEF_JADD flag */
  if ( (wse->flags & NEF_MERGED) && (wse->flags & NEF_ADDED)) {
    wse->flags &= ~ NEF_ADDED;
    wse->flags |= NEF_JADD;
  }

  return wse;
}

void
wsentity_mark(Repository *r, const void *container, 
	      const void *ob, rbtree *memObs)
{
  assert(container == ob);
  THROW(ExBadValue, "bad call to wsentity_mark()");
}

/* Check the status of the entity. */ 
void
wsentity_RecomputeStatus(WsEntity *wse)
{
  Buffer *content;
  OC_bool isExecutable;
  
  if (!path_exists(wse->cur_fsName)) {
    wse->flags |= NEF_MISSING;
    return;
  }
  else
    wse->flags &= ~NEF_MISSING;
  
  isExecutable = path_isexecutable(wse->cur_fsName);

  SER_MODIFIED(wse);

  wse->cur_entityPerms &= ~EPRM_EXEC;
  if (isExecutable)
    wse->cur_entityPerms |= EPRM_EXEC;
    

  /* Conditionally recompute truename and length of non-deleted files */
  {
    struct portstat_t ps;
    const char *tn;
    path_portstat(wse->cur_fsName, &ps);

    if (opt_ForceHash ||
	ps.len != wse->lk_length ||
	ps.modTime != wse->lk_modTime) {

      content = buffer_FromFile(wse->cur_fsName, wse->cur_entityType);
      tn = ser_getTrueName(content);

      /* Cache the results.  (This cache is NOT serialized!) */
      wse->lk_contentTrueName = tn;
      wse->lk_length = buffer_length(content);
      wse->lk_modTime = ps.modTime;
    }
  }

  /* If NEF_JADD, definitely can't be NEF_ADDED*/
  if (wse->flags & NEF_JADD) {
    assert( (wse->flags & NEF_ADDED) == 0 );
  }

  /* Paranoia is good: If NEF_ADDED, definitely can't be anything
     else! */
  if (wse->flags & NEF_ADDED) {
    assert( (wse->flags & NEF_RENAMED) == 0 );
    assert( (wse->flags & NEF_MODIFIED) == 0 );
    assert( (wse->flags & NEF_MERGED) == 0 );
    assert( (wse->flags & NEF_DELETED) == 0 );
    assert( (wse->flags & NEF_RENAMEALERT) == 0 );
    assert( (wse->flags & NEF_CONDDEL) == 0);
    assert( (wse->flags & NEF_PERMS) == 0);
    assert( (wse->flags & NEF_MISSING) == 0);
    assert( (wse->flags & NEF_JADD) == 0);
    return;
  }
    
  /* NOTE: We continue to track modification on deleted things. */
  if (wse->flags & NEF_DELETED) {
    assert( (wse->flags & NEF_ADDED) == 0 );
  }

#if 0
  if ((wse->flags & NEF_RENAMED) == 0)
    assert(nmequal(wse->cur_fsName, wse->old->fsName));
#endif

  wse->flags &= ~NEF_PERMS;

  if (wse->old && wse->cur_entityPerms != wse->old->entityPerms)
    wse->flags |= NEF_PERMS;

  /* Check if content has changed: */
  if (wse->old) {
    OC_bool sameContent = 
      nmequal(wse->lk_contentTrueName, wse->old->contentTrueName);

    if ((wse->flags & NEF_MODIFIED) && sameContent)
      wse->flags &= ~NEF_MODIFIED;

    else if (!sameContent)
      wse->flags |= NEF_MODIFIED;
  }
}

int
wsentity_UploadTo(WsEntity *wse,
		  Repository *r,
		  Mutable *m,
		  CommitInfo *ci,
		  const char *change_name)
{
  struct portstat_t ps;
  Entity *newstate;
  Buffer *content;

  path_portstat(wse->cur_fsName, &ps); /* for the mod time */

  content = buffer_FromFile(wse->cur_fsName, wse->cur_entityType);

  /* Newly added files have no parent (wse->old is NULL).  
   * Thus, create the embedded Entity object with a new FamilyNID
   * and revise content with no previous truename.  If there is
   * a parent for this WsEntity, use its familyNID and its truename
   * as new parent. */
  if (wse->old) {
    assert(nmequal(wse->old->familyNID, wse->familyNID));
    newstate = entity_create(wse->cur_entityType, wse->familyNID);

    SER_MODIFIED(newstate);

    /* Upload the content only if it changed: */
    if (!nmequal(wse->old->contentTrueName, ser_getTrueName(content)))
      repos_ReviseEntity(r, m->uri, wse->old->contentTrueName, content);

    newstate->parent = ser_getTrueName(wse->old);
    newstate->change_parent = change_name;
  } else {
    /* Brand new content, so no parent exists: */
    newstate = entity_create(wse->cur_entityType, NULL);
    /* Upload the content unconditionally: */
    repos_ReviseEntity(r, m->uri, NULL, content);
    newstate->parent = 0;
    newstate->change_parent = 0;
  }

  /* Update the Entity object within the WsEntity to reflect
   * the new state */
  newstate->entityPerms = wse->cur_entityPerms;
  newstate->contentTrueName = ser_getTrueName(content);
  newstate->length = buffer_length(content);
  newstate->commitInfoTrueName = ser_getTrueName(ci);
  newstate->mergeParent = wse->cur_mergeParent;
  newstate->change_mergeParent = wse->cur_mergeParentChange;
  newstate->fsName = wse->cur_fsName;
  newstate->modTime = os_ToISO(ps.modTime);

  /* Upload the Entity object that references the content */
  /* repos_ReviseEntity(r, m->uri, newstate->parent, newstate); */
  wse->old = entity_dup(newstate);
  wse->familyNID = wse->old->familyNID;
  wse->famNidHash = wse->old->famNidHash;

   /* We now need to update this WsEntity record to appear as though it
   * was constructed by downloading the Entity we just uploaded: */
  wse->flags = 0;		/* no longer changed in any way */
  wse->cur_mergeParent = NULL;
  wse->cur_mergeParentChange = NULL;

  assert(nmequal(wse->lk_contentTrueName, wse->old->contentTrueName));
  assert(wse->lk_length == wse->old->length);

  return 0;
}

int 
wse_fam_cmp(const WsEntity *wse1, const WsEntity *wse2)
{
  if (wse1->famNidHash < wse2->famNidHash)
    return -1;
  else if (wse1->famNidHash > wse2->famNidHash)
    return 1;
  else
    return strcmp ( wse1->familyNID, wse2->familyNID );
}

void
wsentity_dodiff(const WsEntity *wse, Repository *r, const char *branchURI)
{
  char *argv[7];
  Buffer *content;
  const char *intmpfile;
  void *obj;
	
  if ((wse->flags & NEF_MODIFIED) == 0)
    return;

  /* FIX: What to do if type has changed? */
  obj = repos_GetEntity(r, branchURI, wse->old->contentTrueName);

  content = (Buffer *)obj;

  assert(GETTYPE(content) == TY_Buffer);

  /* unwind-protect the temporary file name */
  TRY {
    intmpfile = path_mktmpnm(path_scratchdir(),"opencm-");

    buffer_ToFile(content, intmpfile, wse->old->entityType);

    report(0, "Index: %s\n", wse->cur_fsName);
    report(0, "=================================="
           "=================================\n");
    report(0, "Original: %s\n", wse->old->fsName);

    report(0, "diff -u <original> %s\n", wse->cur_fsName);

    argv[0] = "diff";
    argv[1] = "-u";
    argv[2] = "-L";
    argv[3] = (char *) wse->old->fsName;
    argv[4] = (char *) intmpfile;
    argv[5] = (char *) wse->cur_fsName;
    argv[6] = 0;
	
    diff_run(6, argv, NULL, 0);

    path_remove(intmpfile);
  } END_CATCH;
}

WsEntity *
wsentity_ShallowCopy(const WsEntity *wse)
{
  WsEntity *new_wse = GC_MALLOC(sizeof(*wse));

  memcpy(new_wse, wse, sizeof(*wse));

  return new_wse;
}
