/*
 * This file is part of the MicroPython ESP32 project, https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 LoBo (https://github.com/loboris)
 *
 * 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.
 */
/*
 * This file is based on 'ftp' from Pycom Limited.
 *
 * Author: LoBo, loboris@gmail.com
 * Copyright (c) 2017, LoBo
 */

#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

#include "dirent.h"
#include "esp_system.h"
//#include "esp_spi_flash.h" // ESP-IDF V4
#include "esp_flash.h" // ESP-IDF V5
#include "nvs_flash.h"
#include "esp_log.h"

#include "esp_wifi.h"

#include "lwip/sockets.h"
#include "lwip/dns.h"
#include "lwip/netdb.h"

//#include "freertos/semphr.h"

#include "ftp.h"

extern int FTP_TASK_FINISH_BIT;
extern EventGroupHandle_t xEventTask;

int ftp_buff_size = CONFIG_MICROPY_FTPSERVER_BUFFER_SIZE;
int ftp_timeout = FTP_CMD_TIMEOUT_MS;
const char *FTP_TAG = "[Ftp]";
const char *MOUNT_POINT = "/root";

static uint8_t ftp_stop = 0;
char ftp_user[FTP_USER_PASS_LEN_MAX + 1] = "anonymous";
char ftp_pass[FTP_USER_PASS_LEN_MAX + 1] = "";

/******************************************************************************
 DECLARE PRIVATE DATA
 ******************************************************************************/
static ftp_data_t ftp_data = {0};
static char *ftp_path = NULL;
static char *ftp_scratch_buffer = NULL;;
static char *ftp_cmd_buffer = NULL;
static uint8_t ftp_nlist = 0;
static const ftp_cmd_t ftp_cmd_table[] = { { "FEAT" }, { "SYST" }, { "CDUP" }, { "CWD"	},
										   { "PWD"	}, { "XPWD" }, { "SIZE" }, { "MDTM" },
										   { "TYPE" }, { "USER" }, { "PASS" }, { "PASV" },
										   { "LIST" }, { "RETR" }, { "STOR" }, { "DELE" },
										   { "RMD"	}, { "MKD"	}, { "RNFR" }, { "RNTO" },
										   { "NOOP" }, { "QUIT" }, { "APPE" }, { "NLST" }, { "AUTH" } };

// ==== PRIVATE FUNCTIONS ===================================================

uint64_t mp_hal_ticks_ms() {
	uint64_t time_ms = xTaskGetTickCount() * portTICK_PERIOD_MS;
	return time_ms;
}

//--------------------------------
static void stoupper (char *str) {
	while (str && *str != '\0') {
		*str = (char)toupper((int)(*str));
		str++;
	}
}

// ==== File functions =========================================

//--------------------------------------------------------------
static bool ftp_open_file (const char *path, const char *mode) {
	ESP_LOGI(FTP_TAG, "ftp_open_file: path=[%s]", path);
	char fullname[128];
	strcpy(fullname, MOUNT_POINT);
	strcat(fullname, path);
	ESP_LOGI(FTP_TAG, "ftp_open_file: fullname=[%s]", fullname);
	//ftp_data.fp = fopen(path, mode);
	ftp_data.fp = fopen(fullname, mode);
	if (ftp_data.fp == NULL) {
		ESP_LOGE(FTP_TAG, "ftp_open_file: open fail [%s]", fullname);
		return false;
	}
	ftp_data.e_open = E_FTP_FILE_OPEN;
	return true;
}

//--------------------------------------
static void ftp_close_files_dir (void) {
	if (ftp_data.e_open == E_FTP_FILE_OPEN) {
		fclose(ftp_data.fp);
		ftp_data.fp = NULL;
	}
	else if (ftp_data.e_open == E_FTP_DIR_OPEN) {
		closedir(ftp_data.dp);
		ftp_data.dp = NULL;
	}
	ftp_data.e_open = E_FTP_NOTHING_OPEN;
}

//------------------------------------------------
static void ftp_close_filesystem_on_error (void) {
	ftp_close_files_dir();
	if (ftp_data.fp) {
		fclose(ftp_data.fp);
		ftp_data.fp = NULL;
	}
	if (ftp_data.dp) {
		closedir(ftp_data.dp);
		ftp_data.dp = NULL;
	}
}

//---------------------------------------------------------------------------------------------
static ftp_result_t ftp_read_file (char *filebuf, uint32_t desiredsize, uint32_t *actualsize) {
	ftp_result_t result = E_FTP_RESULT_CONTINUE;
	*actualsize = fread(filebuf, 1, desiredsize, ftp_data.fp);
	if (*actualsize == 0) {
		if (feof(ftp_data.fp))
			result = E_FTP_RESULT_OK;
		else
			result = E_FTP_RESULT_FAILED;
		ftp_close_files_dir();
	} else if (*actualsize < desiredsize) {
		ftp_close_files_dir();
		result = E_FTP_RESULT_OK;
	}
	return result;
}

//-----------------------------------------------------------------
static ftp_result_t ftp_write_file (char *filebuf, uint32_t size) {
	ftp_result_t result = E_FTP_RESULT_FAILED;
	uint32_t actualsize = fwrite(filebuf, 1, size, ftp_data.fp);
	if (actualsize == size) {
		result = E_FTP_RESULT_OK;
	} else {
		ftp_close_files_dir();
	}
	return result;
}

//---------------------------------------------------------------
static ftp_result_t ftp_open_dir_for_listing (const char *path) {
	if (ftp_data.dp) {
		closedir(ftp_data.dp);
		ftp_data.dp = NULL;
	}
	ESP_LOGI(FTP_TAG, "ftp_open_dir_for_listing path=[%s] MOUNT_POINT=[%s]", path, MOUNT_POINT);
	char fullname[128];
	strcpy(fullname, MOUNT_POINT);
	strcat(fullname, path);
	ESP_LOGI(FTP_TAG, "ftp_open_dir_for_listing: %s", fullname);
	ftp_data.dp = opendir(fullname);  // Open the directory
	if (ftp_data.dp == NULL) {
		return E_FTP_RESULT_FAILED;
	}
	ftp_data.e_open = E_FTP_DIR_OPEN;
	ftp_data.listroot = false;
	return E_FTP_RESULT_CONTINUE;
}

//---------------------------------------------------------------------------------
static int ftp_get_eplf_item (char *dest, uint32_t destsize, struct dirent *de) {

	char *type = (de->d_type & DT_DIR) ? "d" : "-";

	// Get full file path needed for stat function
	char fullname[128];
	strcpy(fullname, MOUNT_POINT);
	strcat(fullname, ftp_path);
	//strcpy(fullname, ftp_path);
	if (fullname[strlen(fullname)-1] != '/') strcat(fullname, "/");
	strcat(fullname, de->d_name);

	struct stat buf;
	int res = stat(fullname, &buf);
	ESP_LOGI(FTP_TAG, "ftp_get_eplf_item res=%d buf.st_size=%ld", res, buf.st_size);
	if (res < 0) {
		buf.st_size = 0;
		buf.st_mtime = 946684800; // Jan 1, 2000
	}

	char str_time[64];
	struct tm *tm_info;
	time_t now;
	if (time(&now) < 0) now = 946684800;	// get the current time from the RTC
	tm_info = localtime(&buf.st_mtime);		// get broken-down file time

	// if file is older than 180 days show dat,month,year else show month, day and time
	if ((buf.st_mtime + FTP_UNIX_SECONDS_180_DAYS) < now) strftime(str_time, 127, "%b %d %Y", tm_info);
	else strftime(str_time, 63, "%b %d %H:%M", tm_info);

	int addsize = destsize + 64;

	while (addsize >= destsize) {
		if (ftp_nlist) addsize = snprintf(dest, destsize, "%s\r\n", de->d_name);
		else addsize = snprintf(dest, destsize, "%srw-rw-rw-   1 root  root %9"PRIu32" %s %s\r\n", type, (uint32_t)buf.st_size, str_time, de->d_name);
		if (addsize >= destsize) {
			ESP_LOGW(FTP_TAG, "Buffer too small, reallocating [%d > %"PRIi32"]", ftp_buff_size, ftp_buff_size + (addsize - destsize) + 64);
			char *new_dest = realloc(dest, ftp_buff_size + (addsize - destsize) + 65);
			if (new_dest) {
				ftp_buff_size += (addsize - destsize) + 64;
				destsize += (addsize - destsize) + 64;
				dest = new_dest;
				addsize = destsize + 64;
			}
			else {
				ESP_LOGE(FTP_TAG, "Buffer reallocation ERROR");
				addsize = 0;
			}
		}
	}
	return addsize;
}

#if 0
//---------------------------------------------------------------------------
static int ftp_get_eplf_drive (char *dest, uint32_t destsize, char *name) {
	char *type = "d";
	struct tm *tm_info;
	time_t seconds;
	time(&seconds); // get the time from the RTC
	tm_info = gmtime(&seconds);
	char str_time[64];
	strftime(str_time, 63, "%b %d %Y", tm_info);

	return snprintf(dest, destsize, "%srw-rw-rw-   1 root  root %9u %s %s\r\n", type, 0, str_time, name);
}
#endif

//--------------------------------------------------------------------------------------
static ftp_result_t ftp_list_dir(char *list, uint32_t maxlistsize, uint32_t *listsize) {
	uint next = 0;
	uint listcount = 0;
	ftp_result_t result = E_FTP_RESULT_CONTINUE;
	struct dirent *de;

	// read up to 8 directory items
	while (((maxlistsize - next) > 64) && (listcount < 8)) {
		de = readdir(ftp_data.dp);															// Read a directory item
		ESP_LOGI(FTP_TAG, "readdir de=%p", de);
		if (de == NULL) {
			result = E_FTP_RESULT_OK;
			break;																			// Break on error or end of dp
		}
		if (de->d_name[0] == '.' && de->d_name[1] == 0) continue;							// Ignore . entry
		if (de->d_name[0] == '.' && de->d_name[1] == '.' && de->d_name[2] == 0) continue;	// Ignore .. entry

		// add the entry to the list
		ESP_LOGI(FTP_TAG, "Add to dir list: %s", de->d_name);
		next += ftp_get_eplf_item((list + next), (maxlistsize - next), de);
		listcount++;
	}
	if (result == E_FTP_RESULT_OK) {
		ftp_close_files_dir();
	}
	*listsize = next;
	return result;
}

// ==== Socket functions ==============================================================

//------------------------------------
static void ftp_close_cmd_data(void) {
	closesocket(ftp_data.c_sd);
	closesocket(ftp_data.d_sd);
	ftp_data.c_sd  = -1;
	ftp_data.d_sd  = -1;
	ftp_close_filesystem_on_error ();
}

//----------------------------
static void _ftp_reset(void) {
	// close all connections and start all over again
	ESP_LOGW(FTP_TAG, "FTP RESET");
	closesocket(ftp_data.lc_sd);
	closesocket(ftp_data.ld_sd);
	ftp_data.lc_sd = -1;
	ftp_data.ld_sd = -1;
	ftp_close_cmd_data();

	ftp_data.e_open = E_FTP_NOTHING_OPEN;
	ftp_data.state = E_FTP_STE_START;
	ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;
}

//-------------------------------------------------------------------------------------
static bool ftp_create_listening_socket (int32_t *sd, uint32_t port, uint8_t backlog) {
	struct sockaddr_in sServerAddress;
	int32_t _sd;
	int32_t result;

	// open a socket for ftp data listen
	*sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
	_sd = *sd;

	if (_sd > 0) {
		// enable non-blocking mode
		uint32_t option = fcntl(_sd, F_GETFL, 0);
		option |= O_NONBLOCK;
		fcntl(_sd, F_SETFL, option);

		// enable address reusing
		option = 1;
		result = setsockopt(_sd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));

		// bind the socket to a port number
		sServerAddress.sin_family = AF_INET;
		sServerAddress.sin_addr.s_addr = INADDR_ANY;
		sServerAddress.sin_len = sizeof(sServerAddress);
		sServerAddress.sin_port = htons(port);

		result |= bind(_sd, (const struct sockaddr *)&sServerAddress, sizeof(sServerAddress));

		// start listening
		result |= listen (_sd, backlog);

		if (!result) {
			return true;
		}
		closesocket(*sd);
	}
	return false;
}

//--------------------------------------------------------------------------------------------
static ftp_result_t ftp_wait_for_connection (int32_t l_sd, int32_t *n_sd, uint32_t *ip_addr) {
	struct sockaddr_in	sClientAddress;
	socklen_t  in_addrSize;

	// accepts a connection from a TCP client, if there is any, otherwise returns EAGAIN
	*n_sd = accept(l_sd, (struct sockaddr *)&sClientAddress, (socklen_t *)&in_addrSize);
	int32_t _sd = *n_sd;
	if (_sd < 0) {
		if (errno == EAGAIN) {
			return E_FTP_RESULT_CONTINUE;
		}
		// error
		_ftp_reset();
		return E_FTP_RESULT_FAILED;
	}

	if (ip_addr) {
		// check on which network interface the client was connected and save the IP address
		struct sockaddr_in clientAddr, serverAddr;
		in_addrSize = sizeof(struct sockaddr_in);
		getpeername(_sd, (struct sockaddr *)&clientAddr, (socklen_t *)&in_addrSize);
		getsockname(_sd, (struct sockaddr *)&serverAddr, (socklen_t *)&in_addrSize);
		ESP_LOGI(FTP_TAG, "Client IP: 0x%08"PRIx32, clientAddr.sin_addr.s_addr);
		ESP_LOGI(FTP_TAG, "Server IP: 0x%08"PRIx32, serverAddr.sin_addr.s_addr);
		*ip_addr = serverAddr.sin_addr.s_addr;
	}

	// enable non-blocking mode if not data channel connection
	uint32_t option = fcntl(_sd, F_GETFL, 0);
	if (l_sd != ftp_data.ld_sd) option |= O_NONBLOCK;
	fcntl(_sd, F_SETFL, option);

	// client connected, so go on
	return E_FTP_RESULT_OK;
}

//-----------------------------------------------------------
static void ftp_send_reply (uint32_t status, char *message) {
	if (!message) {
		message = "";
	}
	snprintf((char *)ftp_cmd_buffer, 4, "%"PRIu32, status);
	strcat ((char *)ftp_cmd_buffer, " ");
	strcat ((char *)ftp_cmd_buffer, message);
	strcat ((char *)ftp_cmd_buffer, "\r\n");

	int32_t timeout = 200;
	ftp_result_t result;
	//uint32_t size = strlen((char *)ftp_cmd_buffer);
	size_t size = strlen((char *)ftp_cmd_buffer);

	ESP_LOGI(FTP_TAG, "Send reply: [%.*s]", size-2, ftp_cmd_buffer);
	vTaskDelay(1);

	while (1) {
		result = send(ftp_data.c_sd, ftp_cmd_buffer, size, 0);
		if (result == size) {
			if (status == 221) {
				closesocket(ftp_data.d_sd);
				ftp_data.d_sd = -1;
				closesocket(ftp_data.ld_sd);
				ftp_data.ld_sd = -1;
				closesocket(ftp_data.c_sd);
				ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;
				ftp_close_filesystem_on_error();
			}
			else if (status == 426 || status == 451 || status == 550) {
				closesocket(ftp_data.d_sd);
				ftp_data.d_sd = -1;
				ftp_close_filesystem_on_error();
			}
			vTaskDelay(1);
			ESP_LOGI(FTP_TAG, "Send reply: OK (%u)", size);
			break;
		}
		else {
			vTaskDelay(1);
			if ((timeout <= 0) || (errno != EAGAIN)) {
				// error
				_ftp_reset();
				ESP_LOGW(FTP_TAG, "Error sending command reply.");
				break;
			}
		}
		timeout -= portTICK_PERIOD_MS;
	}
}

//------------------------------------------
static void ftp_send_list(uint32_t datasize)
{
	int32_t timeout = 200;
	ftp_result_t result;

	ESP_LOGI(FTP_TAG, "Send list data: (%"PRIu32")", datasize);
	vTaskDelay(1);

	while (1) {
		result = send(ftp_data.d_sd, ftp_data.dBuffer, datasize, 0);
		if (result == datasize) {
			vTaskDelay(1);
			ESP_LOGI(FTP_TAG, "Send OK");
			break;
		}
		else {
			vTaskDelay(1);
			if ((timeout <= 0) || (errno != EAGAIN)) {
				// error
				_ftp_reset();
				ESP_LOGW(FTP_TAG, "Error sending list data.");
				break;
			}
		}
		timeout -= portTICK_PERIOD_MS;
	}
}

//-----------------------------------------------
static void ftp_send_file_data(uint32_t datasize)
{
	ftp_result_t result;
	uint32_t timeout = 200;

	ESP_LOGI(FTP_TAG, "Send file data: (%"PRIu32")", datasize);
	vTaskDelay(1);

	while (1) {
		result = send(ftp_data.d_sd, ftp_data.dBuffer, datasize, 0);
		if (result == datasize) {
			vTaskDelay(1);
			ESP_LOGI(FTP_TAG, "Send OK");
			break;
		}
		else {
			vTaskDelay(1);
			if ((timeout <= 0) || (errno != EAGAIN)) {
				// error
				_ftp_reset();
				ESP_LOGW(FTP_TAG, "Error sending file data.");
				break;
			}
		}
		timeout -= portTICK_PERIOD_MS;
	}
}

//------------------------------------------------------------------------------------------------
static ftp_result_t ftp_recv_non_blocking (int32_t sd, void *buff, int32_t Maxlen, int32_t *rxLen)
{
	if (sd < 0) return E_FTP_RESULT_FAILED;

	*rxLen = recv(sd, buff, Maxlen, 0);
	if (*rxLen > 0) return E_FTP_RESULT_OK;
	else if (errno != EAGAIN) return E_FTP_RESULT_FAILED;

	return E_FTP_RESULT_CONTINUE;
}

// ==== Directory functions =======================

//-----------------------------------
#if 0
static void ftp_fix_path(char *pwd) {
	ESP_LOGI(FTP_TAG, "ftp_fix_path: pwd=[%s]", pwd);
	char ph_path[128];
	uint len = strlen(pwd);

	if (len == 0) {
		strcpy (pwd, "/");
	}
	else if ((len > 1) && (pwd[len-1] == '/')) pwd[len-1] = '\0';

	// Convert to physical path
	if (strstr(pwd, VFS_NATIVE_INTERNAL_MP) == pwd) {
		ESP_LOGI(FTP_TAG, "ftp_fix_path: VFS_NATIVE_INTERNAL_MP=[%s]", VFS_NATIVE_INTERNAL_MP);
		sprintf(ph_path, "%s%s", VFS_NATIVE_MOUNT_POINT, pwd+strlen(VFS_NATIVE_INTERNAL_MP));
		if (strcmp(ph_path, VFS_NATIVE_MOUNT_POINT) == 0) strcat(ph_path, "/");
		ESP_LOGI(FTP_TAG, "ftp_fix_path: ph_path=[%s]", ph_path);
		strcpy(pwd, ph_path);
	}
	else if (strstr(pwd, VFS_NATIVE_EXTERNAL_MP) == pwd) {
		ESP_LOGI(FTP_TAG, "ftp_fix_path: VFS_NATIVE_EXTERNAL_MP=[%s]", VFS_NATIVE_EXTERNAL_MP);
		sprintf(ph_path, "%s%s", VFS_NATIVE_SDCARD_MOUNT_POINT, pwd+strlen(VFS_NATIVE_EXTERNAL_MP));
		if (strcmp(ph_path, VFS_NATIVE_SDCARD_MOUNT_POINT) == 0) strcat(ph_path, "/");
		ESP_LOGI(FTP_TAG, "ftp_fix_path: ph_path=[%s]", ph_path);
		strcpy(pwd, ph_path);
	}
}
#endif


/*
 * Add directory or file name to the current path
 * Initially, pwd is set to "/" (file system root)
 * There are two possible entries in root:
 *	 "flash"	which can be fatfs or spiffs
 *	 "sd"		sd card
 * flash and sd entries have to be translated to their VFS names
 * trailing '/' is required for flash&sd root entries (translated)
 */
//-------------------------------------------------
static void ftp_open_child (char *pwd, char *dir) {
	ESP_LOGI(FTP_TAG, "open_child: [%s] + [%s]", pwd, dir);
	if (strlen(dir) > 0) {
		if (dir[0] == '/') {
			// ** absolute path
			strcpy(pwd, dir);
		}
		else {
			// ** relative path
			// add trailing '/' if needed
			if ((strlen(pwd) > 1) && (pwd[strlen(pwd)-1] != '/') && (dir[0] != '/')) strcat(pwd, "/");
			// append directory/file name
			strcat(pwd, dir);
		}
#if 0
		ftp_fix_path(pwd);
#endif
	}
	ESP_LOGI(FTP_TAG, "open_child, New pwd: %s", pwd);
}

// Return to parent directory
//---------------------------------------
static void ftp_close_child (char *pwd) {
	ESP_LOGI(FTP_TAG, "close_child: %s", pwd);
	uint len = strlen(pwd);
	if (pwd[len-1] == '/') {
		pwd[len-1] = '\0';
		len--;
		if ((len == 0) || (strcmp(pwd, VFS_NATIVE_MOUNT_POINT) == 0) || (strcmp(pwd, VFS_NATIVE_SDCARD_MOUNT_POINT) == 0)) {
			strcpy(pwd, "/");
		}
	}
	else {
		while (len) {
			if (pwd[len-1] == '/') {
				pwd[len-1] = '\0';
				len--;
				break;
			}
			len--;
		}

		if (len == 0) {
			strcpy (pwd, "/");
		}
		else if ((strcmp(pwd, VFS_NATIVE_MOUNT_POINT) == 0) || (strcmp(pwd, VFS_NATIVE_SDCARD_MOUNT_POINT) == 0)) {
			strcat(pwd, "/");
		}
	}
	ESP_LOGI(FTP_TAG, "close_child, New pwd: %s", pwd);
}

// Remove file name from path
//-----------------------------------------------------------
static void remove_fname_from_path (char *pwd, char *fname) {
	ESP_LOGI(FTP_TAG, "remove_fname_from_path: %s - %s", pwd, fname);
	if (strlen(fname) == 0) return;
	char *xpwd = strstr(pwd, fname);
	if (xpwd == NULL) return;

	xpwd[0] = '\0';

#if 0
	ftp_fix_path(pwd);
#endif
	ESP_LOGI(FTP_TAG, "remove_fname_from_path: New pwd: %s", pwd);
}

// ==== Param functions =================================================

//------------------------------------------------------------------------------------------
static void ftp_pop_param(char **str, char *param, bool stop_on_space, bool stop_on_newline)
{
	char lastc = '\0';
	while (**str != '\0') {
		if (stop_on_space && (**str == ' ')) break;
		if ((**str == '\r') || (**str == '\n')) {
			if (!stop_on_newline) {
				(*str)++;
				continue;
			}
			else break;
		}
		if ((**str == '/') && (lastc == '/')) {
			(*str)++;
			continue;
		}
		lastc = **str;
		*param++ = **str;
		(*str)++;
	}
	*param = '\0';
}

//--------------------------------------------------
static ftp_cmd_index_t ftp_pop_command(char **str) {
	char _cmd[FTP_CMD_SIZE_MAX];
	ftp_pop_param (str, _cmd, true, true);
	stoupper (_cmd);
	for (ftp_cmd_index_t i = 0; i < E_FTP_NUM_FTP_CMDS; i++) {
		if (!strcmp (_cmd, ftp_cmd_table[i].cmd)) {
			// move one step further to skip the space
			(*str)++;
			return i;
		}
	}
	return E_FTP_CMD_NOT_SUPPORTED;
}

// Get file name from parameter and append to ftp_path
//-------------------------------------------------------
static void ftp_get_param_and_open_child(char **bufptr) {
	ftp_pop_param(bufptr, ftp_scratch_buffer, false, false);
	ftp_open_child(ftp_path, ftp_scratch_buffer);
	ftp_data.closechild = true;
}

// ==== Ftp command processing =====

//----------------------------------
static void ftp_process_cmd (void) {
	int32_t len;
	char *bufptr = (char *)ftp_cmd_buffer;
	ftp_result_t result;
	struct stat buf;
	int res;

	memset(bufptr, 0, FTP_MAX_PARAM_SIZE + FTP_CMD_SIZE_MAX);
	ftp_data.closechild = false;

	// use the reply buffer to receive new commands
	result = ftp_recv_non_blocking(ftp_data.c_sd, ftp_cmd_buffer, FTP_MAX_PARAM_SIZE + FTP_CMD_SIZE_MAX, &len);
	if (result == E_FTP_RESULT_OK) {
		ftp_cmd_buffer[len] = '\0';
		// bufptr is moved as commands are being popped
		ftp_cmd_index_t cmd = ftp_pop_command(&bufptr);
		if (!ftp_data.loggin.passvalid &&
				((cmd != E_FTP_CMD_USER) && (cmd != E_FTP_CMD_PASS) && (cmd != E_FTP_CMD_QUIT) && (cmd != E_FTP_CMD_FEAT) && (cmd != E_FTP_CMD_AUTH))) {
			ftp_send_reply(332, NULL);
			return;
		}
		if ((cmd >= 0) && (cmd < E_FTP_NUM_FTP_CMDS)) {
			ESP_LOGI(FTP_TAG, "CMD: %s", ftp_cmd_table[cmd].cmd);
		}
		else {
			ESP_LOGI(FTP_TAG, "CMD: %d", cmd);
		}
		char fullname[128];
		char fullname2[128];
		strcpy(fullname, MOUNT_POINT);
		strcpy(fullname2, MOUNT_POINT);

		switch (cmd) {
		case E_FTP_CMD_FEAT:
			ftp_send_reply(502, "no-features");
			break;
		case E_FTP_CMD_AUTH:
			ftp_send_reply(504, "not-supported");
			break;
		case E_FTP_CMD_SYST:
			ftp_send_reply(215, "UNIX Type: L8");
			break;
		case E_FTP_CMD_CDUP:
			ftp_close_child(ftp_path);
			ftp_send_reply(250, NULL);
			break;
		case E_FTP_CMD_CWD:
			ftp_pop_param (&bufptr, ftp_scratch_buffer, false, false);

			if (strlen(ftp_scratch_buffer) > 0) {
				if ((ftp_scratch_buffer[0] == '.') && (ftp_scratch_buffer[1] == '\0')) {
					ftp_data.dp = NULL;
					ftp_send_reply(250, NULL);
					break;
				}
				if ((ftp_scratch_buffer[0] == '.') && (ftp_scratch_buffer[1] == '.') && (ftp_scratch_buffer[2] == '\0')) {
					ftp_close_child (ftp_path);
					ftp_send_reply(250, NULL);
					break;
				}
				else ftp_open_child (ftp_path, ftp_scratch_buffer);
			}

			if ((ftp_path[0] == '/') && (ftp_path[1] == '\0')) {
				ftp_data.dp = NULL;
				ftp_send_reply(250, NULL);
			}
			else {
				strcat(fullname, ftp_path);
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_CWD fullname=[%s]", fullname);
				//ftp_data.dp = opendir(ftp_path);
				ftp_data.dp = opendir(fullname);
				if (ftp_data.dp != NULL) {
					closedir(ftp_data.dp);
					ftp_data.dp = NULL;
					ftp_send_reply(250, NULL);
				}
				else {
					ftp_close_child (ftp_path);
					ftp_send_reply(550, NULL);
				}
			}
			break;
		case E_FTP_CMD_PWD:
		case E_FTP_CMD_XPWD:
			{
				char lpath[128];
#if 0
				if (strstr(ftp_path, VFS_NATIVE_MOUNT_POINT) == ftp_path) {
					sprintf(lpath, "%s%s", VFS_NATIVE_INTERNAL_MP, ftp_path+strlen(VFS_NATIVE_MOUNT_POINT));
				}
				else if (strstr(ftp_path, VFS_NATIVE_SDCARD_MOUNT_POINT) == ftp_path) {
					sprintf(lpath, "%s%s", VFS_NATIVE_EXTERNAL_MP, ftp_path+strlen(VFS_NATIVE_SDCARD_MOUNT_POINT));
				}
				else strcpy(lpath,ftp_path);
#endif
				strcpy(lpath,ftp_path);

				ftp_send_reply(257, lpath);
			}
			break;
		case E_FTP_CMD_SIZE:
			{
				ftp_get_param_and_open_child (&bufptr);
				strcat(fullname, ftp_path);
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_SIZE fullname=[%s]", fullname);
				//int res = stat(ftp_path, &buf);
				int res = stat(fullname, &buf);
				if (res == 0) {
					// send the file size
					snprintf((char *)ftp_data.dBuffer, ftp_buff_size, "%"PRIu32, (uint32_t)buf.st_size);
					ftp_send_reply(213, (char *)ftp_data.dBuffer);
				} else {
					ftp_send_reply(550, NULL);
				}
			}
			break;
		case E_FTP_CMD_MDTM:
			ftp_get_param_and_open_child (&bufptr);
			strcat(fullname, ftp_path);
			ESP_LOGI(FTP_TAG, "E_FTP_CMD_MDTM fullname=[%s]", fullname);
			//res = stat(ftp_path, &buf);
			res = stat(fullname, &buf);
			if (res == 0) {
				// send the file modification time
				//snprintf((char *)ftp_data.dBuffer, ftp_buff_size, "%u", (uint32_t)buf.st_mtime);
				//snprintf((char *)ftp_data.dBuffer, ftp_buff_size, "20210212010203");
				//time_t time = buf.st_mtime + (CONFIG_LOCAL_TIMEZONE*60*60);
				time_t time = buf.st_mtime;
				struct tm *ptm = localtime(&time);
				//char buf[128];
				//strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", ptm);
				strftime((char *)ftp_data.dBuffer, ftp_buff_size, "%Y%m%d%H%M%S", ptm);
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_MDTM ftp_data.dBuffer=[%s]", ftp_data.dBuffer);
				ftp_send_reply(213, (char *)ftp_data.dBuffer);
			} else {
				ftp_send_reply(550, NULL);
			}
			break;
		case E_FTP_CMD_TYPE:
			ftp_send_reply(200, NULL);
			break;
		case E_FTP_CMD_USER:
			ftp_pop_param (&bufptr, ftp_scratch_buffer, true, true);
			if (!memcmp(ftp_scratch_buffer, ftp_user, MAX(strlen(ftp_scratch_buffer), strlen(ftp_user)))) {
				ftp_data.loggin.uservalid = true && (strlen(ftp_user) == strlen(ftp_scratch_buffer));
			}
			ftp_send_reply(331, NULL);
			break;
		case E_FTP_CMD_PASS:
			ftp_pop_param (&bufptr, ftp_scratch_buffer, true, true);
			if (!memcmp(ftp_scratch_buffer, ftp_pass, MAX(strlen(ftp_scratch_buffer), strlen(ftp_pass))) &&
					ftp_data.loggin.uservalid) {
				ftp_data.loggin.passvalid = true && (strlen(ftp_pass) == strlen(ftp_scratch_buffer));
				if (ftp_data.loggin.passvalid) {
					ftp_send_reply(230, NULL);
					ESP_LOGW(FTP_TAG, "Connected.");
					break;
				}
			}
			ftp_send_reply(530, NULL);
			break;
		case E_FTP_CMD_PASV:
			{
				// some servers (e.g. google chrome) send PASV several times very quickly
				closesocket(ftp_data.d_sd);
				ftp_data.d_sd = -1;
				ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;
				bool socketcreated = true;
				if (ftp_data.ld_sd < 0) {
					socketcreated = ftp_create_listening_socket(&ftp_data.ld_sd, FTP_PASIVE_DATA_PORT, FTP_DATA_CLIENTS_MAX - 1);
				}
				if (socketcreated) {
					uint8_t *pip = (uint8_t *)&ftp_data.ip_addr;
					ftp_data.dtimeout = 0;
					snprintf((char *)ftp_data.dBuffer, ftp_buff_size, "(%u,%u,%u,%u,%u,%u)",
							 pip[0], pip[1], pip[2], pip[3], (FTP_PASIVE_DATA_PORT >> 8), (FTP_PASIVE_DATA_PORT & 0xFF));
					ftp_data.substate = E_FTP_STE_SUB_LISTEN_FOR_DATA;
					ESP_LOGI(FTP_TAG, "Data socket created");
					ftp_send_reply(227, (char *)ftp_data.dBuffer);
				}
				else {
					ESP_LOGW(FTP_TAG, "Error creating data socket");
					ftp_send_reply(425, NULL);
				}
			}
			break;
		case E_FTP_CMD_LIST:
		case E_FTP_CMD_NLST:
			ftp_get_param_and_open_child(&bufptr);
			if (cmd == E_FTP_CMD_LIST) ftp_nlist = 0;
			else ftp_nlist = 1;
			if (ftp_open_dir_for_listing(ftp_path) == E_FTP_RESULT_CONTINUE) {
				ftp_data.state = E_FTP_STE_CONTINUE_LISTING;
				ftp_send_reply(150, NULL);
			}
			else ftp_send_reply(550, NULL);
			break;
		case E_FTP_CMD_RETR:
			ftp_data.total = 0;
			ftp_data.time = 0;
			ftp_get_param_and_open_child(&bufptr);
			if ((strlen(ftp_path) > 0) && (ftp_path[strlen(ftp_path)-1] != '/')) {
				if (ftp_open_file(ftp_path, "rb")) {
					ftp_data.state = E_FTP_STE_CONTINUE_FILE_TX;
					vTaskDelay(20 / portTICK_PERIOD_MS);
					ftp_send_reply(150, NULL);
				}
				else {
					ftp_data.state = E_FTP_STE_END_TRANSFER;
					ftp_send_reply(550, NULL);
				}
			}
			else {
				ftp_data.state = E_FTP_STE_END_TRANSFER;
				ftp_send_reply(550, NULL);
			}
			break;
		case E_FTP_CMD_APPE:
			ftp_data.total = 0;
			ftp_data.time = 0;
			ftp_get_param_and_open_child(&bufptr);
			if ((strlen(ftp_path) > 0) && (ftp_path[strlen(ftp_path)-1] != '/')) {
				if (ftp_open_file(ftp_path, "ab")) {
					ftp_data.state = E_FTP_STE_CONTINUE_FILE_RX;
					vTaskDelay(20 / portTICK_PERIOD_MS);
					ftp_send_reply(150, NULL);
				}
				else {
					ftp_data.state = E_FTP_STE_END_TRANSFER;
					ftp_send_reply(550, NULL);
				}
			}
			else {
				ftp_data.state = E_FTP_STE_END_TRANSFER;
				ftp_send_reply(550, NULL);
			}
			break;
		case E_FTP_CMD_STOR:
			ftp_data.total = 0;
			ftp_data.time = 0;
			ftp_get_param_and_open_child(&bufptr);
			if ((strlen(ftp_path) > 0) && (ftp_path[strlen(ftp_path)-1] != '/')) {
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_STOR ftp_path=[%s]", ftp_path);
				if (ftp_open_file(ftp_path, "wb")) {
					ftp_data.state = E_FTP_STE_CONTINUE_FILE_RX;
					vTaskDelay(20 / portTICK_PERIOD_MS);
					ftp_send_reply(150, NULL);
				}
				else {
					ftp_data.state = E_FTP_STE_END_TRANSFER;
					ftp_send_reply(550, NULL);
				}
			}
			else {
				ftp_data.state = E_FTP_STE_END_TRANSFER;
				ftp_send_reply(550, NULL);
			}
			break;
		case E_FTP_CMD_DELE:
			ftp_get_param_and_open_child(&bufptr);
			if ((strlen(ftp_path) > 0) && (ftp_path[strlen(ftp_path)-1] != '/')) {
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_DELE ftp_path=[%s]", ftp_path);

				strcat(fullname, ftp_path);
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_DELE fullname=[%s]", fullname);

				//if (unlink(ftp_path) == 0) {
				if (unlink(fullname) == 0) {
					vTaskDelay(20 / portTICK_PERIOD_MS);
					ftp_send_reply(250, NULL);
				}
				else ftp_send_reply(550, NULL);
			}
			else ftp_send_reply(250, NULL);
			break;
		case E_FTP_CMD_RMD:
			ftp_get_param_and_open_child(&bufptr);
			if ((strlen(ftp_path) > 0) && (ftp_path[strlen(ftp_path)-1] != '/')) {
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_RMD ftp_path=[%s]", ftp_path);

				strcat(fullname, ftp_path);
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_MKD fullname=[%s]", fullname);

				//if (rmdir(ftp_path) == 0) {
				if (rmdir(fullname) == 0) {
					vTaskDelay(20 / portTICK_PERIOD_MS);
					ftp_send_reply(250, NULL);
				}
				else ftp_send_reply(550, NULL);
			}
			else ftp_send_reply(250, NULL);
			break;
		case E_FTP_CMD_MKD:
			ftp_get_param_and_open_child(&bufptr);
			if ((strlen(ftp_path) > 0) && (ftp_path[strlen(ftp_path)-1] != '/')) {
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_MKD ftp_path=[%s]", ftp_path);

				strcat(fullname, ftp_path);
				ESP_LOGI(FTP_TAG, "E_FTP_CMD_MKD fullname=[%s]", fullname);

				//if (mkdir(ftp_path, 0755) == 0) {
				if (mkdir(fullname, 0755) == 0) {
					vTaskDelay(20 / portTICK_PERIOD_MS);
					ftp_send_reply(250, NULL);
				}
				else ftp_send_reply(550, NULL);
			}
			else ftp_send_reply(250, NULL);
			break;
		case E_FTP_CMD_RNFR:
			ftp_get_param_and_open_child(&bufptr);
			ESP_LOGI(FTP_TAG, "E_FTP_CMD_RNFR ftp_path=[%s]", ftp_path);

			strcat(fullname, ftp_path);
			ESP_LOGI(FTP_TAG, "E_FTP_CMD_MKD fullname=[%s]", fullname);

			//res = stat(ftp_path, &buf);
			res = stat(fullname, &buf);
			if (res == 0) {
				ftp_send_reply(350, NULL);
				// save the path of the file to rename
				strcpy((char *)ftp_data.dBuffer, ftp_path);
			} else {
				ftp_send_reply(550, NULL);
			}
			break;
		case E_FTP_CMD_RNTO:
			ftp_get_param_and_open_child(&bufptr);
			// the path of the file to rename was saved in the data buffer
			ESP_LOGI(FTP_TAG, "E_FTP_CMD_RNTO ftp_path=[%s], ftp_data.dBuffer=[%s]", ftp_path, (char *)ftp_data.dBuffer);
			strcat(fullname, (char *)ftp_data.dBuffer);
			ESP_LOGI(FTP_TAG, "E_FTP_CMD_RNTO fullname=[%s]", fullname);
			strcat(fullname2, ftp_path);
			ESP_LOGI(FTP_TAG, "E_FTP_CMD_RNTO fullname2=[%s]", fullname2);

			//if (rename((char *)ftp_data.dBuffer, ftp_path) == 0) {
			if (rename(fullname, fullname2) == 0) {
				ftp_send_reply(250, NULL);
			} else {
				ftp_send_reply(550, NULL);
			}
			break;
		case E_FTP_CMD_NOOP:
			ftp_send_reply(200, NULL);
			break;
		case E_FTP_CMD_QUIT:
			ftp_send_reply(221, NULL);
			break;
		default:
			// command not implemented
			ftp_send_reply(502, NULL);
			break;
		}

		if (ftp_data.closechild) {
			remove_fname_from_path(ftp_path, ftp_scratch_buffer);
		}
	}
	else if (result == E_FTP_RESULT_CONTINUE) {
		if (ftp_data.ctimeout > ftp_timeout) {
			ftp_send_reply(221, NULL);
			ESP_LOGW(FTP_TAG, "Connection timeout");
		}
	}
	else {
		ftp_close_cmd_data();
	}
}

//---------------------------------------
static void ftp_wait_for_enabled (void) {
	// Check if the telnet service has been enabled
	if (ftp_data.enabled) {
		ftp_data.state = E_FTP_STE_START;
	}
}

// ==== PUBLIC FUNCTIONS ===================================================================

//---------------------
void ftp_deinit(void) {
	if (ftp_path) free(ftp_path);
	if (ftp_cmd_buffer) free(ftp_cmd_buffer);
	if (ftp_data.dBuffer) free(ftp_data.dBuffer);
	if (ftp_scratch_buffer) free(ftp_scratch_buffer);
	ftp_path = NULL;
	ftp_cmd_buffer = NULL;
	ftp_data.dBuffer = NULL;
	ftp_scratch_buffer = NULL;
	
	memset(&ftp_data, 0, sizeof(ftp_data));
    ftp_data.c_sd = -1;
    ftp_data.d_sd = -1;
    ftp_data.ld_sd = -1;
}

//-------------------
bool ftp_init(void) {
	ftp_stop = 0;
	// Allocate memory for the data buffer, and the file system structures (from the RTOS heap)
	ftp_deinit();

	memset(&ftp_data, 0, sizeof(ftp_data_t));
	ftp_data.dBuffer = malloc(ftp_buff_size+1);
	if (ftp_data.dBuffer == NULL) return false;
	ftp_path = malloc(FTP_MAX_PARAM_SIZE);
	if (ftp_path == NULL) {
		free(ftp_data.dBuffer);
		return false;
	}
	ftp_scratch_buffer = malloc(FTP_MAX_PARAM_SIZE);
	if (ftp_scratch_buffer == NULL) {
		free(ftp_path);
		free(ftp_data.dBuffer);
		return false;
	}
	ftp_cmd_buffer = malloc(FTP_MAX_PARAM_SIZE + FTP_CMD_SIZE_MAX);
	if (ftp_cmd_buffer == NULL) {
		free(ftp_scratch_buffer);
		free(ftp_path);
		free(ftp_data.dBuffer);
		return false;
	}

	//SOCKETFIFO_Init((void *)ftp_fifoelements, FTP_SOCKETFIFO_ELEMENTS_MAX);

	ftp_data.c_sd  = -1;
	ftp_data.d_sd  = -1;
	ftp_data.lc_sd = -1;
	ftp_data.ld_sd = -1;
	ftp_data.e_open = E_FTP_NOTHING_OPEN;
	ftp_data.state = E_FTP_STE_DISABLED;
	ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;

	//if (ftp_mutex == NULL) ftp_mutex = xSemaphoreCreateMutex();
	return true;
}

//============================
int ftp_run (uint32_t elapsed)
{
	//if (xSemaphoreTake(ftp_mutex, FTP_MUTEX_TIMEOUT_MS / portTICK_PERIOD_MS) !=pdTRUE) return -1;
	if (ftp_stop) return -2;

	ftp_data.dtimeout += elapsed;
	ftp_data.ctimeout += elapsed;
	ftp_data.time += elapsed;

	switch (ftp_data.state) {
		case E_FTP_STE_DISABLED:
			ftp_wait_for_enabled();
			break;
		case E_FTP_STE_START:
			if (ftp_create_listening_socket(&ftp_data.lc_sd, FTP_CMD_PORT, FTP_CMD_CLIENTS_MAX - 1)) {
				ftp_data.state = E_FTP_STE_READY;
			}
			break;
		case E_FTP_STE_READY:
			if (ftp_data.c_sd < 0 && ftp_data.substate == E_FTP_STE_SUB_DISCONNECTED) {
				if (E_FTP_RESULT_OK == ftp_wait_for_connection(ftp_data.lc_sd, &ftp_data.c_sd, &ftp_data.ip_addr)) {
					ftp_data.txRetries = 0;
					ftp_data.logginRetries = 0;
					ftp_data.ctimeout = 0;
					ftp_data.loggin.uservalid = false;
					ftp_data.loggin.passvalid = false;
					strcpy (ftp_path, "/");
					ESP_LOGI(FTP_TAG, "Connected.");
					//ftp_send_reply (220, "Micropython FTP Server");
					ftp_send_reply (220, "ESP32 FTP Server");
					break;
				}
			}
			if (ftp_data.c_sd > 0 && ftp_data.substate != E_FTP_STE_SUB_LISTEN_FOR_DATA) {
				ftp_process_cmd();
				if (ftp_data.state != E_FTP_STE_READY) {
					break;
				}
			}
			break;
		case E_FTP_STE_END_TRANSFER:
			if (ftp_data.d_sd >= 0) {
				closesocket(ftp_data.d_sd);
				ftp_data.d_sd = -1;
			}
			break;
		case E_FTP_STE_CONTINUE_LISTING:
			// go on with listing
			{
				uint32_t listsize = 0;
				ftp_result_t list_res = ftp_list_dir((char *)ftp_data.dBuffer, ftp_buff_size, &listsize);
				if (listsize > 0) ftp_send_list(listsize);
				if (list_res == E_FTP_RESULT_OK) {
					ftp_send_reply(226, NULL);
					ftp_data.state = E_FTP_STE_END_TRANSFER;
				}
				ftp_data.ctimeout = 0;
			}
			break;
		case E_FTP_STE_CONTINUE_FILE_TX:
			// read and send the next block from the file
			{
				uint32_t readsize;
				ftp_result_t result;
				ftp_data.ctimeout = 0;
				result = ftp_read_file ((char *)ftp_data.dBuffer, ftp_buff_size, &readsize);
				if (result == E_FTP_RESULT_FAILED) {
					ftp_send_reply(451, NULL);
					ftp_data.state = E_FTP_STE_END_TRANSFER;
				}
				else {
					if (readsize > 0) {
						ftp_send_file_data(readsize);
						ftp_data.total += readsize;
						ESP_LOGI(FTP_TAG, "Sent %"PRIu32", total: %"PRIu32, readsize, ftp_data.total);
					}
					if (result == E_FTP_RESULT_OK) {
						ftp_send_reply(226, NULL);
						ftp_data.state = E_FTP_STE_END_TRANSFER;
						ESP_LOGI(FTP_TAG, "File sent (%"PRIu32" bytes in %"PRIu32" msec).", ftp_data.total, ftp_data.time);
					}
				}
			}
			break;
		case E_FTP_STE_CONTINUE_FILE_RX:
			{
				int32_t len;
				ftp_result_t result = E_FTP_RESULT_OK;

				ESP_LOGI(FTP_TAG, "ftp_buff_size=%d", ftp_buff_size);
				result = ftp_recv_non_blocking(ftp_data.d_sd, ftp_data.dBuffer, ftp_buff_size, &len);
				if (result == E_FTP_RESULT_OK) {
					// block of data received
					ftp_data.dtimeout = 0;
					ftp_data.ctimeout = 0;
					// save received data to file
					if (E_FTP_RESULT_OK != ftp_write_file ((char *)ftp_data.dBuffer, len)) {
						ftp_send_reply(451, NULL);
						ftp_data.state = E_FTP_STE_END_TRANSFER;
						ESP_LOGW(FTP_TAG, "Error writing to file");
					}
					else {
						ftp_data.total += len;
						ESP_LOGI(FTP_TAG, "Received %"PRIu32", total: %"PRIu32, len, ftp_data.total);
					}
				}
				else if (result == E_FTP_RESULT_CONTINUE) {
					// nothing received
					if (ftp_data.dtimeout > FTP_DATA_TIMEOUT_MS) {
						ftp_close_files_dir();
						ftp_send_reply(426, NULL);
						ftp_data.state = E_FTP_STE_END_TRANSFER;
						ESP_LOGW(FTP_TAG, "Receiving to file timeout");
					}
				}
				else {
					// File received (E_FTP_RESULT_FAILED)
					ftp_close_files_dir();
					ftp_send_reply(226, NULL);
					ftp_data.state = E_FTP_STE_END_TRANSFER;
					ESP_LOGI(FTP_TAG, "File received (%"PRIu32" bytes in %"PRIu32" msec).", ftp_data.total, ftp_data.time);
					break;
				}
			}
			break;
		default:
			break;
	}

	switch (ftp_data.substate) {
	case E_FTP_STE_SUB_DISCONNECTED:
		break;
	case E_FTP_STE_SUB_LISTEN_FOR_DATA:
		if (E_FTP_RESULT_OK == ftp_wait_for_connection(ftp_data.ld_sd, &ftp_data.d_sd, NULL)) {
			ftp_data.dtimeout = 0;
			ftp_data.substate = E_FTP_STE_SUB_DATA_CONNECTED;
			ESP_LOGI(FTP_TAG, "Data socket connected");
		}
		else if (ftp_data.dtimeout > FTP_DATA_TIMEOUT_MS) {
			ESP_LOGW(FTP_TAG, "Waiting for data connection timeout (%"PRIi32")", ftp_data.dtimeout);
			ftp_data.dtimeout = 0;
			// close the listening socket
			closesocket(ftp_data.ld_sd);
			ftp_data.ld_sd = -1;
			ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;
		}
		break;
	case E_FTP_STE_SUB_DATA_CONNECTED:
		if (ftp_data.state == E_FTP_STE_READY && (ftp_data.dtimeout > FTP_DATA_TIMEOUT_MS)) {
			// close the listening and the data socket
			closesocket(ftp_data.ld_sd);
			closesocket(ftp_data.d_sd);
			ftp_data.ld_sd = -1;
			ftp_data.d_sd = -1;
			ftp_close_filesystem_on_error ();
			ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;
			ESP_LOGW(FTP_TAG, "Data connection timeout");
		}
		break;
	default:
		break;
	}

	// check the state of the data sockets
	if (ftp_data.d_sd < 0 && (ftp_data.state > E_FTP_STE_READY)) {
		ftp_data.substate = E_FTP_STE_SUB_DISCONNECTED;
		ftp_data.state = E_FTP_STE_READY;
		ESP_LOGI(FTP_TAG, "Data socket disconnected");
	}

	//xSemaphoreGive(ftp_mutex);
	return 0;
}

//----------------------
bool ftp_enable (void) {
	bool res = false;
	if (ftp_data.state == E_FTP_STE_DISABLED) {
		ftp_data.enabled = true;
		res = true;
	}
	return res;
}

//-------------------------
bool ftp_isenabled (void) {
	bool res = (ftp_data.enabled == true);
	return res;
}

//-----------------------
bool ftp_disable (void) {
	bool res = false;
	if (ftp_data.state == E_FTP_STE_READY) {
		_ftp_reset();
		ftp_data.enabled = false;
		ftp_data.state = E_FTP_STE_DISABLED;
		res = true;
	}
	return res;
}

//---------------------
bool ftp_reset (void) {
	_ftp_reset();
	return true;
}

// Return current ftp server state
//------------------
int ftp_getstate() {
	int fstate = ftp_data.state | (ftp_data.substate << 8);
	if ((ftp_data.state == E_FTP_STE_READY) && (ftp_data.c_sd > 0)) fstate = E_FTP_STE_CONNECTED;
	return fstate;
}

//-------------------------
bool ftp_terminate (void) {
	bool res = false;
	if (ftp_data.state == E_FTP_STE_READY) {
		ftp_stop = 1;
		_ftp_reset();
		res = true;
	}
	return res;
}

//-------------------------
bool ftp_stop_requested() {
	bool res = (ftp_stop == 1);
	return res;
}

#if 0
//-------------------------------
int32_t ftp_get_maxstack (void) {
	int32_t maxstack = ftp_stack_size - uxTaskGetStackHighWaterMark(NULL);
	return maxstack;
}
#endif

//-------------------------------
void ftp_task (void *pvParameters)
{
	ESP_LOGI(FTP_TAG, "ftp_task start");
	ftp_deinit();
	esp_log_level_set(FTP_TAG, ESP_LOG_DEBUG); 
	strcpy(ftp_user, "anonymous");
	strcpy(ftp_pass, "");
	ESP_LOGI(FTP_TAG, "ftp_user:[%s] ftp_pass:[%s]", ftp_user, ftp_pass);

	uint64_t elapsed, time_ms = mp_hal_ticks_ms();
	// Initialize ftp, create rx buffer and mutex
	if (!ftp_init()) {
		ESP_LOGE("[Ftp]", "Init Error");
		xEventGroupSetBits(xEventTask, FTP_TASK_FINISH_BIT);
		vTaskDelete(NULL);
	}

	// We have network connection, enable ftp
	ftp_enable();

	time_ms = mp_hal_ticks_ms();
	while (1) {
		// Calculate time between two ftp_run() calls
		elapsed = mp_hal_ticks_ms() - time_ms;
		time_ms = mp_hal_ticks_ms();

		int res = ftp_run(elapsed);
		if (res < 0) {
			if (res == -1) {
				ESP_LOGE("[Ftp]", "\nRun Error");
			}
			// -2 is returned if Ftp stop was requested by user
			break;
		}

		vTaskDelay(1);

	} // end while

	ESP_LOGW("[Ftp]", "\nTask terminated!");
	ftp_deinit();
	xEventGroupSetBits(xEventTask, FTP_TASK_FINISH_BIT);
	vTaskDelete(NULL);
}
