/*

  Rename - Renaming utility that handles also long file names.

*/
#include <vo_stdio.h>
#include <volink.h>     /* Linker directives like DLLENTRY */
#include <apploader.h>  /* RunLibraryFunction etc */
#include <kernel.h>
#include <lcd.h>
#include <stdbuttons.h>
#include <string.h>
#include <stdlib.h>
#include <lists.h>
#include <sysmemory.h>
#include <swap.h>
#include <imem.h>
#include <unistd.h>
#include <ctype.h>
#include <ByteManipulation.h>
#include <consolestate.h>

#define MAX(a,b) (((a)>(b))?(a):(b))
#define MIN(a,b) (((a)<(b))?(a):(b))


static u_int16 sectorBuf[256];

#if 0
#define VERBOSE
#endif

#if 0
#  define USE_DEBUG
#endif

/* How many alias file names to look for.
   Example: for file "MyFile.txt", max 999 goes through alias file names
   "MyFile~1.txt" through "MyFi~999.txt". */
#define ALIAS_FILE_NAME_MAX 999
/* How many free entries to go throught to find as many consecutive entries
   as are needed for the current file name. */
#define MAX_LOOK_FOR_FREE_ENTRIES 512



#ifdef USE_DEBUG
const char *attrS[7] = {
  /*1*/"READONLY", /*2*/"HIDDEN", /*4*/"SYSTEM", /*8*/"VOLUMEID",
  "DIRECTORY"/*0x10*/, "ARCHIVE"/*0x20*/, "LONGNAME"/*0xf->0x40*/
};
void PrintDirEnt(register const DirectoryEntry *de) {
  int i;
  printf("DirEnt(0x%0x)\n", de);
  printf("  FName %04x %04x %04x %04x %04x = \"",
	 de->filename[0], de->filename[1], de->filename[2],
	 de->filename[3], de->filename[4]);
  for (i=0; i<5; i++) {
    printf("%c%c", de->filename[i]>>8, de->filename[i]&0xff);
  }
  printf("%c", de->filename[5]>>8);
  {
    u_int16 attr = de->attr & 0xFF;
    int first = 1;
    printf("\", attr %02x", attr);
    if (de->attr & 0xf) {
      de->attr = (de->attr & ~0xf) | 0x40;
    }
    for (i=0; i<7; i++) {
      if (attr & (1<<i)) {
	printf("%sATTR_%s", first?" = ":", ", attrS[i]);
      }
    }
    printf("\n");
  }
  printf("  firstClus %04x:%04x, fileSize %04x:%04x\n",
	 Swap16(de->firstClusHi), Swap16(de->firstClusLo),
	 Swap16(de->fileSizeHi), Swap16(de->fileSizeLo));
}
#endif /* USE_DEBUG */

/* Calculate checksum for a 8.3 name that is in the alias form "NNNNNNNNEEE" */
u_int16 ShortNameChecksum(register const char *s) {
   int i;
   u_int16 sum = 0;

   for (i = 0; i<11; i++) {
     sum = (((sum & 1) << 7) + (sum >> 1) + *s++) & 0xff;
   }

   return sum;
}

/* Convert "NNNNNNNNEEE" to nul-terminated 8.3. */
int AliasToFileName(register char *d, register const char *s) {
  int dotPos = 8;
  char *p = NULL;
#ifdef USE_DEBUG
  printf("AliasToFileName: \"%s\" -> ", s);
#endif /* USE_DEBUG */
  memcpy(d, s, 8);
  d[8] = ' ';
  d = strchr(d, ' ');
  if (s[8] != ' ') {
    *d++ = '.';
    memcpy(d, s+8, 3);
    d[3] = '\0';
#ifdef USE_DEBUG
    printf("\"%s\" -> ", d);
#endif /* USE_DEBUG */
    if (p = strchr(d, ' ')) *p = '\0';
#ifdef USE_DEBUG
    printf("\"%s\"\n", d);
#endif /* USE_DEBUG */
  } else {
    *d++ = '\0';
  }
}

/* Convert  null-terminated 8.3 to "NNNNNNNNEEE".
   If x != 0, add alias number x to the end of the NNNNNNNN part.
   Null-terminates alias for easier printing, so alias must have space
   for 12 characters. */
int CreateAlias(register char *d, register const char *s, register int x) {
  char *dot = strrchr(s, '.'), *dd = d;
  int len = 0, xx;
  char numS[8];
  memset(dd, ' ', 11); /* Clear destination. */
  dd[11] = '\0'; /* Makes printing easier. */
  while (*s) {
    char su = toupper(*s);
    if ((su >= 'A' && su <= 'Z') ||
	(su >= '0' && su <= '9') || su == '_') {
      if (len != 8 && len != 12) {
	*dd++ = su;
	len++;
      }
    } else if (s == dot) {
      while (len < 8) {
	*dd++ = ' ';
	len++;
      }
      len = 9;
    }
    s++;
  }
  xx = x;
  len = 2;

  /* If there is no base in the alias, make it "FILE" */
  if (d[0] == ' ') memcpy(d, "FILE", 4);
  
  if (x) { /* Only add number to alias if parameter x is non-zero */
    dd = d+6;
    while (xx >= 10) {
      dd--;
      xx /= 10;
      len++;
    }
    while (dd > d && dd[-1] == ' ') dd--;
    sprintf(numS, "~%d", x);
    memcpy(dd, numS, len);
  }
  return 0;
}


/* Long file name byte indices in a file entry. */
const __mem_y int lfnIdx[13] = {
  1, 3, 5, 7, 9, 0xe, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1c, 0x1e
};



#define MAX_LONG_NAME_ENTRIES 15

struct LNameEntry {
  u_int32 sector;
  u_int16 number;
} lNameEntry[MAX_LONG_NAME_ENTRIES];


/*
  Creates a new entry name in the image of sEnt. If sEnt is a directory,
  the that directorie's ".." entry is changed to correspond to its new
  location.
 */
int MakeNewEntry(register const char *name, register DirectoryEntry *sEnt) {
  int retCode = -1;
  int entryNumber = -1, entriesNeeded = 0, n = 0;
  char *localName = NULL, *fileName = NULL, *localFileName = NULL, *s;
  u_int32 lastDirEntSec = -1;
  u_int16 lastDirEntNum = -1;
  int i;
  DEVICE *dDev = NULL;
  char localAlias[12];
  int entryIsDir = sEnt->attr & __ATTR_DIRECTORY;
  int needAlias = 0;

#ifdef USE_DEBUG
  printf("MakeNewEntry(\"%s\", sEnt=%x\n", name, sEnt);
#endif

  /* Create local copy of the destination name that has necessary size for
     any directory name extension. */
  if (!(localName = calloc(1, strlen(currentDirectory)+strlen(name)+13)) ||
      !currentDirectory) {
    goto finally;
  }

  /* Create a full path. */
  if ((fileName = strrchr(name, ':'))) {
    /* Full path */
    if (fileName != name+1) goto finally;
    fileName++;
    localName[0] = '\0';
  } else {
    /* Relative path, convert to full path */
    strcpy(localName, currentDirectory);
    fileName = name;
  }
  strcat(localName, name);
  localFileName = localName+2;
  
  /* Now we have a full path. Do we have subdirectories? */
  {
    const char *s;
    if ((s = strrchr(name, '/'))) fileName = s+1;
    if ((s = strrchr(localName, '/'))) localFileName = s+1;
  }
  if (!fileName[0] || !localFileName[0]) {
    printf("E: Missing destination file name\n");
    goto finally;
  }

  /* Check that we don't try to rename between devices. */
  i = currentDirectory[0];
  if (name[1] == ':') i = toupper(name[0]);
  if (i != toupper(localName[0])) {
    printf("E: Device crossing\n");
    goto finally;
  }
  
#ifdef USE_DEBUG
  printf("name %x = %s, fileName %x = %s\nlName %x = %s, lFName %x = %s\n",
	 name, name, fileName, fileName,
	 localName, localName, localFileName, localFileName);
#endif /* USE_DEBUG */

  /* 1 entry for short name + 1 for every starting 13-characters of long name. */
  entriesNeeded = (strlen(localFileName)+12)/13+1;
#ifdef USE_DEBUG
  printf("### -> %d characters, %d entries (13 ch/ent) needed for sName+lName\n",
	 strlen(localFileName), entriesNeeded);
#endif /* USE_DEBUG */

  /* Check whether we can represent the file name with uppercase 8.3 format. */
  {
    char *p = localFileName;
    int i;
    int canBe83 = 1;
    int dots = 0;
    int chars[2] = {0};
    char c;
    while ((c = *p++)) {
      char uc = toupper(c);
      /* We are very conservative when generating short file names.
         We don't allow !#$%&'()-@^`{}~ or any 8-bit characters even though
	 they are on the ok side. */
      if ((uc >= 'A' && uc <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
	chars[dots]++;
	/* Lower-case letter prevents only short file name but doesn't yet
	   require an alias. */
	if (uc != c) canBe83 = 0;
      } else if (c == '.') {
	if (++dots >= 2) {
	  canBe83 = 0;
	  needAlias = 1;
	}
      } else {
	canBe83 = 0;
	needAlias = 1;
      }
    }
    if (!chars[0] || chars[0] > 8 || (dots && chars[1] > 3)) {
      canBe83 = 0;
      needAlias = 1;
    }
#ifdef USE_DEBUG
    printf("### canBe83 = %d for \"%s\"\n", canBe83, localFileName);
#endif /* USE_DEBUG */
    if (canBe83) entriesNeeded = 1;

#if 0
    printf("canBe83 = %d, needAlias = %d, entriesNeeded = %d\n",
	   canBe83, needAlias, entriesNeeded);
    if (PERIP(0xFFFF) != 0x1234) goto finally;
#endif
  }

  
  if (entriesNeeded > MAX_LONG_NAME_ENTRIES) {
    goto finally;    
  }

  /* This far we have gathered information to make our changes.
     Now it's time to commit to the changes that actually change the
     file system. */

#if 0
  if (PERIP(0xFACE) != 0x1234) goto finally;
#endif
  
  /* If our entry is a directory, fix its ".." to point to the new parent */
  if (entryIsDir) {
    FILE *fp;
    FatFileInfo *fi = NULL;
    u_int32 parentSector = 0;
    u_int32 parentCluster = 0;
    u_int32 dotDotSector = 0;
    DirectoryEntry *dotDotDirEntry;
    FatDeviceInfo *di;

    fp = fopen(localName, "s");
    if (!fp) {
      goto finally;
    }
#ifdef USE_DEBUG
    printf("%s opened\n", localName);
#endif
    fi = (FatFileInfo *)fp->fileInfo;
    dDev = fp->dev;
    di = (FatDeviceInfo*)dDev->deviceInfo;
    
    if (FatFindFirst(fp, localName+2, NULL, 0) == S_OK) {
      parentCluster = fi->startCluster;
      if (di->fatBits == 32 && di->rootStart == parentCluster) {
#ifdef USE_DEBUG
	printf("# FIX parentCluster from %ld to 0\n", parentCluster);
#endif
	/* Special value of 0 for FAT32 root directory, even though
	   the actual value would work just fine also. */
	parentCluster = 0;
      }
#if 0
      fclose(fp);
      goto finally;
#endif
    } else {
      fclose(fp);
      goto finally;
    }

#ifdef USE_DEBUG
    printf("## parentCluster %lx\n", parentCluster);
#endif
    dotDotSector = di->dataStart +
      (((u_int32)Swap16(sEnt->firstClusHi) << 16) | Swap16(sEnt->firstClusLo)) *
      di->fatSectorsPerCluster;
#ifdef USE_DEBUG
    printf("dotDotSector %lx\n", dotDotSector);
#endif

    /* The ".." entry is always the second entry of a directory, hence "+1". */
    dotDotDirEntry = (DirectoryEntry *)sectorBuf+1;
#ifdef USE_DEBUG
    printf("dotDotDirEntry %x, sectorBuf %x, dotDotSector %lx\n",
	   dotDotDirEntry, sectorBuf, dotDotSector);
#endif
    dDev->BlockRead(dDev, dotDotSector, 1, sectorBuf);
#ifdef USE_DEBUG
    printf("###11\n");
    PrintDirEnt(dotDotDirEntry);
#endif
    dotDotDirEntry->firstClusHi = Swap16((u_int16)(parentCluster >> 16));
    dotDotDirEntry->firstClusLo = Swap16((u_int16)(parentCluster      ));
#ifdef USE_DEBUG
    printf("###12\n");
    PrintDirEnt(dotDotDirEntry);
#endif
    dDev->BlockWrite(dDev, dotDotSector, 1, sectorBuf);
    fclose(fp);
    fp = NULL;
  }

  /* Find entriesNeeded entry points that are sure to be in consecutive
     order by creating 0-size temporary files LNAMETMP.001 etc. Remember
     in which disc sectors and locations inside the sector the consecutive
     ones were. */
  n = 0;
  entryNumber = 1;
  while (n < entriesNeeded) {
    FILE *fp;
    FatFileInfo *fi = NULL;
    sprintf(localFileName, "LNAMETMP.%03d", entryNumber);
#ifdef USE_DEBUG
    printf("entryNumber %2d, entriesNeeded %2d, n %2d, tmpFile %s\n",
	   entryNumber, entriesNeeded, n, localName);
#endif /* USE_DEBUG */
    fp = fopen(localName, "wb");
    if (!fp) {
#ifdef USE_DEBUG
      printf("FAILED!\n");
#endif /* USE_DEBUG */
      entryNumber--;
      goto cleanfiles;
    }
#ifdef USE_DEBUG
    printf("ok\n");
#endif /* USE_DEBUG */
    fi = (FatFileInfo *)fp->fileInfo;
#ifdef USE_DEBUG
    printf("dirEntSec 0x%lx, dirEntNum 0x%x\n",
	   fi->directoryEntrySector, fi->directoryEntryNumber);
#endif /* USE_DEBUG */

    /* We only create long names that don't span sector boundaries to make
       sure we don't do anything stupid accidentally. */
    if (fi->directoryEntrySector == lastDirEntSec &&
	fi->directoryEntryNumber == lastDirEntNum+1) {
#ifdef USE_DEBUG
      printf("### YES %d\n", n);
#endif /* USE_DEBUG */
    } else {
      n = 0;
#ifdef USE_DEBUG
      printf("### NO, back to 0\n");
#endif /* USE_DEBUG */
    }
    lNameEntry[n].sector = fi->directoryEntrySector;
    lNameEntry[n].number = fi->directoryEntryNumber;
    n++;
    lastDirEntSec = fi->directoryEntrySector;
    lastDirEntNum = fi->directoryEntryNumber;

    dDev = fp->dev;
    fclose(fp);
    fp = NULL;
    
    if (++entryNumber > MAX_LOOK_FOR_FREE_ENTRIES) {
      goto cleanfiles;
    }
  }

  /* If only short name is needed, make sure CreateAlias() is called with
     third parameter as 0 because then it'll not create an alias with a serial
     number. */
  i = needAlias;
  {
    FILE *fp = NULL;
    while (!CreateAlias(localAlias, fileName, i) &&
	   (AliasToFileName(localFileName, localAlias),
	    fp = fopen(localName, "rb"))) {
      /* We could open the alias file name: we need to try again to find a
	 free name. */
#ifdef USE_DEBUG
      printf("Could open \"%s\" = 0x%x\n", localName, fp);
#endif /* USE_DEBUG */
      fclose(fp);
      fp = NULL;

      /* We exhausted all alias file names */
      if (++i > ALIAS_FILE_NAME_MAX) {
	goto cleanfiles;
      }
    }
  }
#ifdef USE_DEBUG
  printf("CreateAlias: alias=\"%s\" / name=\"%s\", check 0x%x\n",
	 localAlias, localName, ShortNameChecksum(localAlias));
#endif /* USE_DEBUG */

  {
    u_int16 myEnt[16]= {0};

    /* Create long file name entry shards, if any. */
    for (i=0; i<entriesNeeded-1; i++) {
      int j;
      char *p = fileName + 13*(entriesNeeded-2-i);
      u_int16 ch = *p;
#ifdef USE_DEBUG
      printf("##4 i %d, Sect %lx, Number %x, name \"%s\"\n",
	     i, lNameEntry[i].sector, lNameEntry[i].number, p);
#endif /* USE_DEBUG */
      SetBE8(myEnt, 0x0, (i ? 0 : 0x40) | entriesNeeded-1-i );
      SetBE8(myEnt, 0xb, 0xf);
      SetBE8(myEnt, 0xd, ShortNameChecksum(localAlias));
      for (j=0; j<13; j++) {
	if (!ch) {
	  ch = 0xFFFF;
	} else if (ch != 0xFFFFU) {
	  ch = *p++;
	}
	SetBE8(myEnt, lfnIdx[j]  , ch   );
	SetBE8(myEnt, lfnIdx[j]+1, ch>>8);
      }
#ifdef USE_DEBUG
      for (j=0; j<16; j++) {
	printf(" %04x", myEnt[j]);
	if ((j&7)==7)printf("\n");
      }
#endif /* USE_DEBUG */

      /* Update the long entry shard to its location on disk. */
      dDev->BlockRead(dDev, lNameEntry[i].sector, 1, sectorBuf);
      memcpy((DirectoryEntry *)sectorBuf+lNameEntry[i].number, &myEnt,
	     sizeof(myEnt));
      dDev->BlockWrite(dDev, lNameEntry[i].sector, 1, sectorBuf);
    }

    /* Create short file name entry by modifying the source file entry. */
#ifdef USE_DEBUG
    printf("###5.0\n");
    PrintDirEnt(sEnt);
#endif /* USE_DEBUG */
    /* Copy our short name (alias) to the entry. */
    for (i=0; i<11; i++) {
      SetBE8((u_int16 *)sEnt, i, localAlias[i]);
    }
#ifdef USE_DEBUG
    printf("###5.1\n");
    PrintDirEnt(sEnt);
#endif /* USE_DEBUG */
    i = entriesNeeded-1;
#ifdef USE_DEBUG
    printf("##6 i %d, Sect %lx, Number %x, name \"%s\"\n",
	   i, lNameEntry[i].sector, lNameEntry[i].number, localName);
#endif /* USE_DEBUG */
    /* Update the short entry to its location on disk. */
    dDev->BlockRead(dDev, lNameEntry[i].sector, 1, sectorBuf);
    memcpy((DirectoryEntry *)sectorBuf+lNameEntry[i].number, sEnt,
	   sizeof(*sEnt));
    dDev->BlockWrite(dDev, lNameEntry[i].sector, 1, sectorBuf);

    /* Don't try to remove temporary files that we have already repurposed. */
    entryNumber -= entriesNeeded;
  }
  retCode = 0;
 cleanfiles:
  /* Remove temporary files. */
  for (i=1; i<entryNumber; i++) {
    int t;
    sprintf(localFileName, "LNAMETMP.%03d", i);
    t = unlink(localName);
#ifdef USE_DEBUG
    printf("#8 unlink %s = %d\n", localName, t);
#endif /* USE_DEBUG */
  }
 finally:
  if (localName) {
    free(localName);
    localName = NULL;
  }
  return retCode;
}



int Rename(register const char *sName, register const char *dName) {
  FatFileInfo *sFi = NULL;
  DEVICE *sDev = NULL;
  FILE *sFp;
  DirectoryEntry sEnt;
  u_int32 sDirEntSec = 0;
  u_int16 sDirEntNum = 0;
  int retCode = S_ERROR;

#ifdef USE_DEBUG
  printf("#RENAME(from=\"%s\", to=\"%s\")\n", sName, dName);
#endif
  
  if (sFp = fopen(dName, "rb")) {
    fclose(sFp);
    sFp = NULL;
    printf("E: %s exists\n", dName);
    goto finally;
  }
  
  if (!(sFp = fopen(sName, "rb"))) {
    printf("E: Couldn't open %s\n", sName);
    goto finally;
  }
  
  sFi = (FatFileInfo *)sFp->fileInfo;
  sDev = sFp->dev;
 
  sDirEntSec = sFi->directoryEntrySector;
  sDirEntNum = sFi->directoryEntryNumber;

  sDev->BlockRead(sDev, sDirEntSec, 1, sectorBuf);
  memcpy(&sEnt, (DirectoryEntry *)sectorBuf+sDirEntNum, sizeof(sEnt));

#ifdef USE_DEBUG  
  printf("SRC: startCluster = 0x%lx, dirEntSec 0x%lx, dirEntNum 0x%x\n",
	 sFi->startCluster, sDirEntSec, sDirEntNum);
#endif /* USE_DEBUG */

  fclose(sFp);
  sFp = NULL; 

#ifdef USE_DEBUG  
  PrintDirEnt(&sEnt);
#endif /* USE_DEBUG */
  if (MakeNewEntry(dName, &sEnt)) goto finally;
  /* A new entry has been created. Remove the association of the old
     entry to all file/directory data. */
  {
    DirectoryEntry *de = (DirectoryEntry *)sectorBuf+sDirEntNum;
    sDev->BlockRead(sDev, sDirEntSec, 1, sectorBuf);
    de->fileSizeHi = de->fileSizeLo = de->firstClusHi = de->firstClusLo = 0;
    de->attr &= 0xFF00; /* Remove attributes, particularly directory attribute.
			   Note: 0xFF00 because first byte is last char of
			   file name and only 8 LSbs are attributes. */
    sDev->BlockWrite(sDev, sDirEntSec, 1, sectorBuf);
  }

  /* Then remove the empty entry for the old file / directory. */
  {
    int t = unlink(sName);
#ifdef USE_DEBUG
    printf("#9 unlink(\"%s\") = %d\n", sName, t);
#endif
  }
  
  retCode = S_OK;
 finally:

  return retCode;
}



ioresult main(char *parameters) {
  int nParam, i;
  char *p = parameters;
  ioresult retVal = S_ERROR;
  char *from = NULL, *to = NULL;

  nParam = RunProgram("ParamSpl", (u_int16)parameters);
  for (i=0; i<nParam; i++) {
    if (!strcmp(p, "-h")) {
      printf("Usage: Rename [-h] from to\n"
	     "-h\tShow this help\n");
      goto finally_ok;
    } else if (!from) {
      from = p;
    } else if (!to) {
      to = p;
    } else {
      printf("E: Unknown parameter \"%s\"\n", p);
      goto finally;
    }
    p += strlen(p)+1;
  }

  if (to) {
    if (Rename(from, to)) {
      printf("E: Couldn't rename \"%s\" to \"%s\"\n", from, to);
      goto finally;
    }
  } else {
    printf("E: Missing from/to\n");
    goto finally;
  }
 finally_ok:
  retVal = S_OK;
 finally:
  return retVal;
}
