/*
  xdrv_50_filesystem.ino - unified file system for Tasmota

  Copyright (C) 2020  Gerhard Mutz and Theo Arends

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifdef USE_UFILESYS

// saves 80 bytes of flash, makes the UI cleaner for folders containing lots of files.
// disables recursive folder listing in file UI
//#define UFILESYS_NO_RECURSE_GUI

// Enables serving of static files on /fs/
// costs 1844 bytes of flash and 40 bytes of RAM
// probably not useful on esp8266, but useful on esp32
// You could serve a whole webapp from Tas itself.
//#define UFILESYS_STATIC_SERVING

/*********************************************************************************************\
This driver adds universal file system support for
- ESP8266 (sd card or littlefs on  > 1 M devices with special linker file e.g. eagle.flash.4m2m.ld)
  (makes no sense on 1M devices without sd card)
- ESP32 (sd card or littlefs or sfatfile system).

The sd card chip select is the standard SDCARD_CS or when not found SDCARD_CS_PIN and initializes
the FS System Pointer ufsp which can be used by all standard file system calls.

The only specific call is UfsInfo() which gets the total size (0) and free size (1).

A button is created in the setup section to show up the file directory to download and upload files
subdirectories are supported.

Supported commands:
ufs       fs info
ufstype   get filesytem type 0=none 1=SD  2=Flashfile
ufssize   total size in kB
ufsfree   free size in kB
ufsdelete
ufsrename
ufsrun
ufsServe
ftp       start stop ftp server: 0 = OFF, 1 = SDC, 2 = FlashFile


\*********************************************************************************************/

#define XDRV_50           50

#define UFS_TNONE         0
#define UFS_TSDC          1
#define UFS_TFAT          2
#define UFS_TLFS          3

/*
// In tasmota.ino
#ifdef ESP8266
#include <LittleFS.h>
#include <SPI.h>
#ifdef USE_SDCARD
#include <SD.h>
#include <SDFAT.h>
#endif  // USE_SDCARD
#endif  // ESP8266
#ifdef ESP32
#include <LITTLEFS.h>
#ifdef USE_SDCARD
#include <SD.h>
#endif  // USE_SDCARD
#include "FFat.h"
#include "FS.h"
#endif  // ESP32
*/

// Global file system pointer
FS *ufsp;
// Flash file system pointer
FS *ffsp;
// Local pointer for file managment
FS *dfsp;

char ufs_path[48];
File ufs_upload_file;
uint8_t ufs_dir;
// 0 = None, 1 = SD, 2 = ffat, 3 = littlefs
uint8_t ufs_type;
uint8_t ffs_type;

struct {
  char run_file[48];
  int run_file_pos = -1;
  bool run_file_mutex = 0;
  bool download_busy;
} UfsData;

/*********************************************************************************************/

// Init flash file system
void UfsInitOnce(void) {
  ufs_type = 0;
  ffsp = 0;
  ufs_dir = 0;

#ifdef ESP8266
  ffsp = &LittleFS;
  if (!LittleFS.begin()) {
    ffsp = nullptr;
    return;
  }
#endif  // ESP8266

#ifdef ESP32
  // try lfs first
  ffsp = &LittleFS;
 if (!LittleFS.begin(true, "") && !LittleFS.begin(true, "", 5, "fs_1")) {         // force empty mount point to make it the fallback FS
    // ffat is second
    ffsp = &FFat;
   if (!FFat.begin(true, "")) {
      ffsp = nullptr;
      return;
    }
    ffs_type = UFS_TFAT;
    ufs_type = ffs_type;
    ufsp = ffsp;
    dfsp = ffsp;
    return;
  }
#endif // ESP32
  ffs_type = UFS_TLFS;
  ufs_type = ffs_type;
  ufsp = ffsp;
  dfsp = ffsp;
}

// Called from tasmota.ino at restart. This inits flash file only
void UfsInit(void) {
  UfsData.run_file_pos = -1;
  UfsInitOnce();
  if (ufs_type) {
    AddLog(LOG_LEVEL_INFO, PSTR("UFS: FlashFS mounted with %d kB free"), UfsInfo(1, 0));
  }
}

// simple put a zero at last '/'
// modifies input string
char *folderOnly(char *fname){
  for (int i = strlen(fname)-1; i >= 0; i--){
    if (fname[i] == '/'){
      fname[i] = 0;
      break;
    }
    fname[i] = 0;
  }
  if (!fname[0]){
    fname[0] = '/';
    fname[1] = 0;
  }
  return fname;
}

// returns everything after last '/' of whiole input if no '/'
char *fileOnly(char *fname){
  char *cp = fname;
  for (uint32_t cnt = strlen(fname); cnt >= 0; cnt--) {
    if (fname[cnt] == '/') {
      cp = &fname[cnt + 1];
      break;
    }
  }
  return cp;
}


#ifdef USE_SDCARD
void UfsCheckSDCardInit(void) {
  // Try SPI mode first
  // SPI mode requires SDCARD_CS to be configured
  if (TasmotaGlobal.spi_enabled && PinUsed(GPIO_SDCARD_CS)) {
    int8_t cs = Pin(GPIO_SDCARD_CS);

#ifdef ESP8266
    SPI.begin();
#endif // ESP8266
#ifdef ESP32
    SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1);
#endif // ESP32

    if (SD.begin(cs)) {
#ifdef ESP8266
      ufsp = &SDFS;
#endif  // ESP8266
#ifdef ESP32
      ufsp = &SD;
#endif  // ESP32

      ufs_type = UFS_TSDC;
      dfsp = ufsp;
      if (ffsp) {ufs_dir = 1;}
      // make sd card the global filesystem
#ifdef ESP8266
      // on esp8266 sdcard info takes several seconds !!!, so we ommit it here
      AddLog(LOG_LEVEL_INFO, PSTR("UFS: SDCard mounted"));
#endif // ESP8266
#ifdef ESP32
      AddLog(LOG_LEVEL_INFO, PSTR("UFS: SDCard mounted (SPI mode) with %d kB free"), UfsInfo(1, 0));
#endif // ESP32
    }
  }
#if defined(ESP32) && defined(SOC_SDMMC_HOST_SUPPORTED)     // ESP32 and SDMMC supported (not Esp32C3)
  // check if SDIO is configured
  else if (PinUsed(GPIO_SDIO_CLK) && PinUsed(GPIO_SDIO_CMD) && PinUsed(GPIO_SDIO_D0)) {
    int32_t sdio_cmd = Pin(GPIO_SDIO_CMD);
    int32_t sdio_clk = Pin(GPIO_SDIO_CLK);
    int32_t sdio_d0 = Pin(GPIO_SDIO_D0);
    int32_t sdio_d1 = Pin(GPIO_SDIO_D1);
    int32_t sdio_d2 = Pin(GPIO_SDIO_D2);
    int32_t sdio_d3 = Pin(GPIO_SDIO_D3);
    bool bit_4_mode = (sdio_d1 >= 0) && (sdio_d2 >= 0) && (sdio_d3 >= 0);   // enable 4-bit mode if possible
    if (bit_4_mode) {
      // AddLog(LOG_LEVEL_DEBUG, "UFS: trying SDIO 4-bit clk=%i cmd=%i d0=%i d1=%i d2=%i d3=%i", sdio_clk, sdio_cmd, sdio_d0, sdio_d1, sdio_d2, sdio_d3);
      SD_MMC.setPins(sdio_clk, sdio_cmd, sdio_d0, sdio_d1, sdio_d2, sdio_d3);
    } else {
      // AddLog(LOG_LEVEL_DEBUG, "UFS: trying SDIO 1-bit clk=%i cmd=%i d0=%i", sdio_clk, sdio_cmd, sdio_d0);
      SD_MMC.setPins(sdio_clk, sdio_cmd, sdio_d0);
    }
    if (SD_MMC.begin("/sd", !bit_4_mode /*mode 1 bit*/, false /*format_if_failed*/)) {    // mount under "/sd" to be consistent with SD SPI
      ufsp = &SD_MMC;

      ufs_type = UFS_TSDC;
      dfsp = ufsp;
      if (ffsp) {ufs_dir = 1;}
      // make sd card the global filesystem
      AddLog(LOG_LEVEL_INFO, PSTR("UFS: SDCard mounted (SDIO %i-bit) with %d kB free"), bit_4_mode ? 4 : 1, UfsInfo(1, 0));
    }
  }
#endif
}
#endif // USE_SDCARD

uint32_t UfsInfo(uint32_t sel, uint32_t type) {
  uint64_t result = 0;
  FS *ifsp = ufsp;
  uint8_t itype = ufs_type;
  if (type) {
    ifsp = ffsp;
    itype = ffs_type;
  }

#ifdef ESP8266
  FSInfo64 fsinfo;
#endif  // ESP8266

  switch (itype) {
    case UFS_TSDC:
#ifdef USE_SDCARD
#ifdef ESP8266
      ifsp->info64(fsinfo);
      if (sel == 0) {
        result = fsinfo.totalBytes;
      } else {
        result = (fsinfo.totalBytes - fsinfo.usedBytes);
      }
#endif  // ESP8266
#ifdef ESP32
      if (sel == 0) {
        result = SD.totalBytes();
      } else {
        result = (SD.totalBytes() - SD.usedBytes());
      }
#endif  // ESP32
#endif  // USE_SDCARD
      break;

    case UFS_TLFS:
#ifdef ESP8266
      ifsp->info64(fsinfo);
      if (sel == 0) {
        result = fsinfo.totalBytes;
      } else {
        result = (fsinfo.totalBytes - fsinfo.usedBytes);
      }
#endif  // ESP8266
#ifdef ESP32
      if (sel == 0) {
        result = LittleFS.totalBytes();
      } else {
        result = LittleFS.totalBytes() - LittleFS.usedBytes();
      }
#endif  // ESP32
      break;

    case UFS_TFAT:
#ifdef ESP32
      if (sel == 0) {
        result = FFat.totalBytes();
      } else {
        result = FFat.freeBytes();
      }
#endif  // ESP32
      break;

  }
  return result / 1024;
}

uint32_t UfsSize(void) {
  return UfsInfo(0, ufs_dir == 2 ? 1:0);
}

uint32_t UfsFree(void) {
  return UfsInfo(1, ufs_dir == 2 ? 1:0);
}

#if USE_LONG_FILE_NAMES>0
#undef REJCMPL
#define REJCMPL 6
#else
#undef REJCMPL
#define REJCMPL 8
#endif

uint8_t UfsReject(char *name) {
  char *lcp = strrchr(name,'/');
  if (lcp) {
    name = lcp + 1;
  }

  while (*name=='/') { name++; }
  if (*name=='.') { return 1; }

  if (!strncasecmp(name, "SPOTLI~1", REJCMPL)) { return 1; }
  if (!strncasecmp(name, "TRASHE~1", REJCMPL)) { return 1; }
  if (!strncasecmp(name, "FSEVEN~1", REJCMPL)) { return 1; }
  if (!strncasecmp(name, "SYSTEM~1", REJCMPL)) { return 1; }
  if (!strncasecmp(name, "System Volume", 13)) { return 1; }
  return 0;
}

/*********************************************************************************************\
 * Tfs low level functions
\*********************************************************************************************/

bool TfsFileExists(const char *fname){
  if (!ffs_type) { return false; }

  bool yes = ffsp->exists(fname);
  if (!yes) {
    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("TFS: File '%s' not found"), fname +1);  // Skip leading slash
  }
  return yes;
}

size_t TfsFileSize(const char *fname){
  if (!ffs_type) { return 0; }

  File file = ffsp->open(fname, "r");
  if (!file) { return 0; }
  size_t flen = file.size();
  file.close();
  return flen;
}

bool TfsSaveFile(const char *fname, const uint8_t *buf, uint32_t len) {
  if (!ffs_type) { return false; }
#ifdef USE_WEBCAM
  WcInterrupt(0);  // Stop stream if active to fix TG1WDT_SYS_RESET
#endif
  bool result = false;
  File file = ffsp->open(fname, "w");
  if (!file) {
    AddLog(LOG_LEVEL_INFO, PSTR("TFS: Save failed"));
  } else {
    // This will timeout on ESP32-webcam
    // But now solved with WcInterrupt(0) in support_esp.ino
    file.write(buf, len);
  /*
    // This will still timeout on ESP32-webcam when wcresolution 10
    uint32_t count = len / 512;
    uint32_t chunk = len / count;
    for (uint32_t i = 0; i < count; i++) {
      file.write(buf + (i * chunk), chunk);
      // do actually wait a little to allow ESP32 tasks to tick
      // fixes task timeout in ESP32Solo1 style unicore code and webcam.
      delay(10);
      OsWatchLoop();
    }
    uint32_t left = len % count;
    if (left) {
      file.write(buf + (count * chunk), left);
    }
  */
    file.close();
    result = true;
  }
#ifdef USE_WEBCAM
  WcInterrupt(1);
#endif
  return result;
}

bool TfsInitFile(const char *fname, uint32_t len, uint8_t init_value) {
  if (!ffs_type) { return false; }

  File file = ffsp->open(fname, "w");
  if (!file) {
    AddLog(LOG_LEVEL_INFO, PSTR("TFS: Erase failed"));
    return false;
  }

  for (uint32_t i = 0; i < len; i++) {
    file.write(&init_value, 1);
  }
  file.close();
  return true;
}

bool TfsLoadFile(const char *fname, uint8_t *buf, uint32_t len) {
  if (!ffs_type) { return false; }

  File file = ffsp->open(fname, "r");
  if (!file) {
    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("TFS: File '%s' not found"), fname +1);  // Skip leading slash
    return false;
  }

  size_t flen = file.size();
  if (len > flen) { len = flen; }           // Adjust requested length to smaller file length
  file.read(buf, len);
  file.close();
  return true;
}

String TfsLoadString(const char *fname) {
  // Use a reasonable amount of stack space considering 4k/8k available on ESP8266/ESP32 and manageable string length
  char buf[2048] = { 0 };                   // Prepare empty string of max 2047 characters on stack
  TfsLoadFile(fname, (uint8_t*)buf, 2047);  // Leave last position as end of string ('\0')
  return String(buf);                       // Received string or empty on error
}

bool TfsDeleteFile(const char *fname) {
  if (!ffs_type) { return false; }

  if (!ffsp->remove(fname)) {
    AddLog(LOG_LEVEL_INFO, PSTR("TFS: Delete failed"));
    return false;
  }
  return true;
}

bool TfsRenameFile(const char *fname1, const char *fname2) {
  if (!ffs_type) { return false; }

  if (!ffsp->rename(fname1, fname2)) {
    AddLog(LOG_LEVEL_INFO, PSTR("TFS: Rename failed"));
    return false;
  }
  return true;
}

/*********************************************************************************************\
 * File command execute support
\*********************************************************************************************/

bool UfsExecuteCommandFileReady(void) {
  return (UfsData.run_file_pos < 0);   // Check file ready to disable concurrency
}

void UfsExecuteCommandFileLoop(void) {
  if (UfsExecuteCommandFileReady() || !ffs_type) { return; }

  if (BACKLOG_EMPTY && strlen(UfsData.run_file) && !UfsData.run_file_mutex) {
    File file = ffsp->open(UfsData.run_file, "r");
    if (!file || !file.seek(UfsData.run_file_pos)) {
      UfsData.run_file_pos = -1;       // Signal file ready
      return;
    }

    UfsData.run_file_mutex = true;

    char cmd_line[512];
    cmd_line[0] = '\0';                // Clear in case of re-entry
    while (file.available()) {
      uint16_t index = 0;
      bool comment = false;
      while (file.available()) {
        uint8_t buf[1];
        file.read(buf, 1);
        if ((buf[0] == '\n') || (buf[0] == '\r')) {
          break;                       // End of command with linefeed or carriage return
        }
//        else if (index && !comment && (buf[0] == ';')) {
//          break;                       // End of command on multi command line
//        }
        else if ((0 == index) && isspace(buf[0])) {
                                       // Skip leading spaces (' ','\t','\n','\v','\f','\r')
        }
        else if ((0 == index) && (buf[0] == ';')) {
          comment = true;              // Ignore comment lines until linefeed or carriage return
        }
        else if (!comment && (index < sizeof(cmd_line) - 2)) {
          cmd_line[index++] = buf[0];  // Build command
        }
      }
      if ((index > 0) && (index < sizeof(cmd_line) - 1)) {
        cmd_line[index] = '\0';        // Valid command received
        break;
      }
    }
    UfsData.run_file_pos = (file.available()) ? file.position() : -1;
    file.close();
    if (strlen(cmd_line)) {
      ExecuteCommand(cmd_line, SRC_FILE);
    }

    UfsData.run_file_mutex = false;
  }
}

bool UfsExecuteCommandFile(const char *fname) {
  // Check for non-concurrency and file existance
  if (UfsExecuteCommandFileReady() && TfsFileExists(fname)) {
    snprintf(UfsData.run_file, sizeof(UfsData.run_file), fname);
    UfsData.run_file_pos = 0;          // Signal start of file
    return true;
  }
  return false;
}

/*********************************************************************************************\
 * File JSON settings support using file /.drvset000
 * 
 * {"UserSet1":{"Param1":123,"Param2":"Text1"},"UserSet2":{"Param1":123,"Param2":"Text2"},"UserSet3":{"Param1":123,"Param2":"Text3"}}
\*********************************************************************************************/

bool _UfsJsonSettingsUpdate(const char* data) {
  // Delete: Input UserSet2
  // Append: Input {"UserSet2":{"Param1":123,"Param2":"Text2"}}

  char filename[14];
  snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), 0);  // /.drvset000
  if (!TfsFileExists(filename)) { return false; }  // Error - File not found

  char bfname[14];
  strcpy_P(bfname, PSTR("/settmp"));
  File ofile = ffsp->open(bfname, "w");
  if (!ofile) { return false; }        // Error - unable to open temporary file
  File ifile = ffsp->open(filename, "r");
  if (!ifile) { 
    ofile.close();
    ffsp->remove(bfname);
    return false;                      // Error - unable to open settings file
  }

  bool append = false;
  char* key = (char*)data;
  char key_pos[32];                    // Max key length
  char *p = strchr(data, '"');
  if (p) {
    append = true;
    char *q = strchr(++p, '"');
    if (!q) { return false; }          // Error - No valid key provided in data to append
    uint32_t len = (uint32_t)q - (uint32_t)p;
    memcpy(key_pos, p, len);
    key_pos[len] = '\0';               // key = UserSet2
    key = key_pos;
  }

  char buffer[32];                     // Max key length
  uint8_t buf[1];
  uint32_t index = 0;
  uint32_t bracket_count = 0;
  int entries = 0;
  bool quote = false;
  bool mine = false;
  bool deleted = false;
  while (ifile.available()) {          // Process file
    ifile.read(buf, 1);
    if (bracket_count > 1) {           // Copy or skip old data
      if (!mine) {
        ofile.write(buf, 1);           // Copy data
      }
      if (buf[0] == '}') {
        bracket_count--;
      }
      else if (buf[0] == '{') {        // Next bracket
        bracket_count++;
      }
    } else {
      if (buf[0] == '}') {             // Last bracket
        break;                         // End of file
      }
      else if (buf[0] == '{') {
        bracket_count++;
        if (bracket_count > 1) {       // Skip first bracket
          entries++;
        }
      }
      else if (buf[0] == '"') {
        quote ^= 1;
        if (quote) {
          index = 0;
        } else {
          buffer[index] = '\0';        // End of key name
          mine = (!strcasecmp(buffer, key));
          if (mine) {
            entries--;                 // Skip old data
            deleted = true;
          } else {
            ofile.write((entries) ? (uint8_t*)",\"" : (uint8_t*)"{\"", 2);
            ofile.write((uint8_t*)buffer, strlen(buffer));
            ofile.write((uint8_t*)"\":{", 3);
          }
        }
      }
      else {
        buffer[index++] = buf[0];      // Add key name
        if (index > sizeof(buffer) -2) {
          break;                       // Key name too long
        }
      }
    }
  }
  ifile.close();
  if (append) {
    // Append new data
    ofile.write((entries) ? (uint8_t*)"," : (uint8_t*)"{", 1);
    ofile.write((uint8_t*)data +1, strlen(data) -1);
  } else {
    // Delete data
    if (entries) {
      ofile.write((uint8_t*)"}", 1);
    }
  }
  ofile.close();

  if (index > sizeof(buffer) -2) { 
    // No changes
    ffsp->remove(bfname);
    return false;                      // Error - Key name too long
  }
  ffsp->remove(filename);
  ffsp->rename(bfname, filename);
  if (!append) {
    // Delete data
    if (!entries) {
      ffsp->remove(filename);
    }
    return deleted;                    // State - 0 = Not found, 1 = Deleted
  }
  return true;                         // State - Append success
}

bool UfsJsonSettingsDelete(const char* key) {
  // Delete: Input UserSet2
  //         Output 0 = Not found, 1 = Deleted
  return _UfsJsonSettingsUpdate(key);  // State - 0 = Not found, 1 = Deleted
}

bool UfsJsonSettingsWrite(const char* data) {
  // Add new UserSet replacing present UserSet
  // Input {"UserSet2":{"Param1":123,"Param2":"Text2"}}
  // Output 0 = Error, 1 = Append success

  String json = data;
  JsonParser parser((char*)json.c_str());
  JsonParserObject root = parser.getRootObject();
  if (!root) { return false; }         // Error - invalid JSON

  char filename[14];
  snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), 0);  // /.drvset000
  if (!TfsFileExists(filename)) {
    return TfsSaveFile(filename, (uint8_t*)data, strlen(data));
  }
  return _UfsJsonSettingsUpdate(data); // State - 0 = Error, 1 = Append success
}

String UfsJsonSettingsRead(const char* key) {
  // Read: Input UserSet2
  //       Output "" = Error, {"Param1":123,"Param2":"Text2","Param3":[{"Param3a":1},{"Param3b":1}]} = Data
  String data = "";
  char filename[14];
  snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), 0);      // /.drvset000
  if (!TfsFileExists(filename)) { return data; }  // Error - File not found
  File file = ffsp->open(filename, "r");
  if (!file) { return data; }          // Error - unable to open settings file

  Trim((char*)key);
  char buffer[128];
  uint8_t buf[1] = { 0 };
  uint32_t index = 0;
  uint32_t bracket_count = 0;
  bool quote = false;
  bool mine = false;
  while (file.available()) {           // Process file
    file.read(buf, 1);
    if (bracket_count > 1) {           // Build JSON
      if (mine) {
        buffer[index++] = buf[0];      // Add key data
        if (index > sizeof(buffer) -2) {
          buffer[index] = '\0'; 
          data += buffer;              // Add buffer to result
          index = 0;
        }
      }
      if (buf[0] == '}') {
        bracket_count--;
        if (1 == bracket_count) {
          if (mine) {
            break;                     // End of key data
          } else {
            index = 0;                 // End of data which is not mine
          }
        }
      }
      else if (buf[0] == '{') {        // Next bracket
        bracket_count++;
      }
    } else {
      if (buf[0] == '}') {             // Last bracket
        index = 0;
        break;                         // End of file - key not found
      }
      else if (buf[0] == '{') {
        bracket_count++;
        if (bracket_count > 1) {       // Skip first bracket
          index = 0;
          buffer[index++] = buf[0];    // Start of key data
        }
      }
      else if (buf[0] == '"') {
        quote ^= 1;
        if (quote) {
          index = 0;
        } else {
          buffer[index] = '\0';        // End of key name
          mine = (!strcasecmp(buffer, key));
        }
      }
      else {
        buffer[index++] = buf[0];      // Add key name
        if (index > sizeof(buffer) -2) {
          index = 0;
          break;                       // Key name too long
        }
      }
    }
  }
  file.close();
  buffer[index] = '\0';
  data += buffer;
  return data;                         // State - "" = Error, {"Param1":123,"Param2":"Text2"} = Data
}

/*********************************************************************************************\
 * Commands
\*********************************************************************************************/

const int UFS_FILENAME_SIZE = 48;

char* UfsFilename(char* fname, char* fname_in) {
  fname_in = Trim(fname_in);  // Remove possible leading spaces
  snprintf_P(fname, UFS_FILENAME_SIZE, PSTR("%s%s"), ('/' == fname_in[0]) ? "" : "/", fname_in);
  return fname;
}

const char kUFSCommands[] PROGMEM = "Ufs|"  // Prefix
  "|Type|Size|Free|Delete|Rename|Run"
#ifdef UFILESYS_STATIC_SERVING
  "|Serve"
#endif
#ifdef USE_FTP
  "|FTP"
#endif
  ;

void (* const kUFSCommand[])(void) PROGMEM = {
  &UFSInfo, &UFSType, &UFSSize, &UFSFree, &UFSDelete, &UFSRename, &UFSRun
#ifdef UFILESYS_STATIC_SERVING
  ,&UFSServe
#endif
#ifdef USE_FTP
  ,&Switch_FTP
#endif
  };

void UFSInfo(void) {
  Response_P(PSTR("{\"Ufs\":{\"Type\":%d,\"Size\":%d,\"Free\":%d}"), ufs_type, UfsInfo(0, 0), UfsInfo(1, 0));
  if (ffs_type && (ffs_type != ufs_type)) {
    ResponseAppend_P(PSTR(",{\"Type\":%d,\"Size\":%d,\"Free\":%d}"), ffs_type, UfsInfo(0, 1), UfsInfo(1, 1));
  }
  ResponseJsonEnd();
}

void UFSType(void) {
  if (ffs_type && (ffs_type != ufs_type)) {
    Response_P(PSTR("{\"%s\":[%d,%d]}"), XdrvMailbox.command, ufs_type, ffs_type);
  } else {
    ResponseCmndNumber(ufs_type);
  }
}

void UFSSize(void) {
  if (ffs_type && (ffs_type != ufs_type)) {
    Response_P(PSTR("{\"%s\":[%d,%d]}"), XdrvMailbox.command, UfsInfo(0, 0), UfsInfo(0, 1));
  } else {
    ResponseCmndNumber(UfsInfo(0, 0));
  }
}

void UFSFree(void) {
  if (ffs_type && (ffs_type != ufs_type)) {
    Response_P(PSTR("{\"%s\":[%d,%d]}"), XdrvMailbox.command, UfsInfo(1, 0), UfsInfo(1, 1));
  } else {
    ResponseCmndNumber(UfsInfo(1, 0));
  }
}

void UFSDelete(void) {
  // UfsDelete  sdcard or flashfs file if only one of them available
  // UfsDelete2 flashfs file if available
  if (XdrvMailbox.data_len > 0) {
    char fname[UFS_FILENAME_SIZE];
    UfsFilename(fname, XdrvMailbox.data);
    bool result = false;
    if (ffs_type && (ffs_type != ufs_type) && (2 == XdrvMailbox.index)) {
      result = TfsDeleteFile(fname);
    } else {
      result = (ufs_type && ufsp->remove(fname));
    }
    if (!result) {
      ResponseCmndFailed();
    } else {
      ResponseCmndDone();
    }
  }
}

void UFSRename(void) {
  // UfsRename  sdcard or flashfs file if only one of them available
  // UfsRename2 flashfs file if available
  if (XdrvMailbox.data_len > 0) {
    bool result = false;
    char *fname1 = strtok(XdrvMailbox.data, ",");
    char *fname2 = strtok(nullptr, ",");
    if (fname1 && fname2) {
      char fname_old[UFS_FILENAME_SIZE];
      UfsFilename(fname_old, fname1);
      char fname_new[UFS_FILENAME_SIZE];
      UfsFilename(fname_new, fname2);
      if (ffs_type && (ffs_type != ufs_type) && (2 == XdrvMailbox.index)) {
        result = TfsRenameFile(fname_old, fname_new);
      } else {
        result = (ufs_type && ufsp->rename(fname_old, fname_new));
      }
    }
    if (!result) {
      ResponseCmndFailed();
    } else {
      ResponseCmndDone();
    }
  }
}

#ifdef UFILESYS_STATIC_SERVING
/*
* Serves a filesystem folder at a web url.
* NOTE - this is expensive on flash -> +2.5kbytes.
* like "UFSServe <fs path>,<url>[,<noauth>]"
* e.g. "UFSServe /sd/,/mysdcard/,1" - will serve the /sd/ fs folder as https://<ip>/mysdcard/ with no auth required
* e.g. "UFSServe /www/,/" - will serve the /www/ fs folder as https://<ip>/ with auth required if TAS has a password setup
* <noauth> defaults to 0 - i.e. the default is to require auth if configured
* it WILL serve on / - so conflicting urls could occur.  I beleive native TAS urls will have priority.
* you can serve multiple folders, and they can each be auth or noauth
*
* by default, it also enables cors on the webserver - this allows you to have 
* a website external to TAS which can access the files, else the browser refuses.
*/
#include "detail/RequestHandlersImpl.h"

//#define SERVING_DEBUG

// class to allow us to request auth when required.
// StaticRequestHandler is in the above header
class StaticRequestHandlerAuth : public StaticRequestHandler {
public:
    StaticRequestHandlerAuth(FS& fs, const char* path, const char* uri, const char* cache_header, bool requireAuth):
      StaticRequestHandler(fs, path, uri, cache_header)
    {
      _requireAuth = requireAuth;
    }

    bool _requireAuth;

    // we replace the handle method, 
    // and look for authentication only if we would serve the file.
    // if we check earlier, then we will reject as unauth even though a later
    // handler may be public, and so fail to serve public files.
    bool handle(WebServer& server, HTTPMethod requestMethod, String requestUri) override {
        if (!canHandle(requestMethod, requestUri))
            return false;

        //log_v("StaticRequestHandler::handle: request=%s _uri=%s\r\n", requestUri.c_str(), _uri.c_str());
#ifdef SERVING_DEBUG
        AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handle: request=%s _uri=%s"), requestUri.c_str(), _uri.c_str());
#endif
        String path(_path);

        if (!_isFile) {
            // Base URI doesn't point to a file.
            // If a directory is requested, look for index file.
            if (requestUri.endsWith("/")) 
              requestUri += "index.htm";

            // Append whatever follows this URI in request to get the file path.
            path += requestUri.substring(_baseUriLength);
        }
#ifdef SERVING_DEBUG
        AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handle: path=%s, isFile=%d"), path.c_str(), _isFile);
#endif
        String contentType = getContentType(path);

        // look for gz file, only if the original specified path is not a gz.  So part only works to send gzip via content encoding when a non compressed is asked for
        // if you point the the path to gzip you will serve the gzip as content type "application/x-gzip", not text or javascript etc...
        if (!path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !_fs.exists(path))  {
            String pathWithGz = path + FPSTR(mimeTable[gz].endsWith);
            if(_fs.exists(pathWithGz))
                path += FPSTR(mimeTable[gz].endsWith);
        }

        File f = _fs.open(path, "r");
        if (!f || !f.available()){
            AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handler missing file?"));
            return false;
        }
#ifdef SERVING_DEBUG
        AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handler file open %d"), f.available());
#endif
        if (_requireAuth && !WebAuthenticate()) {
#ifdef SERVING_DEBUG
          AddLog(LOG_LEVEL_ERROR, PSTR("UFS: serv of %s denied"), requestUri.c_str());
#endif          
          server.requestAuthentication();
          return true;
        }

        if (_cache_header.length() != 0)
            server.sendHeader("Cache-Control", _cache_header);

#ifdef SERVING_DEBUG
        AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handler sending"));
#endif
        uint8_t buff[512];
        uint32_t bread;
        uint32_t flen = f.available();
        WiFiClient download_Client = server.client();
        server.setContentLength(flen);   
        server.send(200, contentType, "");

        // transfer is about 150kb/s
        uint32_t cnt = 0;
        while (f.available()) {
          bread = f.read(buff, sizeof(buff));
          cnt += bread;
#ifdef SERVING_DEBUG
          //AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handler sending %d/%d"), cnt, flen);
#endif          
          uint32_t bw = download_Client.write((const char*)buff, bread);
          if (!bw) { break; }
          yield();
        }
#ifdef SERVING_DEBUG
        AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handler sent %d/%d"), cnt, flen);
#endif

        if (cnt != flen){
          AddLog(LOG_LEVEL_ERROR, PSTR("UFS: ::handler incomplete file send: sent %d/%d"), cnt, flen);
        }

        // It does seem that on lesser ESP32, this causes a problem?  A lockup...
        //server.streamFile(f, contentType);

        f.close();
        download_Client.stop();

#ifdef SERVING_DEBUG
        AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ::handler done"));
#endif        
        return true;
    }
};

void UFSServe(void) {
  bool result = false;
  if (XdrvMailbox.data_len > 0) {
    char *fpath = strtok(XdrvMailbox.data, ",");
    char *url = strtok(nullptr, ",");
    char *noauth = strtok(nullptr, ",");
    if (fpath && url) {
      if (Webserver) { // fail if no Webserver yet.
        StaticRequestHandlerAuth *staticHandler;
        if (noauth && *noauth == '1'){
          staticHandler = new StaticRequestHandlerAuth(*ffsp, fpath, url, (char *)nullptr, false);
        } else {
          staticHandler = new StaticRequestHandlerAuth(*ffsp, fpath, url, (char *)nullptr, true);
        }
        if (staticHandler) {
          //Webserver->serveStatic(url, *ffsp, fpath);
          Webserver->addHandler(staticHandler);
          Webserver->enableCORS(true);
          result = true;
        }
      }
    }
  }
  if (!result) {
    ResponseCmndFailed();
  } else {
    ResponseCmndDone();
  }
}
#endif // UFILESYS_STATIC_SERVING

void UFSRun(void) {
  if (XdrvMailbox.data_len > 0) {
    char fname[UFS_FILENAME_SIZE];
    if (UfsExecuteCommandFile(UfsFilename(fname, XdrvMailbox.data))) {
      ResponseClear();
    } else {
      ResponseCmndFailed();
    }
  } else {
    bool not_active = UfsExecuteCommandFileReady();
    UfsData.run_file_pos = -1;
    ResponseCmndChar(not_active ? PSTR(D_JSON_DONE) : PSTR(D_JSON_ABORTED));
  }
}



/*********************************************************************************************\
 * Web support
\*********************************************************************************************/

#ifdef USE_WEBSERVER

const char UFS_WEB_DIR[] PROGMEM =
  "<p><form action='ufsd' method='get'><input type='hidden' name='download' value='%s' /> <button>%s</button></form></p>";

const char UFS_CURRDIR[] PROGMEM = 
  "<p>%s: %s</p>";

#ifndef D_CURR_DIR
  #define D_CURR_DIR "Folder"
#endif

const char UFS_FORM_FILE_UPLOAD[] PROGMEM =
  "<div id='f1' name='f1' style='display:block;'>"
  "<fieldset><legend><b>&nbsp;" D_MANAGE_FILE_SYSTEM "&nbsp;</b></legend>";
const char UFS_FORM_FILE_UPGc[] PROGMEM =
  "<div style='text-align:left;color:#%06x;'>" D_FS_SIZE " %s MB - " D_FS_FREE " %s MB";

const char UFS_FORM_FILE_UPGc1[] PROGMEM =
    " &nbsp;&nbsp;<a href='/ufsd?dir=%d'>%s</a>";

const char UFS_FORM_FILE_UPGc2[] PROGMEM =
  "</div>";

const char UFS_FORM_FILE_UPG[] PROGMEM =
  "<form method='post' action='ufsu?fsz=' enctype='multipart/form-data'>"
  "<br><input type='file' name='ufsu'><br>"
  "<br><button type='submit' "
  "onclick='eb(\"f1\").style.display=\"none\";eb(\"but6\").style.display=\"none\";eb(\"f2\").style.display=\"block\";this.form.action+=this.form[\"ufsu\"].files[0].size;this.form.submit();'"
  ">" D_UPLOAD "</button></form>"
  "<br><hr>";
const char UFS_FORM_SDC_DIRa[] PROGMEM =
  "<div style='text-align:left;overflow:auto;height:250px;'>";
const char UFS_FORM_SDC_DIRc[] PROGMEM =
  "</div>";
const char UFS_FORM_FILE_UPGb[] PROGMEM =
  "<form method='get' action='ufse'><input type='hidden' name='file' value='%s/" D_NEW_FILE "'>"
  "<button type='submit'>" D_CREATE_NEW_FILE "</button></form>";
const char UFS_FORM_FILE_UPGb1[] PROGMEM =
  "<input type='checkbox' id='shf' onclick='sf(eb(\"shf\").checked);' name='shf'>" D_SHOW_HIDDEN_FILES "</input>";

const char UFS_FORM_FILE_UPGb2[] PROGMEM =
  "</fieldset>"
  "</div>"
  "<div id='f2' name='f2' style='display:none;text-align:center;'><b>" D_UPLOAD_STARTED " ...</b></div>";
const char UFS_FORM_SDC_DIR_NORMAL[] PROGMEM =
  "";
const char UFS_FORM_SDC_DIR_HIDDABLE[] PROGMEM =
  " class='hf'";
const char UFS_FORM_SDC_DIRd[] PROGMEM =
  "<pre><a href='%s' file='%s'>%s</a></pre>";
const char UFS_FORM_SDC_DIRb[] PROGMEM =
  "<pre%s><a href='%s' file='%s'>%s</a> %19s %8d %s %s</pre>";
const char UFS_FORM_SDC_HREF[] PROGMEM =
  "ufsd?download=%s/%s";

#ifdef GUI_TRASH_FILE
const char UFS_FORM_SDC_HREFdel[] PROGMEM =
  //"<a href=ufsd?delete=%s/%s>&#128465;</a>"; // 🗑️
  "<a href='ufsd?delete=%s/%s&download=%s' onclick=\"return confirm('" D_CONFIRM_FILE_DEL "')\">&#128293;</a>"; // 🔥
#endif // GUI_TRASH_FILE

#ifdef GUI_EDIT_FILE

#define FILE_BUFFER_SIZE  1024

const char UFS_FORM_SDC_HREFedit[] PROGMEM =
  "<a href='ufse?file=%s/%s'>&#x1F4DD;</a>"; // 📝

const char HTTP_EDITOR_FORM_START[] PROGMEM =
  "<fieldset><legend><b>&nbsp;" D_EDIT_FILE "&nbsp;</b></legend>"
  "<form>"
  "<label for='name'>" D_FILE ":</label><input type='text' id='name' name='name' value='%s'><br><hr width='98%%'>"
  "<textarea id='content' name='content' wrap='off' rows='8' cols='80' style='font-size: 12pt'>";

const char HTTP_EDITOR_FORM_END[] PROGMEM =
  "</textarea>"
  "<button name='save' type='submit' formmethod='post' formenctype='multipart/form-data' formaction='/ufse' class='button bgrn'>" D_SAVE "</button>"
  "</form></fieldset>";

#endif  // #ifdef GUI_EDIT_FILE

void HandleUploadUFSDone(void) {
  if (!HttpCheckPriviledgedAccess()) { return; }

  HTTPUpload& upload = Webserver->upload();

  AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_UPLOAD_DONE));

  WifiConfigCounter();
  UploadServices(1);

  WSContentStart_P(PSTR(D_INFORMATION));

  WSContentSendStyle();
  WSContentSend_P(PSTR("<div style='text-align:center;'><b>" D_UPLOAD " <font color='#"));
  if (Web.upload_error) {
    WSContentSend_P(PSTR("%06x'>" D_FAILED "</font></b><br><br>"), WebColor(COL_TEXT_WARNING));
    char error[100];
    if (Web.upload_error < 10) {
      GetTextIndexed(error, sizeof(error), Web.upload_error -1, kUploadErrors);
    } else {
      snprintf_P(error, sizeof(error), PSTR(D_UPLOAD_ERROR_CODE " %d"), Web.upload_error);
    }
    WSContentSend_P(error);
    Web.upload_error = 0;
  } else {
    WSContentSend_P(PSTR("%06x'>" D_SUCCESSFUL "</font></b><br>"), WebColor(COL_TEXT_SUCCESS));
  }
  WSContentSend_P(PSTR("</div><br>"));

  XdrvCall(FUNC_WEB_ADD_MANAGEMENT_BUTTON);

  WSContentStop();
}

void UfsDirectory(void) {
  if (!HttpCheckPriviledgedAccess()) { return; }

  AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_MANAGE_FILE_SYSTEM));

  uint8_t depth = 0;
  uint8_t isdir = 0;

  strcpy(ufs_path, "/");

  if (Webserver->hasArg(F("dir"))) {
    String stmp = Webserver->arg(F("dir"));
    ufs_dir = atoi(stmp.c_str());
  }

  if (ufs_dir == 1) {
    dfsp = ufsp;
  } else {
    if (ffsp) {
      dfsp = ffsp;
    }
  }

  if (Webserver->hasArg(F("delete"))) {
    String stmp = Webserver->arg(F("delete"));
    char *cp = (char*)stmp.c_str();
    File download_file = dfsp->open(cp, UFS_FILE_READ);
    if (download_file) {
      if (download_file.isDirectory()) {
        download_file.close();
        dfsp->rmdir(cp);
      } else {
        download_file.close();
        dfsp->remove(cp);
      }
    }
  }

  if (Webserver->hasArg(F("download"))) {
    String stmp = Webserver->arg(F("download"));
    char *cp = (char*)stmp.c_str();
    if (UfsDownloadFile(cp)) {
      // is directory
      strcpy(ufs_path, cp);
      isdir = 1;
    } else {
      return;
    }
  }

  WSContentStart_P(PSTR(D_MANAGE_FILE_SYSTEM));
  WSContentSendStyle();
  WSContentSend_P(UFS_FORM_FILE_UPLOAD);

  char ts[FLOATSZ];
  dtostrfd((float)UfsInfo(0, ufs_dir == 2 ? 1:0) / 1000, 3, ts);
  char fs[FLOATSZ];
  dtostrfd((float)UfsInfo(1, ufs_dir == 2 ? 1:0) / 1000, 3, fs);
  WSContentSend_PD(UFS_FORM_FILE_UPGc, WebColor(COL_TEXT), ts, fs);

  if (ufs_dir) {
    WSContentSend_P(UFS_FORM_FILE_UPGc1, (ufs_dir == 1)?2:1, (ufs_dir == 1)?PSTR("SDCard"):PSTR("FlashFS"));
  }
  WSContentSend_P(UFS_FORM_FILE_UPGc2);

  WSContentSend_P(UFS_FORM_FILE_UPG);

  if (isdir){
    // if a folder, show 'folder: xxx' if not '/'
    if (ufs_path[0] != '/' || strlen(ufs_path) != 1){
      WSContentSend_P(UFS_CURRDIR, PSTR(D_CURR_DIR), ufs_path);
    }
  }

  WSContentSend_P(UFS_FORM_SDC_DIRa);
  if (ufs_type) {
    UfsListDir(ufs_path, depth);
  }
  WSContentSend_P(UFS_FORM_SDC_DIRc);
#ifdef GUI_EDIT_FILE
  WSContentSend_P(UFS_FORM_FILE_UPGb, ufs_path);
#endif
  if (!isSDC()) {
    WSContentSend_P(UFS_FORM_FILE_UPGb1);
  }
  WSContentSend_P(UFS_FORM_FILE_UPGb2);

  WSContentSpaceButton(BUTTON_MANAGEMENT);
  WSContentStop();

  Web.upload_file_type = UPL_UFSFILE;
}

// return true if SDC
bool isSDC(void) {
#ifndef SDC_HIDE_INVISIBLES
  return false;
#else
  if (((uint32_t)ufsp != (uint32_t)ffsp) && ((uint32_t)ffsp == (uint32_t)dfsp)) return false;
  if (((uint32_t)ufsp == (uint32_t)ffsp) && (ufs_type != UFS_TSDC)) return false;
  return true;
#endif
}

void UfsListDir(char *path, uint8_t depth) {
  char name[48];
  char npath[128];
  char format[12];
  sprintf(format, PSTR("%%-%ds"), 24 - depth);

  File dir = dfsp->open(path, UFS_FILE_READ);
  if (dir) {
    dir.rewindDirectory();
    if (strlen(path)>1) {
      ext_snprintf_P(npath, sizeof(npath), PSTR("ufsd?download=%s"), path);
      for (uint32_t cnt = strlen(npath) - 1; cnt > 0; cnt--) {
        if (npath[cnt] == '/') {
          if (npath[cnt - 1] == '=') {
            npath[cnt + 1] = 0;
          } else {
            npath[cnt] = 0;
          }
          break;
        }
      }
      WSContentSend_P(UFS_FORM_SDC_DIRd, npath, path, PSTR(".."));
    }
    char *ep;
    while (true) {
      WiFiClient client = Webserver->client();
      // abort if the client disconnected
      // if there is a huge folder, then this gives a way out by refresh of browser
      if (!client.connected()){
        break;
      }

      File entry = dir.openNextFile();
      if (!entry) {
        break;
      }
      // esp32 returns path here, shorten to filename
      ep = (char*)entry.name();
      if (*ep == '/') { ep++; }
      char *lcp = strrchr(ep,'/');
      if (lcp) {
        ep = lcp + 1;
      }

      uint32_t tm = entry.getLastWrite();
      String tstr = GetDT(tm);

      char *pp = path;
      if (!*(pp + 1)) { pp++; }
      char *cp = name;
      // osx formatted disks contain a lot of stuff we dont want
      bool hiddable = UfsReject((char*)ep);

      if (!hiddable || !isSDC() ) {

        for (uint8_t cnt = 0; cnt<depth; cnt++) {
          *cp++ = '-';
        }

        String pp_escaped_string = UrlEscape(pp);
        String ep_escaped_string = UrlEscape(ep);
        const char* ppe = pp_escaped_string.c_str();    // this can't be merged on a single line otherwise the String object can be freed
        const char* epe = ep_escaped_string.c_str();
        sprintf(cp, format, ep);
#ifdef GUI_TRASH_FILE
        char delpath[128+UFS_FILENAME_SIZE];
        ext_snprintf_P(delpath, sizeof(delpath), UFS_FORM_SDC_HREFdel, ppe, epe, ppe[0]?ppe:"/");
#else
        char delpath[2] = " ";
#endif // GUI_TRASH_FILE
        if (entry.isDirectory()) {
          ext_snprintf_P(npath, sizeof(npath), UFS_FORM_SDC_HREF, ppe, epe);

          WSContentSend_P(UFS_FORM_SDC_DIRb, hiddable ? UFS_FORM_SDC_DIR_HIDDABLE : UFS_FORM_SDC_DIR_NORMAL, npath, epe,
                          HtmlEscape(name).c_str(), "", 0, delpath, " ");
          //WSContentSend_P(UFS_FORM_SDC_DIRd, npath, ep, name);
#ifdef UFILESYS_RECURSEFOLDERS_GUI
          uint8_t plen = strlen(path);
          if (plen > 1) {
            strcat(path, "/");
          }
          strcat(path, ep);
          UfsListDir(path, depth + 4);
          path[plen] = 0;
#endif          
        } else {
  #ifdef GUI_EDIT_FILE
          char editpath[128];
          ext_snprintf_P(editpath, sizeof(editpath), UFS_FORM_SDC_HREFedit, ppe, epe);
  #else
          char editpath[2];
          editpath[0]=0;
  #endif // GUI_TRASH_FILE
          ext_snprintf_P(npath, sizeof(npath), UFS_FORM_SDC_HREF, ppe, epe);
          WSContentSend_P(UFS_FORM_SDC_DIRb, hiddable ? UFS_FORM_SDC_DIR_HIDDABLE : UFS_FORM_SDC_DIR_NORMAL, npath, epe,
                          HtmlEscape(name).c_str(), tstr.c_str(), entry.size(), delpath, editpath);
        }
        entry.close();

        yield(); // trigger watchdog reset
      }
    }
    dir.close();
  }
}

#ifdef ESP32
// this actually does not work reliably, as the 
// webserver can close the connection before the download task completes
//#define ESP32_DOWNLOAD_TASK
#endif // ESP32

uint8_t UfsDownloadFile(char *file) {
  File download_file;

  AddLog(LOG_LEVEL_INFO, PSTR("UFS: File '%s' download"), file);

  if (!dfsp->exists(file)) {
    AddLog(LOG_LEVEL_INFO, PSTR("UFS: File '%s' not found"), file);
    return 0;
  }

  download_file = dfsp->open(file, UFS_FILE_READ);
  if (!download_file) {
    AddLog(LOG_LEVEL_INFO, PSTR("UFS: Could not open file '%s'"), file);
    return 0;
  }

  if (download_file.isDirectory()) {
    AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: File '%s' to download is directory"), file);
    download_file.close();
    return 1;
  }

#ifndef ESP32_DOWNLOAD_TASK
  WiFiClient download_Client;
  uint32_t flen = download_file.size();

  download_Client = Webserver->client();
  Webserver->setContentLength(flen);

  char attachment[100];
  char *cp = fileOnly(file);
  snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=%s"), cp);
  Webserver->sendHeader(F("Content-Disposition"), attachment);
  WSSend(200, CT_APP_STREAM, "");

  uint8_t buff[512];
  uint32_t bread;
  // transfer is about 150kb/s
  uint32_t cnt = 0;
  while (download_file.available()) {
    bread = download_file.read(buff, sizeof(buff));
    uint32_t bw = download_Client.write((const char*)buff, bread);
    if (!bw) { break; }
    cnt++;
    if (cnt > 7) {
      cnt = 0;
      //if (glob_script_mem.script_loglevel & 0x80) {
        // this indeed multitasks, but is slower 50 kB/s
      //  loop();
      //}
    }
    delay(0);
    OsWatchLoop();
  }
  download_file.close();
  download_Client.stop();
#endif // ESP32_DOWNLOAD_TASK



// to make this work I thing you wouold need to duplicate the client 
// BEFORE starting the task, so that the webserver does not close it's
// copy of the client.
#ifdef ESP32_DOWNLOAD_TASK
  download_file.close();

  if (UfsData.download_busy == true) {
    AddLog(LOG_LEVEL_INFO, PSTR("UFS: Download is busy"));
    return 0;
  }

  UfsData.download_busy = true;
  char *path = (char*)malloc(128);
  strcpy(path,file);
  BaseType_t ret = xTaskCreatePinnedToCore(download_task, "DT", 6000, (void*)path, 3, nullptr, 1);
  if (ret != pdPASS)
    AddLog(LOG_LEVEL_INFO, PSTR("UFS: Download task failed with %d"), ret);
  yield();
#endif // ESP32_DOWNLOAD_TASK

  return 0;
}


#ifdef ESP32_DOWNLOAD_TASK
#ifndef DOWNLOAD_SIZE
#define DOWNLOAD_SIZE 4096
#endif // DOWNLOAD_SIZE
void download_task(void *path) {
  File download_file;
  WiFiClient download_Client;
  char *file = (char*) path;

  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ESP32 File '%s' to download"), file);

  download_file = dfsp->open(file, UFS_FILE_READ);
  uint32_t flen = download_file.size();

  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: len %d to download"), flen);

  download_Client = Webserver->client();
  Webserver->setContentLength(flen);

  char attachment[100];
  char *cp = fileOnly(file);
  //snprintf_P(attachment, sizeof(attachment), PSTR("download file '%s' as '%s'"), file, cp);
  //Webserver->sendHeader(F("X-Tasmota-Debug"), attachment);
  snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=%s"), cp);
  Webserver->sendHeader(F("Content-Disposition"), attachment);
  WSSend(200, CT_APP_STREAM, "");

  uint8_t *buff = (uint8_t*)malloc(DOWNLOAD_SIZE);
  if (buff) {
    uint32_t bread;
    while (download_file.available()) {
      bread = download_file.read(buff, DOWNLOAD_SIZE);
      uint32_t bw = download_Client.write((const char*)buff, bread);
      if (!bw) { break; }
    }
    free(buff);
  }
  download_file.close();
  download_Client.stop();
  UfsData.download_busy = false;
  vTaskDelete( NULL );
  free(path);
  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: esp32 sent file"));
}
#endif //  ESP32_DOWNLOAD_TASK


bool UfsUploadFileOpen(const char* upload_filename) {
  char npath[48];
  snprintf_P(npath, sizeof(npath), PSTR("%s/%s"), ufs_path, upload_filename);
  dfsp->remove(npath);
  ufs_upload_file = dfsp->open(npath, UFS_FILE_WRITE);
  return (ufs_upload_file);
}

bool UfsUploadFileWrite(uint8_t *upload_buf, size_t current_size) {
  if (ufs_upload_file) {
    ufs_upload_file.write(upload_buf, current_size);
  } else {
    return false;
  }
  return true;
}

void UfsUploadFileClose(void) {
  ufs_upload_file.close();
}

//******************************************************************************************
//  File Editor
//******************************************************************************************

#ifdef GUI_EDIT_FILE

void UfsEditor(void) {
  if (!HttpCheckPriviledgedAccess()) { return; }

  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor GET"));

  char fname_input[UFS_FILENAME_SIZE];
  if (Webserver->hasArg(F("file"))) {
    WebGetArg(PSTR("file"), fname_input, sizeof(fname_input));
  } else {
    snprintf_P(fname_input, sizeof(fname_input), PSTR(D_NEW_FILE));
  }
  char fname[UFS_FILENAME_SIZE];
  UfsFilename(fname, fname_input);                  // Trim spaces and add slash

  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file=%s, ffs_type=%d, TfsFileExist=%d"), fname, ffs_type, dfsp->exists(fname));

  WSContentStart_P(PSTR(D_EDIT_FILE));
  WSContentSendStyle();
  char *bfname = fname +1;
  WSContentSend_P(HTTP_EDITOR_FORM_START, bfname);  // Skip leading slash

  if (ffs_type && dfsp->exists(fname)) {
    File fp = dfsp->open(fname, "r");
    if (!fp) {
      AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file open failed"));
      WSContentSend_P(D_NEW_FILE);
    } else {
      uint8_t *buf = (uint8_t*)malloc(FILE_BUFFER_SIZE+1);
      size_t filelen = fp.size();
      AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file len=%d"), filelen);
      while (filelen > 0) {
        size_t l = fp.read(buf, FILE_BUFFER_SIZE);
        AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("UFS: UfsEditor: read=%d"), l);
        if (l < 0) { break; }
        buf[l] = '\0';
        WSContentSend_P(PSTR("%s"), buf);
        filelen -= l;
      }
      fp.close();
      free(buf);
      AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: read done"));
    }
  } else {
    WSContentSend_P(D_NEW_FILE);
  }

  WSContentSend_P(HTTP_EDITOR_FORM_END);
  folderOnly(fname);
  WSContentSend_P(UFS_WEB_DIR, fname, PSTR(D_MANAGE_FILE_SYSTEM));
  WSContentStop();
}

void UfsEditorUpload(void) {
  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file upload"));

  if (!HttpCheckPriviledgedAccess()) { return; }

  if (!Webserver->hasArg("name")) {
    AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: file upload - no filename"));
    WSSend(400, CT_PLAIN, F("400: Bad request - no filename"));
    return;
  }

  char fname_input[UFS_FILENAME_SIZE];
  WebGetArg(PSTR("name"), fname_input, sizeof(fname_input));
  char fname[UFS_FILENAME_SIZE];
  UfsFilename(fname, fname_input);                  // Trim spaces and add slash
  AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file '%s'"), fname);

  if (!Webserver->hasArg("content")) {
    AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: file upload - no content"));
    WSSend(400, CT_PLAIN, F("400: Bad request - no content"));
    return;
  }
  String content = Webserver->arg("content");

  if (!dfsp) {
    Web.upload_error = 1;
    AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: 507: no storage available"));
    WSSend(507, CT_PLAIN, F("507: no storage available"));
    return;
  }

  // recursively create folder(s)
  char tmp[UFS_FILENAME_SIZE];
  char folder[UFS_FILENAME_SIZE] = "";
  strcpy(tmp, fname);
  // zap file name off the end
  folderOnly(tmp);
  char *tf = strtok(tmp, "/");
  while(tf){
    if (*tf){
      strcat(folder, "/");
      strcat(folder, tf);
    }
    // we don;t care if it fails - it may already exist.
    dfsp->mkdir(folder);
    tf = strtok(nullptr, "/");
  }

  File fp = dfsp->open(fname, "w");
  if (!fp) {
    Web.upload_error = 1;
    AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: 400: invalid file name '%s'"), fname);
    WSSend(400, CT_PLAIN, F("400: bad request - invalid filename"));
    return;
  }

  if (*content.c_str()) {
    content.replace("\r\n", "\n");
    content.replace("\r", "\n");
  }

  if (!fp.print(content)) {
    AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: write error on '%s'"), fname);
  }

  fp.close();

  // zap file name off the end
  folderOnly(fname);
  char t[20+UFS_FILENAME_SIZE] = "/ufsu?download=";
  strcat(t, fname);
  Webserver->sendHeader(F("Location"), t);
  Webserver->send(303);
}

#endif  // GUI_EDIT_FILE

#endif  // USE_WEBSERVER


#ifdef USE_FTP
#include <ESPFtpServer.h>
FtpServer *ftpSrv;

void FTP_Server(uint32_t mode) {
  if (mode > 0) {
    if (ftpSrv) {
      delete ftpSrv;
    }
    ftpSrv = new FtpServer;
    if (mode == 1) {
      ftpSrv->begin(USER_FTP,PW_FTP, ufsp);
    } else {
      ftpSrv->begin(USER_FTP,PW_FTP, ffsp);
    }
    AddLog(LOG_LEVEL_INFO, PSTR("UFS: FTP Server started in mode: '%d'"), mode);
  } else {
    if (ftpSrv) {
      delete ftpSrv;
      ftpSrv = nullptr;
    }
  }
}

void Switch_FTP(void) {
  if (XdrvMailbox.data_len > 0) {
    if (XdrvMailbox.payload >= 0 && XdrvMailbox.payload <= 2) {
      FTP_Server(XdrvMailbox.payload);
      Settings->mbflag2.FTP_Mode = XdrvMailbox.payload;
    }
  }
  ResponseCmndNumber(Settings->mbflag2.FTP_Mode);
}
#endif // USE_FTP

/*********************************************************************************************\
 * Interface
\*********************************************************************************************/


bool Xdrv50(uint32_t function) {
  bool result = false;

  switch (function) {
    case FUNC_LOOP:
      UfsExecuteCommandFileLoop();

#ifdef USE_FTP
      if (ftpSrv) {
        ftpSrv->handleFTP();
      }
#endif

      break;
    
    case FUNC_NETWORK_UP:
#ifdef USE_FTP
      if (Settings->mbflag2.FTP_Mode && !ftpSrv) {
        FTP_Server(Settings->mbflag2.FTP_Mode);
      }
#endif
      break;

/*
// Moved to support_tasmota.ino for earlier init to be used by scripter
#ifdef USE_SDCARD
    case FUNC_PRE_INIT:
      UfsCheckSDCardInit();
      break;
#endif // USE_SDCARD
*/
    case FUNC_MQTT_INIT:
      if (!TasmotaGlobal.no_autoexec) {
        UfsExecuteCommandFile(TASM_FILE_AUTOEXEC);
      }
      break;
    case FUNC_COMMAND:
      result = DecodeCommand(kUFSCommands, kUFSCommand);
      break;
#ifdef USE_WEBSERVER
    case FUNC_WEB_ADD_MANAGEMENT_BUTTON:
      if (ufs_type) {
        if (XdrvMailbox.index) {
          XdrvMailbox.index++;
        } else {
          WSContentSend_PD(UFS_WEB_DIR, "/", PSTR(D_MANAGE_FILE_SYSTEM));
        }
      }
      break;
    case FUNC_WEB_ADD_HANDLER:
//      Webserver->on(F("/ufsd"), UfsDirectory);
//      Webserver->on(F("/ufsu"), HTTP_GET, UfsDirectory);
//      Webserver->on(F("/ufsu"), HTTP_POST,[](){Webserver->sendHeader(F("Location"),F("/ufsu"));Webserver->send(303);}, HandleUploadLoop);
      Webserver->on("/ufsd", UfsDirectory);
      Webserver->on("/ufsu", HTTP_GET, UfsDirectory);
      //Webserver->on("/ufsu", HTTP_POST,[](){Webserver->sendHeader(F("Location"),F("/ufsu"));Webserver->send(303);}, HandleUploadLoop);
      Webserver->on("/ufsu", HTTP_POST, HandleUploadUFSDone, HandleUploadLoop);
#ifdef GUI_EDIT_FILE
      Webserver->on("/ufse", HTTP_GET, UfsEditor);
      Webserver->on("/ufse", HTTP_POST, UfsEditorUpload);
#endif
      break;
#endif // USE_WEBSERVER
    case FUNC_ACTIVE:
      result = true;
      break;
  }
  return result;
}
#endif  // USE_UFILESYS
