module ftp

/*
basic ftp module
	RFC-959
	https://tools.ietf.org/html/rfc959

	Methods:
	ftp.connect(host)
	ftp.login(user, passw)
	pwd := ftp.pwd()
	ftp.cd(folder)
	dtp := ftp.pasv()
	ftp.dir()
	ftp.get(file)
	dtp.read()
	dtp.close()
	ftp.close()
*/

import net
import io

const (
	connected             = 220
	specify_password      = 331
	logged_in             = 230
	login_first           = 503
	anonymous             = 530
	open_data_connection  = 150
	close_data_connection = 226
	command_ok            = 200
	denied                = 550
	passive_mode          = 227
	complete              = 226
)

struct DTP {
mut:
	conn net.TcpConn
	reader io.BufferedReader
	ip   string
	port int
}

fn (mut dtp DTP) read() ?[]byte {
	mut data := []byte{len: 1024}
	dtp.reader.read(mut data)?
	return data
}

fn (dtp DTP) close() {
	dtp.conn.close()
}

struct FTP {
mut:
	conn        net.TcpConn
	reader io.BufferedReader
	buffer_size int
}

pub fn new() FTP {
	mut f := FTP{}
	f.buffer_size = 1024
	return f
}

fn (mut ftp FTP) write(data string) ? {
	$if debug {
		println('FTP.v >>> $data')
	}
	ftp.conn.write('$data\r\n'.bytes())?
}

fn (mut ftp FTP) read() ?(int, string) {
	mut data := ftp.reader.read_line()?
	$if debug {
		println('FTP.v <<< $data')
	}
	if data.len < 5 {
		return 0, ''
	}
	code := data[..3].int()
	if data[3] == `-` {
		for {
			data = ftp.reader.read_line()?
			if data[..3].int() == code && data[3] != `-` {
				break
			}
		}
	}
	return code, data
}

pub fn (mut ftp FTP) connect(ip string) ?bool {
	conn := net.dial_tcp('$ip:21')?
	ftp.conn = conn
	code, _ := ftp.read()?
	if code == connected {
		return true
	}
	ftp.reader = io.new_buffered_reader(reader: io.make_reader(ftp.conn))
	return false
}

pub fn (mut ftp FTP) login(user string, passwd string) ?bool {
	ftp.write('USER $user') or {
		$if debug {
			println('ERROR sending user')
		}
		return false
	}
	mut code, _ := ftp.read()?
	if code == logged_in {
		return true
	}
	if code != specify_password {
		return false
	}
	ftp.write('PASS $passwd') or {
		$if debug {
			println('ERROR sending password')
		}
		return false
	}
	code, _ = ftp.read()?
	if code == logged_in {
		return true
	}
	return false
}

pub fn ( mut ftp FTP) close() ? {
	ftp.write('QUIT')?
	ftp.conn.close()
}

pub fn ( mut ftp FTP) pwd() ?string {
	ftp.write('PWD')? 
	_, data := ftp.read()?
	spl := data.split('"') // "
	if spl.len >= 2 {
		return spl[1]
	}
	return data
}

pub fn ( mut ftp FTP) cd(dir string) ? {
	ftp.write('CWD $dir') or {
		return
	}
	mut code, mut data := ftp.read()?
	match int(code) {
		denied {
			$if debug {
				println('CD $dir denied!')
			}
		}
		complete {
			code, data = ftp.read()?
		}
		else {}
	}
	$if debug {
		println('CD $data')
	}
}

fn new_dtp(msg string) ?DTP {
	if !is_dtp_message_valid(msg) {
		return error('Bad message')
	}
	ip, port := get_host_ip_from_dtp_message(msg)
	conn := net.dial_tcp('$ip:$port') or {
		return error('Cannot connect to the data channel')
	}
	dtp := DTP{
		conn: conn
		ip: ip
		port: port
	}
	return dtp
}

fn ( mut ftp FTP) pasv() ?DTP {
	ftp.write('PASV') or {
	}
	code, data := ftp.read()?
	$if debug {
		println('pass: $data')
	}
	if code != passive_mode {
		return error('pasive mode not allowed')
	}
	dtp := new_dtp(data) ?
	return dtp
}

pub fn ( mut ftp FTP) dir() ?[]string {
	mut dtp := ftp.pasv() or {
		return error('cannot establish data connection')
	}
	ftp.write('LIST') or {
	}
	code, _ := ftp.read()?
	if code == denied {
		return error('LIST denied')
	}
	if code != open_data_connection {
		return error('data channel empty')
	}
	list_dir := dtp.read()?
	result, _ := ftp.read()?
	if result != close_data_connection {
		println('LIST not ok')
	}
	dtp.close()
	mut dir := []string{}
	sdir := list_dir.bytestr()
	for lfile in sdir.split('\n') {
		if lfile.len > 1 {
			spl := lfile.split(' ')
			dir << spl[spl.len - 1]
		}
	}
	return dir
}

pub fn (mut ftp FTP) get(file string) ?[]byte {
	mut dtp := ftp.pasv() or {
		return error('Cannot stablish data connection')
	}
	ftp.write('RETR $file') or {
	}
	code, _ := ftp.read()?
	if code == denied {
		return error('Permission denied')
	}
	if code != open_data_connection {
		return error('Data connection not ready')
	}
	blob := dtp.read()?
	dtp.close()
	return blob
}

fn is_dtp_message_valid(msg string) bool {
	// An example of message:
	// '227 Entering Passive Mode (209,132,183,61,48,218)'
	return msg.contains('(') && msg.contains(')') && msg.contains(',')
}

fn get_host_ip_from_dtp_message(msg string) (string, int) {
	mut par_start_idx := -1
	mut par_end_idx := -1
	for i, c in msg {
		if c == `(` {
			par_start_idx = i + 1
		} else if c == `)` {
			par_end_idx = i
		}
	}
	data := msg[par_start_idx..par_end_idx].split(',')
	ip := data[0..4].join('.')
	port := data[4].int() * 256 + data[5].int()
	return ip, port
}
