/*
*	myNZB (C) 2009 E. Oriani, ema <AT> fastwebnet <DOT> it
*
*	This file is part of myNZB.
*
*	myNZB 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.
*
*	myNZB 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 myNZB.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <iostream>
#include <stdexcept>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include "settings.h"
#include "connection.h"
#include "nzb.h"
#include "nzb_file.h"
#include "notify.h"
#include "sig.h"
#include "utils.h"

const static std::string	__myNZB__ = "myNZB",
				__version__ = "0.2.1";

const static int		__UID__ = geteuid(),
				__GID__ = getegid();

// Macros to see if a file/directory is rwx by current user
#define IS_O_RWX(x)	((x & (S_IROTH|S_IWOTH|S_IXOTH))==(S_IROTH|S_IWOTH|S_IXOTH))
#define IS_G_RWX(x, d)	((d==__GID__) && (S_IRGRP|S_IWGRP|S_IXGRP)==(x & (S_IRGRP|S_IWGRP|S_IXGRP)))
#define IS_U_RWX(x, d)	((d==__UID__) && (S_IRUSR|S_IWUSR|S_IXUSR)==(x & (S_IRUSR|S_IWUSR|S_IXUSR)))
// a file/directory is rwx by current user if
// (u+rwx) OR (not_user AND (g+rwx OR (not_group AND o+rxw)))
#define IS_F_RWX(x, ud, gd)	(IS_U_RWX(x, ud) || ((ud!=__UID__) && (IS_G_RWX(x, gd) || ((gd!=__GID__) && IS_O_RWX(x)))))

// Macros to see if a file is rx by current user
#define IS_O_RX(x)	((x & (S_IROTH|S_IXOTH))==(S_IROTH|S_IXOTH))
#define IS_G_RX(x, d)	((d==__GID__) && (S_IRGRP|S_IXGRP)==(x & (S_IRGRP|S_IXGRP)))
#define IS_U_RX(x, d)	((d==__UID__) && (S_IRUSR|S_IXUSR)==(x & (S_IRUSR|S_IXUSR)))
// a file is rx by current user if
// (u+rx) OR (not_user AND (g+rx OR (not_group AND o+rx)))
#define IS_F_RX(x, ud, gd)	(IS_U_RX(x, ud) || ((ud!=__UID__) && (IS_G_RX(x, gd) || ((gd!=__GID__) && IS_O_RX(x)))))

void print_help(void) {
	std::cerr <<	__myNZB__ << " v" << __version__ << " - (C) 2009 E. Oriani\n"
			"Usage: myNZB [options] file0.nzb file0.nzb ...\n"
			"\t-s: NNTP server to connect (use the colon notation to specify the port, eg. \"nntp.server.com:119\")\n"
			"\t-u: NNTP username (default none, eg. \"\")\n"
			"\t-p: NNTP password (default none, eg. \"\")\n"
			"\t-c: number of parallel connections to NNTP server (default 2)\n"
			"\t-t: wait time (ms) in between dowloading NZB segments (default 0)\n"
			"\t-y: yydecode path (default to: \"/usr/bin/yydecode\")\n"
			"\t-o: output directory (default \"./\")\n"
			"\t-x: percentage value to allow retry download segments with errors (default 0.2)\n"
			"\t-r: reset TCP/IP connections with NNTP server for each segment\n"
			"\t-n: do NOT notify file activity through libnotify (dynamically loaded)\n"
			"\t-l: set log level:\n"
			"\t\t0 No log\n"
			"\t\t1 Errors only\n"
			"\t\t2 Warnings and errors\n"
			"\t\t3 Info, warnings and errors (default)\n"
			"\t-T: set temporary directory (default \"./\")\n"
			"\t-S: set recv timeout (msec) on NNTP connections (default 2000 , 2 sec)\n"
			"\t-Y: strict search for yEnc encoding in NNTP subject (default false)\n"
			"\t-h: print this help and exit\n"
		 <<	std::flush;
}

int parse_options(int argc, char *argv[]) {
	opterr = 0;
     	char c;

	while ((c = getopt (argc, argv, "s:u:p:c:t:o:x:l:T:S:hrnY")) != -1) {
		switch (c) {
			case 's':
				{
					const char*	p_colon = strchr(optarg, ':');
					if (!p_colon) settings::NNTP_server = optarg;
					else {
						settings::NNTP_server = std::string(optarg, p_colon-optarg);
						settings::NNTP_port = atoi(p_colon+1);
						if (0 >= settings::NNTP_port) settings::NNTP_port = 119;
					}
				}
             			break;
			case 'u':
				settings::NNTP_user = optarg;
				break;
			case 'p':
				settings::NNTP_pass = optarg;
				break;
			case 'c':
				settings::NNTP_max_conn = atoi(optarg);
				if (0 >= settings::NNTP_max_conn) settings::NNTP_max_conn = 1;
				break;
			case 't':
				settings::NNTP_wait_segment = atoi(optarg);
				if (0 > settings::NNTP_wait_segment) settings::NNTP_max_conn = 1000;
				break;
			case 'y':
				settings::YYDECODE_PATH = optarg;
				break;
			case 'o':
				settings::OUT_DIR = optarg;
				if (settings::OUT_DIR == "") settings::OUT_DIR = "./"; 
				break;
			case 'x':
				settings::MAX_PERC_REPAIR = atof(optarg);
				if (settings::MAX_PERC_REPAIR < 0.0) settings::MAX_PERC_REPAIR = 0.0;
				if (settings::MAX_PERC_REPAIR > 1.0) settings::MAX_PERC_REPAIR = 1.0;
				break;
			case 'l':
				if (isdigit(optarg[0])) {
					char log_level[2];
					log_level[0] = optarg[0];
					log_level[1] = '\0';
					const int log_ilev = atoi(optarg);
					switch (log_ilev) {
						case 0:
							settings::LOG = 0x00;
							break;
						case 1:
							settings::LOG = 0x01;
							break;
						case 2:
							settings::LOG = 0x03;
							break;
						case 3:
							settings::LOG = 0x07;
							break;
						default:
							break;
					}
				}
				break;
			case 'T':
				settings::TMP_DIR = optarg;
				if (settings::TMP_DIR == "") settings::TMP_DIR = "./";
				if ('/' != *settings::TMP_DIR.rbegin())  settings::TMP_DIR += '/';
				break;
			case 'S':
				settings::NNTP_recv_tmout = atoi(optarg);
				if (settings::NNTP_recv_tmout <= 0 ) settings::NNTP_recv_tmout = 0;
				break;
			case 'r':
				settings::USE_CONN_POOL = false;
				break;
			case 'n':
				settings::USE_LIB_NOTIFY = false;
				break;
			case 'Y':
				settings::NNTP_strict_yenc = true;
				break;
			case 'h':
				print_help();
				exit(0);
				break;
			case '?':
             			if (strchr("supctoxlTS", optopt)) {
					std::cerr << "Option -" << (char)optopt << " requires an argument" << std::endl;
					print_help();
					exit(1);
				} else if (isprint (optopt)) {
					std::cerr << "Option -" << (char)optopt << " is unknown" << std::endl;
					print_help();
					exit(1);
				}
				break;
			default:
				std::cerr << "Invalid option: " << c << std::endl;
				print_help();
				exit(1);
				break;
		}
	}
	return optind;
}

void check_basic(void) {
	if (settings::NNTP_server.length() == 0) throw std::runtime_error("NNTP server not specified");
	//
	struct stat64 s;
	//
	if (stat64(settings::OUT_DIR.c_str(), &s)) throw std::runtime_error("Output directory isn't valid");
	if (!S_ISDIR(s.st_mode) || !IS_F_RWX(s.st_mode, s.st_uid, s.st_gid)) throw std::runtime_error("Output directory isn't a directory or can't be R/W by current user");
	//
	if (stat64(settings::TMP_DIR.c_str(), &s)) throw std::runtime_error("Temporary directory isn't valid");
	if (!S_ISDIR(s.st_mode) || !IS_F_RWX(s.st_mode, s.st_uid, s.st_gid)) throw std::runtime_error("Temporary directory isn't a directory or can't be R/W by current user");
	//
	if (stat64(settings::YYDECODE_PATH.c_str(), &s)) throw std::runtime_error("yydecode path isn't valid");
	if (!S_ISREG(s.st_mode) || !IS_F_RX(s.st_mode, s.st_uid, s.st_gid)) throw std::runtime_error("yydecode isn't valid or can't be R/X by current user");
}

int main(int argc, char *argv[]) {
	try {
		const int f_arg = parse_options(argc, argv);
		// init connections, notify and signals
		Connection::Init();
		notify::init();
		sig::init();
		// check that basic paths/config are ok
		check_basic();
		// for each .nzb proceed
		for(int i = f_arg; i < argc; ++i) {
			bool			is_ok=true;
			std::vector<NZB_file>	vec_NZB_file;

			LOG_INFO << "Processing NZB [" << argv[i] << "] ..." << std::endl;
			notify::message(__myNZB__ + " - Processing NZB ...", argv[i]);
			get_NZB_content(argv[i], vec_NZB_file, settings::OUT_DIR);
			int		idx = 1;
			const int	total = vec_NZB_file.size();
			for(std::vector<NZB_file>::const_iterator it = vec_NZB_file.begin(); it != vec_NZB_file.end(); ++it, ++idx) {
				const std::string curr_part = XtoS(idx) + "/" + XtoS(total);

				LOG_INFO << "Downloading (" << it->seg_name << ") [" << curr_part << "] ..." << std::endl;
				notify::message(__myNZB__ + " - Downloading [" + curr_part + "] ...", it->seg_name);
				if(fetch_nzb_file(it->group, it->segments)) {
					LOG_INFO << "Done (" << it->seg_name << ") [" << curr_part << "]" << std::endl;
					notify::message(__myNZB__ + " - Ok [" + curr_part + "]", it->seg_name);
				} else {
					LOG_ERROR << "Error (" << it->seg_name << ") [" << curr_part << "]" << std::endl;
					notify::message(__myNZB__ + " - Error [" + curr_part + "]", it->seg_name);
					is_ok=false;
				}
			}
			if (is_ok) {
				LOG_INFO << "NZB completed, all ok" << std::endl;
				notify::message(__myNZB__ + " - NZB completed", argv[i]);
			} else {
				LOG_WARNING << "NZB completed, some parts have to be re downloaded" << std::endl;
				notify::message(__myNZB__ + " - NZB completed with errors", argv[i]);
			}
		}
		// cleanup connections
		Connection::Cleanup();
		return 0;
	} catch(sig::except& e) {
		LOG_INFO << e.what() << std::endl;
	} catch(std::exception& e) {
		LOG_ERROR << e.what() << std::endl;
	} catch(...) {
		LOG_ERROR << "Unknonw exception" << std::endl;
	}
	return -1;
}
