#define MODULE_LOG_PREFIX "work"

#include "globals.h"
#include "module-cacheex.h"
#include "oscam-client.h"
#include "oscam-ecm.h"
#include "oscam-emm.h"
#include "oscam-lock.h"
#include "oscam-net.h"
#include "oscam-reader.h"
#include "oscam-string.h"
#include "oscam-work.h"
#include "reader-common.h"
#include "module-cccam.h"
#include "module-cccam-data.h"
#include "module-cccshare.h"
#include "oscam-time.h"

extern CS_MUTEX_LOCK system_lock;
extern int32_t thread_pipe[2];

struct job_data
{
	enum actions action;
	struct s_reader *rdr;
	struct s_client *cl;
	void *ptr;
	struct timeb time;
	uint16_t len;
};

static void free_job_data(struct job_data *data)
{
	if(!data)
		{ return; }
	if(data->len && data->ptr)
	{
		// special free checks
		if(data->action==ACTION_ECM_ANSWER_CACHE)
		{
			NULLFREE(((struct s_write_from_cache *)data->ptr)->er_cache);
		}

		NULLFREE(data->ptr);
	}
	NULLFREE(data);
}

void free_joblist(struct s_client *cl)
{
	int32_t lock_status = pthread_mutex_trylock(&cl->thread_lock);
	LL_ITER it = ll_iter_create(cl->joblist);

	struct job_data *data;
	while((data = ll_iter_next(&it)))
	{
		free_job_data(data);
	}

	ll_destroy(&cl->joblist);
	cl->account = NULL;

	if(cl->work_job_data) // Free job_data that was not freed by work_thread
		{ free_job_data(cl->work_job_data); }

	cl->work_job_data = NULL;

	if(lock_status == 0)
		{ SAFE_MUTEX_UNLOCK(&cl->thread_lock); }

	pthread_mutex_destroy(&cl->thread_lock);
}

/*
 Work threads are named like this:
   w[r|c]XX-[rdr->label|client->username]

   w      - work thread prefix
   [r|c]  - depending whether the the action is related to reader or client
   XX     - two digit action code from enum actions
   label  - reader label or client username (see username() function)
*/
static void set_work_thread_name(struct job_data *data)
{
	char thread_name[16 + 1];
	snprintf(thread_name, sizeof(thread_name), "w%c%02d-%s",
			 data->action < ACTION_CLIENT_FIRST ? 'r' : 'c',
			 data->action,
			 username(data->cl)
			);
	set_thread_name(thread_name);
}

#define __free_job_data(client, job_data) \
	do { \
		client->work_job_data = NULL; \
		if(job_data && job_data != &tmp_data) { \
			free_job_data(job_data); \
		} \
		job_data = NULL; \
	} while(0)

void *work_thread(void *ptr)
{
	struct job_data *data = (struct job_data *)ptr;
	struct s_client *cl = data->cl;
	struct s_reader *reader = cl->reader;
	struct timeb start, end; // start time poll, end time poll

	struct job_data tmp_data;
	struct pollfd pfd[1];

	SAFE_SETSPECIFIC(getclient, cl);
	cl->thread = pthread_self();
	cl->thread_active = 1;

	set_work_thread_name(data);

	struct s_module *module = get_module(cl);
	uint16_t bufsize = module->bufsize; // CCCam needs more than 1024bytes!
	if(!bufsize)
		{ bufsize = DEFAULT_MODULE_BUFSIZE; }

	uint8_t *mbuf;
	if(!cs_malloc(&mbuf, bufsize))
		{ return NULL; }

	cl->work_mbuf = mbuf; // Track locally allocated data, because some callback may call cs_exit/cs_disconect_client/pthread_exit and then mbuf would be leaked
	int32_t n = 0, rc = 0, i, idx, s, dblvl;
	(void)dblvl;
	uint8_t dcw[16];
	int8_t restart_reader = 0;

	while(cl->thread_active)
	{
		cs_ftime(&start); // register start time

		while(cl->thread_active)
		{
			if(!cl || cl->kill || !is_valid_client(cl))
			{
				SAFE_MUTEX_LOCK(&cl->thread_lock);
				cl->thread_active = 0;
				SAFE_MUTEX_UNLOCK(&cl->thread_lock);
				cs_log_dbg(D_TRACE, "ending thread (kill)");
				__free_job_data(cl, data);
				cl->work_mbuf = NULL; // Prevent free_client from freeing mbuf (->work_mbuf)
				free_client(cl);
				if(restart_reader)
					{ restart_cardreader(reader, 0); }
				NULLFREE(mbuf);
				pthread_exit(NULL);
				return NULL;
			}

			if(data && data->action != ACTION_READER_CHECK_HEALTH)
				{ cs_log_dbg(D_TRACE, "data from add_job action=%d client %c %s", data->action, cl->typ, username(cl)); }

			if(!data)
			{
				if(!cl->kill && cl->typ != 'r')
					{ client_check_status(cl); } // do not call for physical readers as this might cause an endless job loop

				SAFE_MUTEX_LOCK(&cl->thread_lock);
				if(cl->joblist && ll_count(cl->joblist) > 0)
				{
					LL_ITER itr = ll_iter_create(cl->joblist);
					data = ll_iter_next_remove(&itr);
					if(data)
						{ set_work_thread_name(data); }
					//cs_log_dbg(D_TRACE, "start next job from list action=%d", data->action);
				}
				SAFE_MUTEX_UNLOCK(&cl->thread_lock);
			}

			if(!data)
			{
				/* for serial client cl->pfd is file descriptor for serial port not socket
				   for example: pfd=open("/dev/ttyUSB0"); */
				if(!cl->pfd || module->listenertype == LIS_SERIAL)
					{ break; }

				pfd[0].fd = cl->pfd;
				pfd[0].events = POLLIN | POLLPRI;

				SAFE_MUTEX_LOCK(&cl->thread_lock);
				cl->thread_active = 2;
				SAFE_MUTEX_UNLOCK(&cl->thread_lock);

				rc = poll(pfd, 1, 3000);

				SAFE_MUTEX_LOCK(&cl->thread_lock);
				cl->thread_active = 1;
				SAFE_MUTEX_UNLOCK(&cl->thread_lock);

				if(rc > 0)
				{
					cs_ftime(&end); // register end time
					cs_log_dbg(D_TRACE, "[OSCAM-WORK] new event %d occurred on fd %d after %"PRId64" ms inactivity", pfd[0].revents,
								pfd[0].fd, comp_timeb(&end, &start));
					data = &tmp_data;
					data->ptr = NULL;
					cs_ftime(&start); // register start time for new poll next run

					if(reader)
						{ data->action = ACTION_READER_REMOTE; }
					else
					{
						if(cl->is_udp)
						{
							data->action = ACTION_CLIENT_UDP;
							data->ptr = mbuf;
							data->len = bufsize;
						}
						else
							{ data->action = ACTION_CLIENT_TCP; }

						if(pfd[0].revents & (POLLHUP | POLLNVAL | POLLERR))
							{ cl->kill = 1; }
					}
				}
			}

			if(!data)
				{ continue; }

			if(!reader && data->action < ACTION_CLIENT_FIRST)
			{
				__free_job_data(cl, data);
				break;
			}

			if(!data->action)
				{ break; }

			struct timeb actualtime;
			cs_ftime(&actualtime);
			int64_t gone = comp_timeb(&actualtime, &data->time);
			if(data != &tmp_data && gone > (int) cfg.ctimeout+1000)
			{
				cs_log_dbg(D_TRACE, "dropping client data for %s time %"PRId64" ms", username(cl), gone);
				__free_job_data(cl, data);
				continue;
			}

			if(data != &tmp_data)
				{ cl->work_job_data = data; } // Track the current job_data

			switch(data->action)
			{
				case ACTION_READER_IDLE:
					reader_do_idle(reader);
					break;

				case ACTION_READER_REMOTE:
					s = check_fd_for_data(cl->pfd);
					if(s == 0) // no data, another thread already read from fd?
						{ break; }
					if(s < 0)
					{
						if(cl->reader->ph.type == MOD_CONN_TCP)
							{ network_tcp_connection_close(reader, "disconnect"); }
						break;
					}
					rc = cl->reader->ph.recv(cl, mbuf, bufsize);
					if(rc < 0)
					{
						if(cl->reader->ph.type == MOD_CONN_TCP)
							{
								network_tcp_connection_close(reader, "disconnect on receive");
#ifdef CS_CACHEEX_AIO
								cl->cacheex_aio_checked = 0;
#endif
							}
						break;
					}
					cl->last = time(NULL); // *********************************** TO BE REPLACE BY CS_FTIME() LATER ****************
					idx = cl->reader->ph.c_recv_chk(cl, dcw, &rc, mbuf, rc);
					if(idx < 0) { break; }  // no dcw received
					if(!idx) { idx = cl->last_idx; }
					cl->reader->last_g = time(NULL); // *********************************** TO BE REPLACE BY CS_FTIME() LATER **************** // for reconnect timeout
					for(i = 0, n = 0; i < cfg.max_pending && n == 0; i++)
					{
						if(cl->ecmtask[i].idx == idx)
						{
							cl->pending--;
							casc_check_dcw(reader, i, rc, dcw);
							n++;
						}
					}
					break;

				case ACTION_READER_RESET:
					cardreader_do_reset(reader);
					break;

				case ACTION_READER_ECM_REQUEST:
					reader_get_ecm(reader, data->ptr);
					break;

				case ACTION_READER_EMM:
					reader_do_emm(reader, data->ptr);
					break;

				case ACTION_READER_SENDCMD:
#ifdef WITH_CARDREADER
					dblvl = cs_dblevel;
					cs_dblevel = dblvl | D_READER;
					rc = cardreader_do_rawcmd(reader, data->ptr);
					cs_log_dbg(D_TRACE, "sendcmd rc: %i, csystem: %s", rc, reader->csystem->desc);
					if(rc == -9)
					{
						CMD_PACKET *cp = data->ptr;
						uint8_t response[MAX_CMD_SIZE];
						memset(response, 0, sizeof(response));
						uint16_t response_length[1] = { 0 };
						rc = reader_cmd2icc(reader, cp->cmd, cp->cmdlen, response, response_length);
						cs_log_dbg(D_TRACE, "sendcmd rc: %i, len: %i", rc, *response_length);
						if (*response_length)
						{
							cs_log_dump_dbg(D_TRACE, response, *response_length, "sendcmd response:");
						}
					}
					cs_dblevel = dblvl;
#endif
					break;

				case ACTION_READER_CARDINFO:
					reader_do_card_info(reader);
					break;

				case ACTION_READER_POLL_STATUS:
#ifdef READER_VIDEOGUARD
					cardreader_poll_status(reader);
#endif
					break;

				case ACTION_READER_INIT:
					if(!cl->init_done)
						{ reader_init(reader); }
					break;

				case ACTION_READER_RESTART:
					cl->kill = 1;
					restart_reader = 1;
					break;

				case ACTION_READER_RESET_FAST:
					cl->reader->card_status = CARD_NEED_INIT;
					cardreader_do_reset(reader);
					break;

				case ACTION_READER_CHECK_HEALTH:
					cardreader_do_checkhealth(reader);
					break;

				case ACTION_READER_CAPMT_NOTIFY:
					if(cl->reader->ph.c_capmt) { cl->reader->ph.c_capmt(cl, data->ptr); }
					break;

				case ACTION_CLIENT_UDP:
					n = module->recv(cl, data->ptr, data->len);
					if(n < 0) { break; }
					module->s_handler(cl, data->ptr, n);
					break;

				case ACTION_CLIENT_TCP:
					s = check_fd_for_data(cl->pfd);
					if(s == 0) // no data, another thread already read from fd?
						{ break; }
					if(s < 0) // system error or fd wants to be closed
					{
						cl->kill = 1; // kill client on next run
						continue;
					}
					n = module->recv(cl, mbuf, bufsize);
					if(n < 0)
					{
						cl->kill = 1; // kill client on next run
						continue;
					}
					module->s_handler(cl, mbuf, n);
					break;

				case ACTION_CACHEEX1_DELAY:
					cacheex_mode1_delay(data->ptr);
					break;

				case ACTION_CACHEEX_TIMEOUT:
					cacheex_timeout(data->ptr);
					break;

				case ACTION_FALLBACK_TIMEOUT:
					fallback_timeout(data->ptr);
					break;

				case ACTION_CLIENT_TIMEOUT:
					ecm_timeout(data->ptr);
					break;

				case ACTION_ECM_ANSWER_READER:
					chk_dcw(data->ptr);
					break;

				case ACTION_ECM_ANSWER_CACHE:
					write_ecm_answer_fromcache(data->ptr);
					break;

				case ACTION_CLIENT_INIT:
					if(module->s_init)
						{ module->s_init(cl); }
					cl->is_udp = module->type == MOD_CONN_UDP;
					cl->init_done = 1;
					break;

				case ACTION_CLIENT_IDLE:
					if(module->s_idle)
						{ module->s_idle(cl); }
					else
					{
						cs_log("user %s reached %d sec idle limit.", username(cl), cfg.cmaxidle);
						cl->kill = 1;
					}
					break;

				case ACTION_CACHE_PUSH_OUT:
					cacheex_push_out(cl, data->ptr);
					break;

				case ACTION_CLIENT_KILL:
					cl->kill = 1;
					break;

				case ACTION_CLIENT_SEND_MSG:
				{
					if (config_enabled(MODULE_CCCAM))
					{
						struct s_clientmsg *clientmsg = (struct s_clientmsg *)data->ptr;
						cc_cmd_send(cl, clientmsg->msg, clientmsg->len, clientmsg->cmd);
					}
					break;
				}

				case ACTION_PEER_IDLE:
					if(module->s_peer_idle)
						{ module->s_peer_idle(cl); }
					break;

				case ACTION_CLIENT_HIDECARDS:
				{
#ifdef CS_ANTICASC
					if(config_enabled(MODULE_CCCSHARE))
					{
						int32_t hidetime = (cl->account->acosc_penalty_duration == -1 ? cfg.acosc_penalty_duration : cl->account->acosc_penalty_duration);
						if(hidetime)
						{
							int32_t hide_count;
							int32_t cardsize;
							int32_t ii, uu=0;
							LLIST **sharelist = get_and_lock_sharelist();
							LLIST *sharelist2 = ll_create("hidecards-sharelist");

							for(ii = 0; ii < CAID_KEY; ii++)
							{
								if(sharelist[ii])
								{
									ll_putall(sharelist2, sharelist[ii]);
								}
							}

							unlock_sharelist();

							struct cc_card **cardarray = get_sorted_card_copy(sharelist2, 0, &cardsize);
							ll_destroy(&sharelist2);

							for(ii = 0; ii < cardsize; ii++)
							{
								if(hidecards_card_valid_for_client(cl, cardarray[ii]))
								{
									if (cardarray[ii]->id)
									{
										hide_count = hide_card_to_client(cardarray[ii], cl);
										if(hide_count)
										{
											cs_log_dbg(D_TRACE, "Hiding card_%d caid=%04x remoteid=%08x from %s for %d %s",
												 uu, cardarray[ii]->caid, cardarray[ii]->remote_id, username(cl), hidetime, hidetime>1 ? "secconds" : "seccond");
											uu += 1;
										}
									}
								}
							}

							cs_sleepms(hidetime * 1000);
							uu = 0;

							for(ii = 0; ii < cardsize; ii++)
							{
								if(hidecards_card_valid_for_client(cl, cardarray[ii]))
								{
									if (cardarray[ii]->id)
									{
										hide_count = unhide_card_to_client(cardarray[ii], cl);
										if(hide_count)
										{
											cs_log_dbg(D_TRACE, "Unhiding card_%d caid=%04x remoteid=%08x for %s",
											 uu, cardarray[ii]->caid, cardarray[ii]->remote_id, username(cl));
											uu += 1;
										}
									}
								}
							}

							NULLFREE(cardarray);
						}
					}
#endif
					break;
				} // case ACTION_CLIENT_HIDECARDS

			} // switch

			__free_job_data(cl, data);
		}

		if(thread_pipe[1] && (mbuf[0] != 0x00))
		{
			cs_log_dump_dbg(D_TRACE, mbuf, 1, "[OSCAM-WORK] Write to pipe:");
			if(write(thread_pipe[1], mbuf, 1) == -1)    // wakeup client check
			{
				cs_log_dbg(D_TRACE, "[OSCAM-WORK] Writing to pipe failed (errno=%d %s)", errno, strerror(errno));
			}
		}

		// Check for some race condition where while we ended, another thread added a job
		SAFE_MUTEX_LOCK(&cl->thread_lock);
		if(cl->joblist && ll_count(cl->joblist) > 0)
		{
			SAFE_MUTEX_UNLOCK(&cl->thread_lock);
			continue;
		}
		else
		{
			cl->thread_active = 0;
			SAFE_MUTEX_UNLOCK(&cl->thread_lock);
			break;
		}
	}
	cl->thread_active = 0;
	cl->work_mbuf = NULL; // Prevent free_client from freeing mbuf (->work_mbuf)
	NULLFREE(mbuf);
	pthread_exit(NULL);
	return NULL;
}

/**
 * adds a job to the job queue
 * if ptr should be free() after use, set len to the size
 * else set size to 0
**/
int32_t add_job(struct s_client *cl, enum actions action, void *ptr, int32_t len)
{
	if(!cl || cl->kill)
	{
		if(!cl)
			{ cs_log("WARNING: add_job failed. Client killed!"); } // Ignore jobs for killed clients
		if(len && ptr)
			{ NULLFREE(ptr); }
		return 0;
	}

	if(action == ACTION_CACHE_PUSH_OUT && cacheex_check_queue_length(cl))
	{
		if(len && ptr)
			{ NULLFREE(ptr); }
		return 0;
	}

	struct job_data *data;
	if(!cs_malloc(&data, sizeof(struct job_data)))
	{
		if(len && ptr)
			{ NULLFREE(ptr); }
		return 0;
	}

	data->action = action;
	data->ptr = ptr;
	data->cl = cl;
	data->len = len;
	cs_ftime(&data->time);

	SAFE_MUTEX_LOCK(&cl->thread_lock);
	if(cl && !cl->kill && cl->thread_active)
	{
		if(!cl->joblist)
			{ cl->joblist = ll_create("joblist"); }
		ll_append(cl->joblist, data);
		if(cl->thread_active == 2)
			{ pthread_kill(cl->thread, OSCAM_SIGNAL_WAKEUP); }
		SAFE_MUTEX_UNLOCK(&cl->thread_lock);
		cs_log_dbg(D_TRACE, "add %s job action %d queue length %d %s",
					action > ACTION_CLIENT_FIRST ? "client" : "reader", action,
					ll_count(cl->joblist), username(cl));
		return 1;
	}

	/* pcsc doesn't like this; segfaults on x86, x86_64 */
	int8_t modify_stacksize = 0;
	struct s_reader *rdr = cl->reader;
	if(cl->typ != 'r' || !rdr || rdr->typ != R_PCSC)
		{ modify_stacksize = 1; }

	if(action != ACTION_READER_CHECK_HEALTH)
	{
		cs_log_dbg(D_TRACE, "start %s thread action %d",
					action > ACTION_CLIENT_FIRST ? "client" : "reader", action);
	}

	int32_t ret = start_thread("client work", work_thread, (void *)data, &cl->thread, 1, modify_stacksize);
	if(ret)
	{
		cs_log("ERROR: can't create thread for %s (errno=%d %s)",
				action > ACTION_CLIENT_FIRST ? "client" : "reader", ret, strerror(ret));
		free_job_data(data);
	}

	cl->thread_active = 1;
	SAFE_MUTEX_UNLOCK(&cl->thread_lock);
	return 1;
}
