#define FTP_BUF_SIZE    1024

I64 FTPMessageGet(CTCPSocket *message_socket, U8 *buf)
{ // returns FTP status code of message
    U8      *temp;
    Bool     first = TRUE;
    I64      status;

    "\n";
    while (TCPSocketReceive(message_socket, buf, FTP_BUF_SIZE) > 0)
    {
        if (first)
        {
            status = Str2I64(buf);
            first = FALSE;
        }
        temp = MStrPrint("%Q", buf);
        temp = StrReplace(temp, "\\r", "",, TRUE);
        temp = StrReplace(temp, "\\n", "\n",, TRUE);
        temp = StrReplace(temp, "\\t", "\t",, TRUE);
        temp = StrReplace(temp, "\\\"", "\"",, TRUE);
        "%s", temp;
        MemSet(buf, 0, FTP_BUF_SIZE);
        Free(temp);
    }
    "\n";

    return status;
}

I64 FTPReplyPassiveParse(CTCPSocket *message_socket, CSocketAddressIPV4 *dest_addr_out)
{ // Parse an FTP reply message to the PASV command, destination written to arg.
    U8           buf[8192], *str;
    I64          tk, cur_section = 0, recv = 0, recv_sum = 0, res;
    U32          ip_addr = 0;
    U16          port = 0;
    CCompCtrl   *cc;

    while ((recv = TCPSocketReceive(message_socket, buf + recv_sum, 8192 - recv_sum)) > 0)
    {
        recv_sum += recv;
    }
    str = StrNew(buf);
    cc = CompCtrlNew(str);

    while ((tk = Lex(cc)))
    {
        switch (tk)
        {
            case TK_IDENT:
                cur_section = 0;
                ip_addr = 0;
                port = 0;
                break;

            case ',':
                break; // skip

            case TK_I64:
                if (cur_section < 4)
                {
                    ip_addr.u8[3 - cur_section] = cc->cur_i64;
                    cur_section++;
                }
                else if (4 <= cur_section < 6)
                {
                    port.u8[5 - cur_section] = cc->cur_i64;
                    cur_section++;
                }
                
                if (cur_section >= 6)
                    goto parse_done;

                break;

            default:
                break;
        }
    }

parse_done:
    if (cur_section < 6)
        res = -1;
    else
    {
        dest_addr_out->family           = AF_INET;
        dest_addr_out->port             = EndianU16(port);
        dest_addr_out->address.address  = ip_addr;

        res = 0;
    }

    CompCtrlDel(cc);
    return res;
}

I64 FTPFileDownload(CTCPSocket *data_socket, U8 *dest)
{
    CFile   *f;
    I64      data_len = 0, recv_sum = 0, recv;
    U8       buf[BLK_SIZE];

    progress4 = 0;
    f = FOpen(dest, "w");
    if (!f)
    {
        ST_ERR_ST "Failed to open %s for writing\n", dest;
        return -1;
    }

    while (TRUE)
    {
        recv = TCPSocketReceive(data_socket, buf + data_len, sizeof(buf) - data_len);
        if (recv <= 0)
        {
            if (recv < 0)
                ST_ERR_ST "Failed to receive TCP data\n";
            if (data_len != 0 && !FBlkWrite(f, buf))
                break;

            f->de.size = recv_sum;
            FClose(f);
            TCPSocketClose(data_socket);

            return recv;
        }

        data_len += recv;
        recv_sum += recv;
        progress4 += recv;

        if (data_len == BLK_SIZE)
        {
            if (!FBlkWrite(f, buf))
                break;

            data_len = 0;
        }
    }

    TCPSocketClose(data_socket);
    ST_ERR_ST "Write failed, %s may be corrupted\n", dest;
    FClose(f);
    return -1;
}

I64 FTPFileView(U8 *filename=NULL, CTask *parent=NULL, CTask **_pu_task=NULL)
{
    U8 *st = MStrPrint("Cd(\"%Q\");Plain(\"%Q\");", __DIR__, filename);
    I64 res = PopUp(st, parent, _pu_task);

    Free(st);

    return res;
}

U8 *FTPBasename(U8 *path)
{
    U8 *lastslash = StrLastOcc(path, "/"), *result;

    if (lastslash == NULL)
        result = path;
    else
        result = lastslash + 1;

    //BAD FOR FILENAMES: ? / | = % : ; * + " < > space 
    result = StrReplace(result, "?", "");
    result = StrReplace(result, "/", "",, TRUE);
    result = StrReplace(result, "|", "",, TRUE);
    result = StrReplace(result, "=", "",, TRUE);
    result = StrReplace(result, "%", "",, TRUE);
    result = StrReplace(result, ":", "",, TRUE);
    result = StrReplace(result, ";", "",, TRUE);
    result = StrReplace(result, "*", "",, TRUE);
    result = StrReplace(result, "+", "",, TRUE);
    result = StrReplace(result, "\"", "",, TRUE);
    result = StrReplace(result, "<", "",, TRUE);
    result = StrReplace(result, ">", "",, TRUE);
    result = StrReplace(result, " ", "",, TRUE);

    if (StrLen(result) > 22)
        result[21] = 0; // truncate filename len

    SysLog("%s\n", result);

    return result;
}

class CFTPFilePrompt
{
    U8 name[256] format "$DA-P,LEN=255,A=\"FileName:%s\"$";
};
U8 *FTPFilePrompt(U8 *path)
{
    CFTPFilePrompt   form;
    U8              *basename = FTPBasename(path);

    MemSet(form.name, 0, 256);
    MemCopy(form.name, basename, MinI64(StrLen(basename), sizeof(form.name) - 1));
    form.name[255] = 0;
    if (PopUpForm(&form))
    {
        if (StrLen(form.name) >= 26)
            form.name[25] = 0;
        return StrNew(form.name);
    }

    return NULL;
}

I64 FTPClient(U8 *hostname=NULL, U16 port=21)
{
    U32                  addr;
    CAddressInfo        *current;
    CAddressInfo        *result = NULL;
    I64                  error, tk, i;
    CSocketAddressIPV4   ipv4_address, *temp_ipv4, data_ipv4;
    CTCPSocket          *message_socket = TCPSocket(AF_INET), *data_socket;
    I64                  status = 0;
    U8                   buf[FTP_BUF_SIZE], *temp, *input_str, *dest;
    CCompCtrl           *cc;

    if (!hostname)
        hostname = StrGet("\nEnter FTP server address (URL or IPV4): ");


    if (!IPV4AddressParse(hostname, &addr))
    {
        error = DNSAddressInfoGet(hostname, NULL, &result);
        if (error < 0)
        {
            NetErr("FTP Client: Failed at DNS Get Address Info.");
            return -1;
        }
        current = result;
        while (current)
        {
            if (current->family == AF_INET)
            {
                temp_ipv4 = current->address;
                addr = EndianU32(temp_ipv4->address); // why does it need EndianU32
                break;
            }
            current = current->next;
        }

        if (!current)
        {
            NetErr("FTP Client: Failed to resolve address.");
            return -1;
        }

    }

    ipv4_address.port               = EndianU16(port);
    ipv4_address.family             = AF_INET;
    ipv4_address.address.address    = addr;

    message_socket->timeout = TCP_TIMEOUT;

    if (TCPSocketConnect(message_socket, &ipv4_address) != 0)
    {
        "\nFailed to connect to server.\n";
        TCPSocketClose(message_socket);
        return -1;
    }
    else
        "\nSuccessfully connected.\n";

    message_socket->timeout = 2 * JIFFY_FREQ;

    FTPMessageGet(message_socket, buf);


    "\n\nType HELP for command list.\n\n";
    while (TRUE)
    {
        input_str = StrGet(">");
        cc = CompCtrlNew(input_str);

        while ((tk = Lex(cc)))
        {
            switch (tk)
            {
                case TK_IDENT: // command
                    "COMMAND:%s\n", cc->cur_str;
                    for (i = 0; i < StrLen(cc->cur_str); i++)
                        cc->cur_str[i] = ToUpper(cc->cur_str[i]);

                    if (!StrCompare(cc->cur_str, "CWD") ||
                        !StrCompare(cc->cur_str, "CD"))
                    {
                        StrFirstRemove(input_str, " ");
                        if (!StrCompare(input_str, ""))
                        {
                            ST_ERR_ST "Must provide argument!\n";
                            goto lex_done;
                        }

                        "ARG:%s\n", input_str;

                        temp = MStrPrint("CWD %s\r\n", input_str);
                        TCPSocketSendString(message_socket, temp);
                        FTPMessageGet(message_socket, buf);
                        Free(temp);

                        goto lex_done;
                    }
                    else if (!StrCompare(cc->cur_str, "LIST")   ||
                             !StrCompare(cc->cur_str, "DIR")    ||
                             !StrCompare(cc->cur_str, "LS"))
                    {
                        TCPSocketSendString(message_socket, "PASV\r\n");
                        if (FTPReplyPassiveParse(message_socket, &data_ipv4) != 0)
                        {
                            ST_ERR_ST "Error parsing server response to PASV command!\n";
                            goto lex_done;
                        }
                        data_socket = TCPSocket(AF_INET);
                        data_socket->timeout = 2 * JIFFY_FREQ;

                        if (TCPSocketConnect(data_socket, &data_ipv4) != 0)
                        {
                            ST_ERR_ST "Failed at data socket connect!";
                            TCPSocketClose(data_socket);
                            goto lex_done;
                        }

                        TCPSocketSendString(message_socket, "LIST\r\n");
                        FTPMessageGet(data_socket, buf);
                        FTPMessageGet(message_socket, buf);

                        if (TCPSocketClose(data_socket) != 0)
                            ST_ERR_ST "Failed at data socket close!";

                        goto lex_done;
                    }
                    else if (!StrCompare(cc->cur_str, "PWD"))
                    {
                        TCPSocketSendString(message_socket, "PWD\r\n");
                        FTPMessageGet(message_socket, buf);
                    }
                    else if (!StrCompare(cc->cur_str, "RETR")   ||
                             !StrCompare(cc->cur_str, "GET"))
                    {
                        StrFirstRemove(input_str, " ");
                        if (!StrCompare(input_str, ""))
                        {
                            ST_ERR_ST "Must provide argument!\n";
                            goto lex_done;
                        }

                        TCPSocketSendString(message_socket, "PASV\r\n");
                        if (FTPReplyPassiveParse(message_socket, &data_ipv4) != 0)
                        {
                            ST_ERR_ST "Error parsing server response to PASV command!\n";
                            goto lex_done;
                        }
                        data_socket = TCPSocket(AF_INET);
                        data_socket->timeout = 2 * JIFFY_FREQ;

                        if (TCPSocketConnect(data_socket, &data_ipv4) != 0)
                        {
                            ST_ERR_ST "Failed at data socket connect!";
                            TCPSocketClose(data_socket);
                            goto lex_done;
                        }

                        dest = FTPFilePrompt(input_str);
                        if (dest == NULL)
                        {
                            ST_ERR_ST "Download filename cannot be empty!";
                            TCPSocketClose(data_socket);
                            goto lex_done;
                        }


                        temp = MStrPrint("RETR %s\r\n", input_str);
                        TCPSocketSendString(message_socket, temp);
                        FTPFileDownload(data_socket, dest);
                        FTPMessageGet(message_socket, buf);
                        "\nOpen file with Ed? ";
                        if (YorN)
                            FTPFileView(dest);

                        goto lex_done;


                    }
                    else if (!StrCompare(cc->cur_str, "VIEW")   ||
                             !StrCompare(cc->cur_str, "CAT"))
                    {
                        StrFirstRemove(input_str, " ");
                        if (!StrCompare(input_str, ""))
                        {
                            ST_ERR_ST "Must provide argument!\n";
                            goto lex_done;
                        }

                        TCPSocketSendString(message_socket, "PASV\r\n");
                        if (FTPReplyPassiveParse(message_socket, &data_ipv4) != 0)
                        {
                            ST_ERR_ST "Error parsing server response to PASV command!\n";
                            goto lex_done;
                        }
                        data_socket = TCPSocket(AF_INET);
                        data_socket->timeout = 2 * JIFFY_FREQ;

                        if (TCPSocketConnect(data_socket, &data_ipv4) != 0)
                        {
                            ST_ERR_ST "Failed at data socket connect!";
                            TCPSocketClose(data_socket);
                            goto lex_done;
                        }
                        temp = MStrPrint("RETR %s\r\n", input_str);
                        TCPSocketSendString(message_socket, temp);
                        FTPMessageGet(data_socket, buf);
                        FTPMessageGet(message_socket, buf);

                        if (TCPSocketClose(data_socket) != 0)
                            ST_ERR_ST "Failed at data socket close!";

                        goto lex_done;
                    }
                    else if (!StrCompare(cc->cur_str, "USER"))
                    {
                        StrFirstRemove(input_str, " ");
                        if (!StrCompare(input_str, ""))
                        {
                            ST_ERR_ST "Must provide argument!\n";
                            goto lex_done;
                        }

                        "ARG:%s\n", input_str;

                        temp = MStrPrint("USER %s\r\n", input_str);
                        TCPSocketSendString(message_socket, temp);
                        FTPMessageGet(message_socket, buf);
                        Free(temp);

                        goto lex_done;

                    }
                    else if (!StrCompare(cc->cur_str, "PASS"))
                    {
                        StrFirstRemove(input_str, " ");
                        if (!StrCompare(input_str, ""))
                        {
                            ST_ERR_ST "Must provide argument!\n";
                            goto lex_done;
                        }
    
                        "ARG:%s\n", input_str;

                        temp = MStrPrint("PASS %s\r\n", input_str);
                        TCPSocketSendString(message_socket, temp);
                        FTPMessageGet(message_socket, buf);
                        Free(temp);

                        goto lex_done;

                    }
                    else if (!StrCompare(cc->cur_str, "QUIT") ||
                             !StrCompare(cc->cur_str, "EXIT") ||
                             !StrCompare(cc->cur_str, "BYE"))
                    {
                        TCPSocketSendString(message_socket, "QUIT\r\n");
                        status = FTPMessageGet(message_socket, buf);
                        if (status == 221)
                            "Server closed successfully, exiting normally...\n\n";
                        else
                            ST_WARN_ST "Server error during close, force exit...\n\n";
                        TCPSocketClose(message_socket);
                        "See ya later!\n\n";
                        CompCtrlDel(cc);
                        return 0;
                    }
                    else
                    {
                        "
Command List:

    (Alternate names separated by '/'; names case-insensitive)


CWD/CD <path>       = Change Working Directory to <path>.
LIST/DIR/LS         = List directory contents.
PWD                 = Print name of current directory.
RETR/GET <file>     = Download copy of <file> from server.
VIEW/CAT <file>     = Print the contents of <file> to the screen.
USER <username>     = Set username to <username>.
PASS <password>     = Set password to <password>.
QUIT/EXIT/BYE       = End FTP session.\n\n";
                    }

                    break;

                default:
                    "\nCommand expected. Type HELP for command list.\n";
                    break;
            }
        }
lex_done:
        CompCtrlDel(cc);
    }
};

FTPClient;