/*
Universal logic for Commodore files on foreign host file systems

1. Convert C64 name to the host file system. Mapping different C64 characters
   to the same host character is OK. Delete characters which cannot be represented
   by the host's file system.

2. If the resulting file name is empty, then set the file name to underscore.

3. Open the file in existing mode.

4. If the file could not be opened, then:

4.1. Add the default extension, e.g. ".c64" for a C64 emulator.

4.2. Try to open the file again.

4.3. If the file could not be opened, then goto ...

5. If isatty() reports that the file is a device, then close the file, add an
   underscore, and goto 3.

6. Read the 26 byte P00 header. If it starts with "C64File",0, then:

6.1. Compare the C64 names. If they differ, then:

6.2.1. If the C64 file is opened for reading, then search all files in the current
       directory.


try the 8 byte P00 name reduction which works with every host file system


*/

#include <EDK.h>
#include "General.h"
#include "CC64File.h"
#include "CIECDevice.h"

const int giBufSize = 4096;

/*
  CString DosName;
  CString Directory;
  long lHeader;
  long lPosition;
  HANDLE hFile;
  byte* pbBuffer;
  int iIn;
  int iOut;
*/

CC64File::CC64File() {
  hFile = INVALID_HANDLE_VALUE;
  pbBuffer = NULL;
}


/////////////////////////////////////////////////////////////////////////////

CC64File::~CC64File() {
  Close();
}


/////////////////////////////////////////////////////////////////////////////

void CC64File::SetDOSError() {
  DWORD dwError = GetLastError();
  switch (dwError) {
  case 0x02:
    error("62,FILE NOT FOUND,00,00");
  default:
    error("99,SYSTEM ERROR %08lX,00,00", dwError);
  }
}


/////////////////////////////////////////////////////////////////////////////

void CC64File::Open(const CString& Dir, const char* pcC64Name, int iLength, int iChannel) {
  Close();

  // initialize member variables
  DosName.Empty();
  Parse(CString(pcC64Name, iLength), iChannel, 70);

  // switch to file directory
  if (!SetCurrentDirectory(Dir)) {
    SetDOSError();
  }
  char* pcBuffer = (char*)MemAlloc(4096);
  if (GetCurrentDirectory(4096, pcBuffer) == 0) {
    MemFree(pcBuffer);
    SetDOSError();
  }
  Directory = pcBuffer;
  MemFree(pcBuffer);

  // either read the directory or open a normal file
  if (fDirectory) {
    trace("CC64File::ReadDirectory\n");
    trace("  Directory = \"%s\"\n", (LPCSTR)Directory);
    trace("  Name = \"%s\"\n", (LPCSTR)Name);
    trace("  fWildcards = %s\n", fWildcards ? "TRUE" : "FALSE");
    trace("  cType = '%c'\n", cType);
    ReadDirectory();
  } else {
    trace("CC64File::OpenFile\n");
    trace("  Directory = \"%s\"\n", (LPCSTR)Directory);
    trace("  Name = \"%s\"\n", (LPCSTR)Name);
    trace("  fOverwrite = %s\n", fOverwrite ? "TRUE" : "FALSE");
    trace("  fWildcards = %s\n", fWildcards ? "TRUE" : "FALSE");
    trace("  cType = '%c'\n", cType);
    trace("  cMode = '%c'\n", cMode);
    OpenFile();
    trace("  DosName = \"%s\"\n", (LPCSTR)DosName);
  }
}


/////////////////////////////////////////////////////////////////////////////

void CC64File::OpenFile() {
  pbBuffer = MemAlloc(giBufSize);
  iIn = 0;
  iOut = 0;
  try {
    // try C64 file with long name
    CString Name = LongName();
    if (cMode == 'R') {
      // read
      if (!OpenExisting(Name)) {
        error("62,FILE NOT FOUND,00,00");
      }
    } else {
      // write
      if (fWildcards) {
        error("33,SYNTAX ERROR,00,00");
      }
      if (OpenExisting(Name)) {
        iOut = iIn;
        if (cMode == 'W') {
          if (!fOverwrite) {
            error("63,FILE EXISTS,00,00");
          }
          if (SetFilePointer(hFile, lHeader, NULL, FILE_BEGIN) == 0xFFFFFFFF) {
            SetDOSError();
          }
          if (!SetEndOfFile(hFile)) {
            SetDOSError();
          }
        } else {
          // append
          if (SetFilePointer(hFile, 0, NULL, FILE_END) == 0xFFFFFFFF) {
            SetDOSError();
          }
          cMode = 'W';
        }
      } else {
        CreateNew(Name);
        cMode = 'W';
      }
    }
  } catch (...) {
    MemFree(pbBuffer);
    pbBuffer = NULL;
    throw;
  }
}

/////////////////////////////////////////////////////////////////////////////

flag CC64File::OpenExisting(const CString& Name) {
  // try C64 file with long name
  if (OpenOneExisting(Name)) {
    return TRUE;
  }
  // try normal DOS file without the extension
  int iExt = Name.ReverseFind('.');
  if (iExt > 0) {
    if (OpenOneExisting(Name.Left(iExt))) {
      return TRUE;
    } 
  }
  // try old 8.3 file name
  if ((iExt > 0 && iExt <= 16) || Name.GetLength() <= 16) {
    return OpenOneExisting(ShortName());
  } else {
    return FALSE;
  }
}

/////////////////////////////////////////////////////////////////////////////

flag CC64File::OpenOneExisting(const CString& Name) {
  // relative files and write/append mode need write permission
  DWORD fdwAccess = GENERIC_READ;
  if (cType == 'R' || cMode != 'R') {
    fdwAccess |= GENERIC_WRITE;
  }
  // relative files use random access, all others use sequential access
  DWORD fdwAttrsAndFlags = FILE_ATTRIBUTE_ARCHIVE;
  if (cType == 'R') {
    fdwAttrsAndFlags |= FILE_FLAG_RANDOM_ACCESS;
  } else {
    fdwAttrsAndFlags |= FILE_FLAG_SEQUENTIAL_SCAN;
  }
  hFile = CreateFile(Name, fdwAccess, FILE_SHARE_READ, NULL, OPEN_EXISTING, fdwAttrsAndFlags, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    // try to open relative read only file
    if (GetLastError() != ERROR_ACCESS_DENIED || cType != 'R') {
      return FALSE;
    }
    hFile = CreateFile(Name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, fdwAttrsAndFlags, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
      return FALSE;
    }
  }
  // read file header
  if (!ReadFile(hFile, pbBuffer, giBufSize, (dword*)&iIn, NULL)) {
    SetDOSError();
  }
  lHeader = 0;
  if (iIn >= 26 && strcmp((char*)pbBuffer, "C64File") == 0) {
    lHeader = 26;
    bRecordLength = pbBuffer[25];
    iOut = 26;
  }
  DosName = Name;
  return TRUE;
}

/////////////////////////////////////////////////////////////////////////////

void CC64File::CreateNew(const CString& Name) {
  // relative files use random access, all others use sequential access
  DWORD fdwAttrsAndFlags = FILE_ATTRIBUTE_ARCHIVE;
  if (cType == 'R') {
    fdwAttrsAndFlags |= FILE_FLAG_RANDOM_ACCESS;
  } else {
    fdwAttrsAndFlags |= FILE_FLAG_SEQUENTIAL_SCAN;
  }
  // create the file
  hFile = CreateFile(Name, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, fdwAttrsAndFlags, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    SetDOSError();
  }
  // write file header
  memset(pbBuffer, 0, 26);
  strcpy((char*)pbBuffer, "C64File");
  strncpy((char*)(pbBuffer + 8), Name, 16);
  pbBuffer[25] = bRecordLength;
  lHeader = 26;
  iIn = 26;
  if (!WriteFile(hFile, pbBuffer, 26, (dword*)&iOut, NULL)) {
    SetDOSError();
  }
  DosName = Name;
}

/////////////////////////////////////////////////////////////////////////////

CString CC64File::LongName() {
  int iLength = Name.GetLength();
  // buffer for result 
  char ac[256];
  char* pc = ac;
  // does the C64Name contain lower case letters?
  flag fToLower = TRUE;
  for (int i = 0; i < iLength; i++) {
    if (islower(Name[i])) {
      fToLower = FALSE;
      break;
    }
  }
  // switch between printable character and `XX`
  flag fEscape = FALSE;
  // convert C64Name to ac
  for (i = 0; i < iLength; i++) {
    byte b = Name[i];
    // is character valid?
    if (isalnum(b) || memchr(" !#$%&'()+-.@^_\xFF", b, 16) != 0) {
      if (b == 0xFF) {
        b = '~';
      }
      // eventually convert upper case letters to lower case
      if (fToLower && isupper(b)) {
        b = (byte)_tolower(b);
      }
      // switch between printable character and `XX`
      if (fEscape) {
        *pc++ = '`';
        fEscape = FALSE;
      }
      // add printable character
      *pc++ = b;
    } else {
      // switch between printable character and `XX`
      if (!fEscape) {
        *pc++ = '`';
        fEscape = TRUE;
      }
      // add `XX` escape sequence
      pc += wsprintf(pc, "%02X", b);
    }
  }
  // add final `
  if (fEscape) {
    *pc++ = '`';
  }
  // add the extension
  switch (cType) {
  case 'P':
    // .prg is reserved for dBase, OzCIS, ...
    strcpy(pc, ".c64");
    break;
  case 'S':
    strcpy(pc, ".s00");
    break;
  case 'U':
    strcpy(pc, ".u00");
    break;
  case 'D':
    strcpy(pc, ".d00");
    break;
  case 'R':
    strcpy(pc, ".r00");
    break;
  case 'C':
    break;
  default:
    strcpy(pc, ".*");
    fWildcards = TRUE;
    break;
  }
  // insert an _ if the file name is a basic device
  if (memicmp(ac, "NUL.", 4) == 0 || memicmp(ac, "CON.", 4) == 0 || memicmp(ac, "AUX.", 4) == 0 || memicmp(ac, "PRN.", 4) == 0 || ((memicmp(ac, "LPT", 3) == 0 || memicmp(ac, "COM", 3) == 0) && isdigit(ac[3]) && ac[4] == '.')) {
    ASSERT(strlen(ac) >= 4);
    char* pcExt = ac + 3;
    if (*pcExt != '.') {
      pcExt++;
    }
    ASSERT(*pcExt == '.');
    memmove(pcExt + 1, pcExt, strlen(pcExt) + 1);
    *pcExt = '_';
  }
  ASSERT(strlen(ac) < sizeof ac);
  return CString(ac);
}

/////////////////////////////////////////////////////////////////////////////

CString CC64File::ShortName() {
  char ac[16];
  int iLength;
  int iGood;
  int iSkip;
  int i;
  // reduction is impossible if the file name contains * or ?
  if (fWildcards) {
    ac[0] = '*';
    iLength = 1;
    iGood = 1;
    goto Finished;
  }
  // get the C64 name into manipulation buffer
  iLength = Name.GetLength();
  ASSERT(iLength <= 16);
  memcpy(ac, Name, iLength);
  // delete invalid characters
  iGood = iLength;
  for (i = 0; i < iLength; i++) {
    switch (ac[i]) {
    case ' ':
    case '-':
      // replace spaces and dashes with underscores
      ac[i] = '_';
      break;
    default:
      // convert lower case letters to upper case
      if (islower((byte)ac[i])) {
        ac[i] = (char)_toupper(ac[i]);
        break;
      }
      // upper case letters and digits are OK
      if (isalnum((byte)ac[i])) {
        break;
      }
      // otherwise remove the invalid character
      ac[i] = 0;
      iGood--;
    }
  }
  // reduce the name to 8 characters
  if (iGood <= 8) {
    goto Finished;
  }
  // remove underscores from the right
  for (i = iLength - 1; i >= 0; i--) {
    if (ac[i] == '_') {
      ac[i] = 0;
      if (--iGood <= 8) {
        goto Finished;
      }
    }
  }
  // skip vocals at the beginning
  for (iSkip = 0; iSkip < iLength; iSkip++) {
    if (ac[iSkip] != 0 && !memchr("AEIOU", ac[iSkip], 5)) {
      break;
    }
  }
  // remove vocals from the right
  for (i = iLength - 1; i >= iSkip; i--) {
    if (memchr("AEIOU", ac[i], 5)) {
      ac[i] = 0;
      if (--iGood <= 8) {
        goto Finished;
      }
    }
  }
  // remove consonants from the right
  for (i = iLength - 1; i >= 0; i--) {
    if (isalpha((byte)ac[i])) {
      ac[i] = 0;
      if (--iGood <= 8) {
        goto Finished;
      }
    }
  }
  // remove the remaining digits from the left
  for (i = 0; i < iLength; i++) {
    if (ac[i] != 0) {
      ac[i] = 0;
      if (--iGood <= 8) {
        goto Finished;
      }
    }
  }
Finished:
  // dummy name if no characters were valid
  if (iGood < 1) {
    ac[0] = '_';
    iLength = 1;
    iGood = 1;
  }
  // build the DOS name
  CString Name;
  char* pcName = Name.GetBuffer(iGood + 1 + 4 + 1);
  char* pc = pcName;
  for (i = 0; i < iLength; i++) {
    if (ac[i]) {
      *pc++ = ac[i];
    }
  }
  // add an _ if the file name is a basic device
  *pc = 0;
  if (strcmp(pcName, "NUL") == 0 || strcmp(pcName, "CON") == 0 || strcmp(pcName, "AUX") == 0 || strcmp(pcName, "PRN") == 0 || (pc - pcName == 4 && (memcmp(pcName, "LPT", 3) == 0 || memcmp(pcName, "COM", 3) == 0) && isdigit(pcName[3]))) {
    *pc++ = '_';
  }
  // add the extension
  *pc++ = '.';
  *pc++ = (char)cType;
  // ambiguous file names are no longer supported
  *pc++ = '0';
  *pc++ = '0';
  *pc = 0;
  ASSERT(pc - pcName <= iGood + 4);
  Name.ReleaseBuffer(pc - pcName);
  fWildcards = TRUE;
  return Name;
}

/////////////////////////////////////////////////////////////////////////////

static int HexToBin(int i) {
  return isdigit(i) ? i - '0' : toupper(i) - 'A' + 0x0A;
}

/////////////////////////////////////////////////////////////////////////////

char* GetC64ExtAndCutDOSName(char* pcDOSName) {
  char* pcExt = "PRG";
  char* pc = strrchr(pcDOSName, '.');
  if (pc != NULL && strlen(pc) == 4) {
    if (_stricmp(pc, ".c64") == 0) {
      *pc = 0;
    } else if (_stricmp(pc, ".s00") == 0) {
      pcExt = "SEQ";
      *pc = 0;
    } else if (_stricmp(pc, ".u00") == 0) {
      pcExt = "USR";
      *pc = 0;
    } else if (_stricmp(pc, ".r00") == 0) {
      pcExt = "REL";
      *pc = 0;
    } else if (_stricmp(pc, ".d00") == 0) {
      pcExt = "DEL";
      *pc = 0;
    }
  }
  return pcExt;
}

char* DOSNameToC64Name(char* pcDOS, int iMaxLength) {
  ASSERT(iMaxLength < 80);
  static char acC64[80];
  int i = 0;
  do {
    switch (*pcDOS) {
    case 0:
      acC64[i] = 0;
      return acC64;
    case '`':
      if (strchr(pcDOS + 1, '`') && isxdigit(pcDOS[1]) && isxdigit(pcDOS[2])) {
        pcDOS++;
        do {
          acC64[i++] = (char)(HexToBin(pcDOS[0]) * 16 + HexToBin(pcDOS[1]));
          if (i >= iMaxLength) {
            acC64[iMaxLength - 1] = '*';
            acC64[iMaxLength] = 0;
            return acC64;
          }
          pcDOS += 2;
        } while (isxdigit(pcDOS[0]) && isxdigit(pcDOS[1]));
        if (*pcDOS == '`') {
          pcDOS++;
        }
      }
      // fall through
    default:
      acC64[i++] = (char)toupper(*pcDOS++);
    }
  } while (i <= iMaxLength);
  acC64[iMaxLength - 1] = '*';
  acC64[iMaxLength] = 0;
  return acC64;
}

/////////////////////////////////////////////////////////////////////////////

void CC64File::ReadDirectory() {
  byte* pbTemp = MemAlloc(256 * 1024);
  byte* pb = pbTemp;
  // load address
  *pb++ = 0x01;
  *pb++ = 0x04;
  // dummy link pointer
  *pb++ = 0x01;
  *pb++ = 0x01;
  // line number
  *pb++ = 0x00;
  *pb++ = 0x00;
  // DOS directory as disk name
  char* pcDir = strrchr(LPCTSTR(Directory), '\\');
  if (pcDir) {
    pcDir = (char*)DOSNameToC64Name(pcDir + 1, 16);
  } else {
    pcDir = "?UNKNOWN DIR";
  }
  pb += sprintf((char*)pb, "\022\"%-16.16s\" %c: 00", pcDir, toupper(Directory[0])) + 1;
  WIN32_FIND_DATA wfd;
  HANDLE hFindFile = FindFirstFile("*.*", &wfd);
  if (hFindFile != INVALID_HANDLE_VALUE) {
    do {
      // dummy link pointer
      *pb++ = 0x01;
      *pb++ = 0x01;
      // file size in blocks
      int iBlocks = (wfd.nFileSizeLow - 26 + 253) / 254;
      if (wfd.nFileSizeHigh != 0 || iBlocks > 65535) {
        iBlocks = 65535;
      }
      *pb++ = (byte)iBlocks;
      *pb++ = (byte)(iBlocks >> 8);
      byte* pbEnd = pb + 27;
      if (iBlocks < 1000) {
        *pb++ = ' ';
        if (iBlocks < 100) {
          *pb++ = ' ';
          if (iBlocks < 10) {
            *pb++ = ' ';
          }
        }
      }
      // C64 extension
      char* pcExt;
      if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
        pcExt = "CBM";
      } else {
        pcExt = GetC64ExtAndCutDOSName(wfd.cFileName);
      }
      char cReadOnly = ' ';
      if (wfd.dwFileAttributes & FILE_ATTRIBUTE_READONLY) {
        cReadOnly = '<';
      }
      // C64 file name
      char* pcC64Name = DOSNameToC64Name(wfd.cFileName, 16);
      if (!strchr(pcC64Name, '"')) {
        strcat(pcC64Name, "\"");
      }
      pb += sprintf((char*)pb, "\"%-17.17s %s%c", pcC64Name, pcExt, cReadOnly);
      while (pb < pbEnd) {
        *pb++ = ' ';
      }
      *pb++ = 0;
    } while (FindNextFile(hFindFile, &wfd));
  }
  FindClose(hFindFile);
  // dummy link pointer
  *pb++ = 0x01;
  *pb++ = 0x01;
  // free blocks
  int iFreeBlocks = 0;
  DWORD dwSectorsPerCluster;
  DWORD dwBytesPerSector;
  DWORD dwNumberOfFreeClusters;
  DWORD dwTotalNumberOfClusters;
  if (GetDiskFreeSpace(NULL, &dwSectorsPerCluster, &dwBytesPerSector, &dwNumberOfFreeClusters, &dwTotalNumberOfClusters)) {
    double r = (double)dwNumberOfFreeClusters * (double)dwSectorsPerCluster * (double)dwBytesPerSector / 256;
    if (r < 65536) {
      iFreeBlocks = (int)r;
    } else {
      iFreeBlocks = 65535;
    }
  }
  *pb++ = (byte)iFreeBlocks;
  *pb++ = (byte)(iFreeBlocks >> 8);
  pb += sprintf((char*)pb, "BLOCKS FREE.             ");
  *pb++ = 0;
  *pb++ = 0;
  *pb++ = 0;
  // resize memory block
  int iSize = pb - pbTemp;
  pbBuffer = MemAlloc(iSize);
  memcpy(pbBuffer, pbTemp, iSize);
  MemFree(pbTemp);
  iIn = iSize;
  iOut = 0;
}

/////////////////////////////////////////////////////////////////////////////

void CC64File::Close() {
  if (hFile != INVALID_HANDLE_VALUE) {
    if (pbBuffer != NULL && iOut > iIn) {
      DWORD dw;
      if (!WriteFile(hFile, pbBuffer + iIn, iOut - iIn, &dw, NULL)) {
        SetDOSError();
      }
      iIn += dw;
      if (iIn < iOut) {
        error("72,DISK FULL,00,00");
      }
    }
    CloseHandle(hFile);
    hFile = INVALID_HANDLE_VALUE;
  }
  if (pbBuffer != NULL) {
    MemFree(pbBuffer);
    pbBuffer = NULL;
  }
}

/////////////////////////////////////////////////////////////////////////////

int CC64File::Put(int iData) {
  if (cMode != 'W' || pbBuffer == NULL) {
    return TIMEOUT_RECEIVE;
  }
  if (iOut >= giBufSize) {
    DWORD dw;
    if (!WriteFile(hFile, pbBuffer + iIn, iOut - iIn, &dw, NULL)) {
      SetDOSError();
    }
    iIn += dw;
    if (iIn < iOut) {
      error("72,DISK FULL,00,00");
    }
    iOut = 0;
    iIn = 0;
  }
  pbBuffer[iOut++] = (byte)iData;
  return 0;
}

/////////////////////////////////////////////////////////////////////////////

int CC64File::Get() {
  if (cMode != 'R' || pbBuffer == NULL) {
    return TIMEOUT_SEND;
  }
  if (iOut >= iIn) {
    if (fDirectory) {
      return TIMEOUT_SEND;
    }
    iIn = 0;
    iOut = 0;
    if (!ReadFile(hFile, pbBuffer, giBufSize, (DWORD*)&iIn, NULL)) {
      SetDOSError();
    }
    if (iOut >= iIn) {
      return TIMEOUT_SEND;
    }
  }
  int i = pbBuffer[iOut++];
  if (iOut >= iIn) {
    if (fDirectory) {
      return i | END_OF_FILE;
    }
    iIn = 0;
    iOut = 0;
    if (!ReadFile(hFile, pbBuffer, giBufSize, (DWORD*)&iIn, NULL)) {
      SetDOSError();
    }
    if (iOut >= iIn) {
      return i | END_OF_FILE;
    }
  }
  return i;
}

/////////////////////////////////////////////////////////////////////////////

int CC64File::Read(byte* pbDest, int iCount) {
  if (cMode != 'R' || pbBuffer == NULL) {
    return 0;
  }
  int i = iIn - iOut;
  if (i > 0) {
    if (i > iCount) {
      i = iCount;
    }
    memcpy(pbDest, pbBuffer + iOut, i);
    iOut += i;
    pbDest += i;
    iCount -= i;
  } else {
    i = 0;
  }
  if (iCount > 0 && !fDirectory) {
    DWORD dwRead;
    if (!ReadFile(hFile, pbDest, iCount, &dwRead, NULL)) {
      SetDOSError();
    }
    i += dwRead;
  }
  return i;
}

/////////////////////////////////////////////////////////////////////////////

int CC64File::Write(const byte* /*pbSrc*/, int /*iCount*/) {
  error("CC64File::Write not implemented");
  return 0;
}
