/**
 * Copyright (c) 2011-2025 Bill Greiman
 * This file is part of the SdFat library for SD memory cards.
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
#define DBG_FILE "FatFileLFN.cpp"
#include "../common/DebugMacros.h"
#include "../common/FsUtf.h"
#include "../common/upcase.h"
#include "FatLib.h"
#if USE_LONG_FILE_NAMES
//------------------------------------------------------------------------------
static bool isLower(char c) { return 'a' <= c && c <= 'z'; }
//------------------------------------------------------------------------------
static bool isUpper(char c) { return 'A' <= c && c <= 'Z'; }
//------------------------------------------------------------------------------
// A bit smaller than toupper in AVR 328.
inline char toUpper(char c) { return isLower(c) ? c - 'a' + 'A' : c; }
//------------------------------------------------------------------------------
/**
 * Store a 16-bit long file name character.
 *
 * \param[in] ldir Pointer to long file name directory entry.
 * \param[in] i Index of character.
 * \param[in] c The 16-bit character.
 */
static void putLfnChar(DirLfn_t* ldir, uint8_t i, uint16_t c) {
  if (i < 5) {
    setLe16(ldir->unicode1 + 2 * i, c);
  } else if (i < 11) {
    setLe16(ldir->unicode2 + 2 * (i - 5), c);
  } else if (i < 13) {
    setLe16(ldir->unicode3 + 2 * (i - 11), c);
  }
}
//==============================================================================
bool FatFile::cmpName(uint16_t index, FatLfn_t* fname, uint8_t lfnOrd) {
  FatFile dir;
  dir.copy(this);
  const DirLfn_t* ldir;
  fname->reset();
  for (uint8_t order = 1; order <= lfnOrd; order++) {
    ldir = reinterpret_cast<DirLfn_t*>(dir.cacheDir(index - order));
    if (!ldir) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    // These should be checked in caller.
    DBG_HALT_IF(ldir->attributes != FAT_ATTRIB_LONG_NAME);
    DBG_HALT_IF(order != (ldir->order & 0X1F));
    for (uint8_t i = 0; i < 13; i++) {
      uint16_t u = getLfnChar(ldir, i);
      if (fname->atEnd()) {
        return u == 0;
      }
#if USE_UTF8_LONG_NAMES
      uint16_t cp = fname->get16();
      // Make sure caller checked for valid UTF-8.
      DBG_HALT_IF(cp == 0XFFFF);
      if (toUpcase(u) != toUpcase(cp)) {
        return false;
      }
#else   // USE_UTF8_LONG_NAMES
      if (u > 0X7F || toUpper(u) != toUpper(fname->getch())) {
        return false;
      }
#endif  // USE_UTF8_LONG_NAMES
    }
  }
  return true;

fail:
  return false;
}
//------------------------------------------------------------------------------
bool FatFile::createLFN(uint16_t index, FatLfn_t* fname, uint8_t lfnOrd) {
  FatFile dir;
  dir.copy(this);
  DirLfn_t* ldir;
  uint8_t checksum = lfnChecksum(fname->sfn);
  uint8_t fc = 0;
  fname->reset();

  for (uint8_t order = 1; order <= lfnOrd; order++) {
    ldir = reinterpret_cast<DirLfn_t*>(dir.cacheDir(index - order));
    if (!ldir) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    dir.m_vol->cacheDirty();
    ldir->order = order == lfnOrd ? FAT_ORDER_LAST_LONG_ENTRY | order : order;
    ldir->attributes = FAT_ATTRIB_LONG_NAME;
    ldir->mustBeZero1 = 0;
    ldir->checksum = checksum;
    setLe16(ldir->mustBeZero2, 0);
    for (uint8_t i = 0; i < 13; i++) {
      uint16_t cp;
      if (fname->atEnd()) {
        cp = fc++ ? 0XFFFF : 0;
      } else {
        cp = fname->get16();
        // Verify caller checked for valid UTF-8.
        DBG_HALT_IF(cp == 0XFFFF);
      }
      putLfnChar(ldir, i, cp);
    }
  }
  return true;

fail:
  return false;
}
//------------------------------------------------------------------------------
bool FatFile::makeSFN(FatLfn_t* fname) {
  bool is83;
  //  char c;
  uint8_t c;
  uint8_t bit = FAT_CASE_LC_BASE;
  uint8_t lc = 0;
  uint8_t uc = 0;
  uint8_t i = 0;
  uint8_t in = 7;
  const char* dot;
  const char* end = fname->end;
  const char* ptr = fname->begin;

  // Assume not zero length.
  DBG_HALT_IF(end == ptr);
  // Assume blanks removed from start and end.
  DBG_HALT_IF(*ptr == ' ' || *(end - 1) == ' ' || *(end - 1) == '.');

  // Blank file short name.
  for (uint8_t k = 0; k < 11; k++) {
    fname->sfn[k] = ' ';
  }
  // Not 8.3 if starts with dot.
  is83 = *ptr == '.' ? false : true;
  // Skip leading dots.
  for (; *ptr == '.'; ptr++) {
  }
  // Find last dot.
  for (dot = end - 1; dot > ptr && *dot != '.'; dot--) {
  }

  for (; ptr < end; ptr++) {
    c = *ptr;
    if (c == '.' && ptr == dot) {
      in = 10;                // Max index for full 8.3 name.
      i = 8;                  // Place for extension.
      bit = FAT_CASE_LC_EXT;  // bit for extension.
    } else {
      if (sfnReservedChar(c)) {
        is83 = false;
        // Skip UTF-8 trailing characters.
        if ((c & 0XC0) == 0X80) {
          continue;
        }
        c = '_';
      }
      if (i > in) {
        is83 = false;
        if (in == 10 || ptr > dot) {
          // Done - extension longer than three characters or no extension.
          break;
        }
        // Skip to dot.
        ptr = dot - 1;
        continue;
      }
      if (isLower(c)) {
        c += 'A' - 'a';
        lc |= bit;
      } else if (isUpper(c)) {
        uc |= bit;
      }
      fname->sfn[i++] = c;
      if (i < 7) {
        fname->seqPos = i;
      }
    }
  }
  if (fname->sfn[0] == ' ') {
    DBG_HALT_MACRO;
    goto fail;
  }
  if (is83) {
    fname->flags = (lc & uc) ? FNAME_FLAG_MIXED_CASE : lc;
  } else {
    fname->flags = FNAME_FLAG_LOST_CHARS;
    fname->sfn[fname->seqPos] = '~';
    fname->sfn[fname->seqPos + 1] = '1';
  }
  return true;

fail:
  return false;
}
//------------------------------------------------------------------------------
bool FatFile::makeUniqueSfn(FatLfn_t* fname) {
  const uint8_t FIRST_HASH_SEQ = 2;  // min value is 2
  uint8_t pos = fname->seqPos;
  const DirFat_t* dir;
  uint16_t hex = 0;

  DBG_HALT_IF(!(fname->flags & FNAME_FLAG_LOST_CHARS));
  DBG_HALT_IF(fname->sfn[pos] != '~' && fname->sfn[pos + 1] != '1');

  for (uint8_t seq = FIRST_HASH_SEQ; seq < 100; seq++) {
    DBG_WARN_IF(seq > FIRST_HASH_SEQ);
    hex += millis();
    if (pos > 3) {
      // Make space in name for ~HHHH.
      pos = 3;
    }
    for (uint8_t i = pos + 4; i > pos; i--) {
      uint8_t h = hex & 0XF;
      fname->sfn[i] = h < 10 ? h + '0' : h + 'A' - 10;
      hex >>= 4;
    }
    fname->sfn[pos] = '~';
    rewind();
    while (1) {
      dir = readDirCache();
      if (!dir) {
        if (!getError()) {
          // At EOF and name not found if no error.
          goto done;
        }
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (dir->name[0] == FAT_NAME_FREE) {
        goto done;
      }
      if (isFatFileOrSubdir(dir) && !memcmp(fname->sfn, dir->name, 11)) {
        // Name found - try another.
        break;
      }
    }
  }
  // fall inti fail - too many tries.
  DBG_FAIL_MACRO;

fail:
  return false;

done:
  return true;
}
//------------------------------------------------------------------------------
bool FatFile::open(FatFile* dirFile, FatLfn_t* fname, oflag_t oflag) {
  bool fnameFound = false;
  uint8_t lfnOrd = 0;
  uint8_t freeFound = 0;
  uint8_t freeNeed;
  uint8_t order = 0;
  uint8_t checksum = 0;
  uint8_t ms10;
  uint8_t nameOrd;
  uint16_t curIndex;
  uint16_t date;
  uint16_t freeIndex = 0;
  uint16_t freeTotal;
  uint16_t time;
  DirFat_t* dir;
  const DirLfn_t* ldir;
  auto vol = dirFile->m_vol;

  if (!dirFile->isDir() || isOpen()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Number of directory entries needed.
  nameOrd = (fname->len + 12) / 13;
  freeNeed = (fname->flags & FNAME_FLAG_NEED_LFN) ? 1 + nameOrd : 1;
  dirFile->rewind();
  while (1) {
    curIndex = dirFile->m_curPosition / FS_DIR_SIZE;
    dir = dirFile->readDirCache();
    if (!dir) {
      if (dirFile->getError()) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      // At EOF
      goto create;
    }
    if (dir->name[0] == FAT_NAME_DELETED || dir->name[0] == FAT_NAME_FREE) {
      if (freeFound == 0) {
        freeIndex = curIndex;
      }
      if (freeFound < freeNeed) {
        freeFound++;
      }
      if (dir->name[0] == FAT_NAME_FREE) {
        goto create;
      }
    } else {
      if (freeFound < freeNeed) {
        freeFound = 0;
      }
    }
    // skip empty slot or '.' or '..'
    if (dir->name[0] == FAT_NAME_DELETED || dir->name[0] == '.') {
      lfnOrd = 0;
    } else if (isFatLongName(dir)) {
      ldir = reinterpret_cast<DirLfn_t*>(dir);
      if (!lfnOrd) {
        order = ldir->order & 0X1F;
        if (order != nameOrd ||
            (ldir->order & FAT_ORDER_LAST_LONG_ENTRY) == 0) {
          continue;
        }
        lfnOrd = nameOrd;
        checksum = ldir->checksum;
      } else if (ldir->order != --order || checksum != ldir->checksum) {
        lfnOrd = 0;
        continue;
      }
      if (order == 1) {
        if (!dirFile->cmpName(curIndex + 1, fname, lfnOrd)) {
          lfnOrd = 0;
        }
      }
    } else if (isFatFileOrSubdir(dir)) {
      if (lfnOrd) {
        if (1 == order && lfnChecksum(dir->name) == checksum) {
          goto found;
        }
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (!memcmp(dir->name, fname->sfn, sizeof(fname->sfn))) {
        if (!(fname->flags & FNAME_FLAG_LOST_CHARS)) {
          goto found;
        }
        fnameFound = true;
      }
    } else {
      lfnOrd = 0;
    }
  }

found:
  // Don't open if create only.
  if (oflag & O_EXCL) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  goto open;

create:
  // don't create unless O_CREAT and write mode
  if (!(oflag & O_CREAT) || !isWriteMode(oflag)) {
    DBG_WARN_MACRO;
    goto fail;
  }
  // Keep found entries or start at current index if no free entries found.
  if (freeFound == 0) {
    freeIndex = curIndex;
  }
  while (freeFound < freeNeed) {
    dir = dirFile->readDirCache();
    if (!dir) {
      if (dirFile->getError()) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      // EOF if no error.
      break;
    }
    freeFound++;
  }
  // Loop handles the case of huge filename and cluster size one.
  freeTotal = freeFound;
  while (freeTotal < freeNeed) {
    // Will fail if FAT16 root.
    if (!dirFile->addDirCluster()) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    // 16-bit freeTotal needed for large cluster size.
    freeTotal += vol->dirEntriesPerCluster();
  }
  if (fnameFound) {
    if (!dirFile->makeUniqueSfn(fname)) {
      goto fail;
    }
  }
  lfnOrd = freeNeed - 1;
  curIndex = freeIndex + lfnOrd;
  if (!dirFile->createLFN(curIndex, fname, lfnOrd)) {
    goto fail;
  }
  dir = dirFile->cacheDir(curIndex);
  if (!dir) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // initialize as empty file
  memset(dir, 0, sizeof(DirFat_t));
  memcpy(dir->name, fname->sfn, 11);

  // Set base-name and extension lower case bits.
  dir->caseFlags = (FAT_CASE_LC_BASE | FAT_CASE_LC_EXT) & fname->flags;

  // Set timestamps.
  if (FsDateTime::callback) {
    // call user date/time function
    FsDateTime::callback(&date, &time, &ms10);
    setLe16(dir->createDate, date);
    setLe16(dir->createTime, time);
    dir->createTimeMs = ms10;
  } else {
    setLe16(dir->createDate, FS_DEFAULT_DATE);
    setLe16(dir->modifyDate, FS_DEFAULT_DATE);
    setLe16(dir->accessDate, FS_DEFAULT_DATE);
    if (FS_DEFAULT_TIME) {
      setLe16(dir->createTime, FS_DEFAULT_TIME);
      setLe16(dir->modifyTime, FS_DEFAULT_TIME);
    }
  }
  // Force write of entry to device.
  vol->cacheDirty();

open:
  // open entry in cache.
  if (!openCachedEntry(dirFile, curIndex, oflag, lfnOrd)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  return true;

fail:
  return false;
}
//------------------------------------------------------------------------------
bool FatFile::parsePathName(const char* path, FatLfn_t* fname,
                            const char** ptr) {
  size_t len = 0;
  // Skip leading spaces.
  while (*path == ' ') {
    path++;
  }
  fname->begin = path;
  fname->len = 0;
  while (*path && !isDirSeparator(*path)) {
#if USE_UTF8_LONG_NAMES
    uint32_t cp;
    // Allow end = path + 4 since path is zero terminated.
    path = FsUtf::mbToCp(path, path + 4, &cp);
    if (!path) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    len += cp <= 0XFFFF ? 1 : 2;
    if (cp < 0X80 && lfnReservedChar(cp)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
#else   // USE_UTF8_LONG_NAMES
    uint8_t cp = *path++;
    if (cp >= 0X80 || lfnReservedChar(cp)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    len++;
#endif  // USE_UTF8_LONG_NAMES
    if (cp != '.' && cp != ' ') {
      // Need to trim trailing dots spaces.
      fname->len = len;
      fname->end = path;
    }
  }
  if (!fname->len || fname->len > FAT_MAX_LFN_LENGTH) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Advance to next path component.
  for (; *path == ' ' || isDirSeparator(*path); path++) {
  }
  *ptr = path;
  return makeSFN(fname);

fail:
  return false;
}
//------------------------------------------------------------------------------
bool FatFile::remove() {
  bool last;
  uint8_t checksum;
  FatFile dirFile;
  DirFat_t* dir;
  DirLfn_t* ldir;

  // Cant' remove not open for write.
  if (!isWritable()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Free any clusters.
  if (m_firstCluster && !m_vol->freeChain(m_firstCluster)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Cache directory entry.
  dir = cacheDirEntry(FsCache::CACHE_FOR_WRITE);
  if (!dir) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  checksum = lfnChecksum(dir->name);

  // Mark entry deleted.
  dir->name[0] = FAT_NAME_DELETED;

  // Set this file closed.
  m_attributes = FILE_ATTR_CLOSED;
  m_flags = 0;

  // Write entry to device.
  if (!m_vol->cacheSync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (!isLFN()) {
    // Done, no LFN entries.
    return true;
  }
  if (!dirFile.openCluster(this)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  for (uint8_t order = 1; order <= m_lfnOrd; order++) {
    ldir = reinterpret_cast<DirLfn_t*>(dirFile.cacheDir(m_dirIndex - order));
    if (!ldir) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    if (ldir->attributes != FAT_ATTRIB_LONG_NAME ||
        order != (ldir->order & 0X1F) || checksum != ldir->checksum) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    last = ldir->order & FAT_ORDER_LAST_LONG_ENTRY;
    ldir->order = FAT_NAME_DELETED;
    m_vol->cacheDirty();
    if (last) {
      if (!m_vol->cacheSync()) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      return true;
    }
  }
  // Fall into fail.
  DBG_FAIL_MACRO;

fail:
  return false;
}
#endif  // #if USE_LONG_FILE_NAMES
