/**
 * Copyright (c) 2011-2020 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 "ExFatFileWrite.cpp"
#include "../common/DebugMacros.h"
#include "ExFatFile.h"
#include "ExFatVolume.h"
#include "upcase.h"


namespace sdfat {


//==============================================================================
#if READ_ONLY
bool ExFatFile::mkdir(ExFatFile* parent, const ExChar_t* path, bool pFlag) {
  (void) parent;
  (void)path;
  (void)pFlag;
  return false;
}
bool ExFatFile::preAllocate(uint64_t length) {
  (void)length;
  return false;
}
bool ExFatFile::rename(const ExChar_t* newPath) {
  (void)newPath;
  return false;
}
bool ExFatFile::rename(ExFatFile* dirFile, const ExChar_t* newPath) {
  (void)dirFile;
  (void)newPath;
  return false;
}
bool ExFatFile::sync() {
  return false;
}
bool ExFatFile::truncate() {
  return false;
}
size_t ExFatFile::write(const void* buf, size_t nbyte) {
  (void)buf;
  (void)nbyte;
  return false;
}
//==============================================================================
#else  // READ_ONLY
//------------------------------------------------------------------------------
static uint16_t exFatDirChecksum(const uint8_t* data, uint16_t checksum) {
  bool skip = data[0] == EXFAT_TYPE_FILE;
  for (size_t i = 0; i < 32; i += i == 1 && skip ? 3 : 1) {
    checksum = ((checksum << 15) | (checksum >> 1)) + data[i];
  }
  return checksum;
}
//------------------------------------------------------------------------------
bool ExFatFile::addCluster() {
  uint32_t find = m_vol->bitmapFind(m_curCluster ?  m_curCluster + 1 : 0, 1);
  if (find < 2) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (!m_vol->bitmapModify(find, 1, 1)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (m_curCluster == 0) {
    m_flags |= FILE_FLAG_CONTIGUOUS;
    goto done;
  }
  if (isContiguous()) {
    if (find == (m_curCluster + 1)) {
      goto done;
    }
    // No longer contiguous so make FAT chain.
    m_flags &= ~FILE_FLAG_CONTIGUOUS;

    for (uint32_t c = m_firstCluster; c < m_curCluster; c++) {
      if (!m_vol->fatPut(c, c + 1)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
  }
  // New cluster is EOC.
  if (!m_vol->fatPut(find, EXFAT_EOC)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Connect new cluster to existing chain.
  if (m_curCluster) {
    if (!m_vol->fatPut(m_curCluster, find)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  }

 done:
  m_curCluster = find;
  return true;

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::addDirCluster() {
  uint32_t sector;
  uint32_t dl = isRoot() ? m_vol->rootLength() : m_dataLength;
  uint8_t* cache;
  dl += m_vol->bytesPerCluster();
  if (dl >= 0X4000000) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (!addCluster()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  cache =  m_vol->cacheClear();
  if (!cache) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  memset(cache, 0, m_vol->bytesPerSector());
  sector = m_vol->clusterStartSector(m_curCluster);
  for (uint32_t i = 0; i < m_vol->sectorsPerCluster(); i++) {
    if (!m_vol->writeSector(sector + i, cache)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  }
  if (!isRoot()) {
    m_flags |= FILE_FLAG_DIR_DIRTY;
    m_dataLength  += m_vol->bytesPerCluster();
    m_validLength += m_vol->bytesPerCluster();
  }
  return sync();

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::mkdir(ExFatFile* parent, const ExChar_t* path, bool pFlag) {
  ExName_t fname;
  ExFatFile tmpDir;

  if (isOpen() || !parent->isDir()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (isDirSeparator(*path)) {
    while (isDirSeparator(*path)) {
      path++;
    }
    if (!tmpDir.openRoot(parent->m_vol)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    parent = &tmpDir;
  }
  while (1) {
    if (!parsePathName(path, &fname, &path)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    if (!*path) {
      break;
    }
    if (!open(parent, &fname, O_RDONLY)) {
      if (!pFlag || !mkdir(parent, &fname)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
    tmpDir = *this;
    parent = &tmpDir;
    close();
  }
  return mkdir(parent, &fname);

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::mkdir(ExFatFile* parent, ExName_t* fname) {
  if (!parent->isDir()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // create a normal file
  if (!open(parent, fname, O_CREAT | O_EXCL | O_RDWR)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // convert file to directory

  m_attributes = FILE_ATTR_SUBDIR;

  // allocate and zero first cluster
  if (!addDirCluster()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  m_firstCluster = m_curCluster;

  // Set to start of dir
  rewind();
  m_flags = FILE_FLAG_READ | FILE_FLAG_CONTIGUOUS | FILE_FLAG_DIR_DIRTY;
  return sync();

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::preAllocate(uint64_t length) {
  uint32_t find;
  uint32_t need;
  if (!length || !isWritable() || m_firstCluster) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  need = 1 + ((length - 1) >> m_vol->bytesPerClusterShift());
  find = m_vol->bitmapFind(0, need);
  if (find < 2) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (!m_vol->bitmapModify(find, need, 1)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  m_dataLength = length;
  m_firstCluster = find;
  m_flags |= FILE_FLAG_DIR_DIRTY | FILE_FLAG_CONTIGUOUS;
  if (!sync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  return true;

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::remove() {
  DirPos_t pos = m_dirPos;
  uint8_t* cache;
  if (!isWritable()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Free any clusters.
  if (m_firstCluster) {
    if (isContiguous()) {
      uint32_t nc = 1 + ((m_dataLength - 1) >> m_vol->bytesPerClusterShift());
      if (!m_vol->bitmapModify(m_firstCluster, nc, 0)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    } else {
      if (!m_vol->freeChain(m_firstCluster)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
  }

  for (uint8_t i = 0; i <= m_setCount; i++) {
    if (i && m_vol->dirSeek(&pos, 32) != 1) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    cache = m_vol->dirCache(&pos, FsCache::CACHE_FOR_WRITE);
    if (!cache) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    // Mark entry not used.
    cache[0] &= 0x7F;
  }
  // Set this file closed.
  m_attributes = FILE_ATTR_CLOSED;
  m_flags = 0;

  // Write entry to device.
  return m_vol->cacheSync();

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::rename(const ExChar_t* newPath) {
  return rename(m_vol->vwd(), newPath);
}
//------------------------------------------------------------------------------
bool ExFatFile::rename(ExFatFile* dirFile, const ExChar_t* newPath) {
  ExFatFile file;
  ExFatFile oldFile;

  // Must be an open file or subdirectory.
  if (!(isFile() || isSubDir())) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Can't move file to new volume.
  if (m_vol != dirFile->m_vol) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (!file.open(dirFile, newPath, O_CREAT | O_EXCL | O_WRONLY)) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  oldFile = *this;
  m_dirPos = file.m_dirPos;
  m_setCount = file.m_setCount;
  m_flags |= FILE_FLAG_DIR_DIRTY;
  if (!sync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // Remove old directory entry;
  oldFile.m_firstCluster = 0;
  oldFile.m_flags = FILE_FLAG_WRITE;
  oldFile.m_attributes = FILE_ATTR_FILE;
  return oldFile.remove();

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::rmdir() {
  int n;
  uint8_t dir[32];
  // must be open subdirectory
  if (!isSubDir()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  rewind();

  // make sure directory is empty
  while (1) {
    n = read(dir, 32);
    if (n == 0) {
      break;
    }
    if (n != 32 || dir[0] & 0X80) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    if (dir[0] == 0) {
      break;
    }
  }
  // convert empty directory to normal file for remove
  m_attributes = FILE_ATTR_FILE;
  m_flags |= FILE_FLAG_WRITE;
  return remove();

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::sync() {
  if (!isOpen()) {
    return true;
  }
  if (m_flags & FILE_FLAG_DIR_DIRTY) {
    // clear directory dirty
    m_flags &= ~FILE_FLAG_DIR_DIRTY;
    return syncDir();
  }
  if (!m_vol->cacheSync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  return true;

 fail:
  m_error |= WRITE_ERROR;
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::syncDir() {
  DirFile_t* df;
  DirStream_t* ds;
  uint8_t* cache;
  uint16_t checksum = 0;
  uint8_t setCount = 0;

  DirPos_t pos = m_dirPos;

  for (uint8_t i = 0;; i++) {
    cache = m_vol->dirCache(&pos, FsCache::CACHE_FOR_READ);
    if (!cache) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    switch (cache[0]) {
      case EXFAT_TYPE_FILE:
        df = reinterpret_cast<DirFile_t*>(cache);
        setCount = df->setCount;
        setLe16(df->attributes, m_attributes & FILE_ATTR_COPY);
        if (FsDateTime::callback) {
          uint16_t date, time;
          uint8_t ms10;
          FsDateTime::callback(&date, &time, &ms10);
          df->modifyTimeMs = ms10;
          setLe16(df->modifyTime, time);
          setLe16(df->modifyDate, date);
          setLe16(df->accessTime, time);
          setLe16(df->accessDate, date);
        }
        m_vol->dataCacheDirty();
        break;

      case EXFAT_TYPE_STREAM:
        ds = reinterpret_cast<DirStream_t*>(cache);
        if (isContiguous()) {
          ds->flags |= EXFAT_FLAG_CONTIGUOUS;
        } else {
          ds->flags &= ~EXFAT_FLAG_CONTIGUOUS;
        }
        setLe64(ds->validLength, m_validLength);
        setLe32(ds->firstCluster, m_firstCluster);
        setLe64(ds->dataLength, m_dataLength);
        m_vol->dataCacheDirty();
        break;

      case EXFAT_TYPE_NAME:
        break;

      default:
        DBG_FAIL_MACRO;
        goto fail;
        break;
    }
    checksum = exFatDirChecksum(cache, checksum);
    if (i == setCount) break;
    if (m_vol->dirSeek(&pos, 32) != 1) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  }
  df = reinterpret_cast<DirFile_t*>
       (m_vol->dirCache(&m_dirPos, FsCache::CACHE_FOR_WRITE));
  if (!df) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  setLe16(df->setChecksum, checksum);
  if (!m_vol->cacheSync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  return true;

 fail:
  m_error |= WRITE_ERROR;
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::timestamp(uint8_t flags, uint16_t year, uint8_t month,
                   uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) {
  DirFile_t* df;
  uint8_t* cache;
  uint16_t checksum = 0;
  uint8_t setCount = 0;
  uint16_t date;
  uint16_t time;
  uint8_t ms10;
  DirPos_t pos;

  if (!isFile()
      || year < 1980
      || year > 2107
      || month < 1
      || month > 12
      || day < 1
      || day > 31
      || hour > 23
      || minute > 59
      || second > 59) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // update directory entry
  if (!sync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }

  date = FS_DATE(year, month, day);
  time = FS_TIME(hour, minute, second);
  ms10 = second & 1 ? 100 : 0;
  pos = m_dirPos;

  for (uint8_t i = 0;; i++) {
    cache = m_vol->dirCache(&pos, FsCache::CACHE_FOR_READ);
    if (!cache) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    switch (cache[0]) {
      case EXFAT_TYPE_FILE:
        df = reinterpret_cast<DirFile_t*>(cache);
        setCount = df->setCount;
        setLe16(df->attributes, m_attributes & FILE_ATTR_COPY);
        m_vol->dataCacheDirty();
        if (flags & T_ACCESS) {
          setLe16(df->accessTime, time);
          setLe16(df->accessDate, date);
        }
        if (flags & T_CREATE) {
          df->createTimeMs = ms10;
          setLe16(df->createTime, time);
          setLe16(df->createDate, date);
        }
        if (flags & T_WRITE) {
          df->modifyTimeMs = ms10;
          setLe16(df->modifyTime, time);
          setLe16(df->modifyDate, date);
        }
        break;

      case EXFAT_TYPE_STREAM:
        break;

      case EXFAT_TYPE_NAME:
        break;

      default:
        DBG_FAIL_MACRO;
        goto fail;
        break;
    }
    checksum = exFatDirChecksum(cache, checksum);
    if (i == setCount) break;
    if (m_vol->dirSeek(&pos, 32) != 1) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  }
  df = reinterpret_cast<DirFile_t*>
       (m_vol->dirCache(&m_dirPos, FsCache::CACHE_FOR_WRITE));
  if (!df) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  setLe16(df->setChecksum, checksum);
  if (!m_vol->cacheSync()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  return true;

 fail:
  return false;
}
//------------------------------------------------------------------------------
bool ExFatFile::truncate() {
  uint32_t toFree;
  // error if not a normal file or read-only
  if (!isWritable()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  if (m_firstCluster == 0) {
      return true;
  }
  if (isContiguous()) {
    uint32_t nc = 1 + ((m_dataLength - 1) >> m_vol->bytesPerClusterShift());
    if (m_curCluster) {
      toFree = m_curCluster + 1;
      nc -= 1 + m_curCluster - m_firstCluster;
    } else {
      toFree = m_firstCluster;
      m_firstCluster = 0;
    }
    if (nc && !m_vol->bitmapModify(toFree, nc, 0)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  } else {
    // need to free chain
    if (m_curCluster) {
      toFree = 0;
      int8_t fg = m_vol->fatGet(m_curCluster, &toFree);
      if (fg < 0) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (fg) {
        // current cluster is end of chain
        if (!m_vol->fatPut(m_curCluster, EXFAT_EOC)) {
          DBG_FAIL_MACRO;
          goto fail;
        }
      }
    } else {
      toFree = m_firstCluster;
      m_firstCluster = 0;
    }
    if (toFree) {
      if (!m_vol->freeChain(toFree)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
  }
  m_dataLength = m_curPosition;
  m_validLength = m_curPosition;
  m_flags |= FILE_FLAG_DIR_DIRTY;
  return sync();

 fail:
  return false;
}
//------------------------------------------------------------------------------
size_t ExFatFile::write(const void* buf, size_t nbyte) {
  // convert void* to uint8_t*  -  must be before goto statements
  const uint8_t* src = reinterpret_cast<const uint8_t*>(buf);
  uint8_t* cache;
  uint8_t cacheOption;
  uint16_t sectorOffset;
  uint32_t sector;
  uint32_t clusterOffset;

  // number of bytes left to write  -  must be before goto statements
  size_t toWrite = nbyte;
  size_t n;
  // error if not an open file or is read-only
  if (!isWritable()) {
    DBG_FAIL_MACRO;
    goto fail;
  }
  // seek to end of file if append flag
  if ((m_flags & FILE_FLAG_APPEND)) {
    if (!seekSet(m_validLength)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  }
  while (toWrite) {
    clusterOffset = m_curPosition & m_vol->clusterMask();
    sectorOffset = clusterOffset & m_vol->sectorMask();
    if (clusterOffset == 0) {
      // start of new cluster
      if (m_curCluster != 0) {
        int fg;

        if (isContiguous()) {
          uint32_t lc = m_firstCluster;
          lc += (m_dataLength - 1) >> m_vol->bytesPerClusterShift();
          if (m_curCluster < lc) {
            m_curCluster++;
            fg = 1;
          } else {
            fg = 0;
          }
        } else {
          fg = m_vol->fatGet(m_curCluster, &m_curCluster);
          if (fg < 0) {
            DBG_FAIL_MACRO;
            goto fail;
          }
        }
        if (fg == 0) {
          // add cluster if at end of chain
          if (!addCluster()) {
            DBG_FAIL_MACRO;
            goto fail;
          }
        }
      } else {
        if (m_firstCluster == 0) {
          // allocate first cluster of file
          if (!addCluster()) {
            DBG_FAIL_MACRO;
            goto fail;
          }
          m_firstCluster = m_curCluster;
        } else {
          m_curCluster = m_firstCluster;
        }
      }
    }
    // sector for data write
    sector = m_vol->clusterStartSector(m_curCluster) +
             (clusterOffset >> m_vol->bytesPerSectorShift());

    if (sectorOffset != 0 || toWrite < m_vol->bytesPerSector()) {
      // partial sector - must use cache
      // max space in sector
      n = m_vol->bytesPerSector() - sectorOffset;
      // lesser of space and amount to write
      if (n > toWrite) {
        n = toWrite;
      }

      if (sectorOffset == 0 && m_curPosition >= m_validLength) {
        // start of new sector don't need to read into cache
        cacheOption = FsCache::CACHE_RESERVE_FOR_WRITE;
      } else {
        // rewrite part of sector
        cacheOption = FsCache::CACHE_FOR_WRITE;
      }
      cache = m_vol->dataCacheGet(sector, cacheOption);
      if (!cache) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      uint8_t* dst = cache + sectorOffset;
      memcpy(dst, src, n);
      if (m_vol->bytesPerSector() == (n + sectorOffset)) {
        // Force write if sector is full - improves large writes.
        if (!m_vol->dataCacheSync()) {
          DBG_FAIL_MACRO;
          goto fail;
        }
      }
#if USE_MULTI_SECTOR_IO
    } else if (toWrite >= 2*m_vol->bytesPerSector()) {
      // use multiple sector write command
      uint32_t ns = toWrite >> m_vol->bytesPerSectorShift();
      // Limit writes to current cluster.
      uint32_t maxNs = m_vol->sectorsPerCluster()
                       - (clusterOffset >> m_vol->bytesPerSectorShift());
      if (ns > maxNs) {
        ns = maxNs;
      }
      n = ns << m_vol->bytesPerSectorShift();
      // Check for cache sector in write range.
      if (sector <= m_vol->dataCacheSector()
          && m_vol->dataCacheSector() < (sector + ns)) {
        // Invalidate cache if cache sector is in the range.
        m_vol->dataCacheInvalidate();
      }
      if (!m_vol->writeSectors(sector, src, ns)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
#endif  // USE_MULTI_SECTOR_IO
    } else {
      // use single sector write command
      n = m_vol->bytesPerSector();
      if (m_vol->dataCacheSector() == sector) {
        m_vol->dataCacheInvalidate();
      }
      if (!m_vol->writeSector(sector, src)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
    m_curPosition += n;
    src += n;
    toWrite -= n;
    if (m_curPosition > m_validLength) {
      m_flags |= FILE_FLAG_DIR_DIRTY;
      m_validLength = m_curPosition;
    }
  }
  if (m_curPosition > m_dataLength) {
    m_dataLength = m_curPosition;
    // update fileSize and insure sync will update dir entry
    m_flags |= FILE_FLAG_DIR_DIRTY;
  } else if (FsDateTime::callback) {
    // insure sync will update modified date and time
    m_flags |= FILE_FLAG_DIR_DIRTY;
  }
  return nbyte;

 fail:
  // return for write error
  m_error |= WRITE_ERROR;
  return -1;
}
#endif  // READ_ONLY


}; // namespace sdfat
