#define MODULE_LOG_PREFIX "cache"

#include "globals.h"
#include "module-cacheex.h"
#include "module-cw-cycle-check.h"
#include "oscam-cache.h"
#include "oscam-chk.h"
#include "oscam-client.h"
#include "oscam-ecm.h"
#include "oscam-garbage.h"
#include "oscam-lock.h"
#include "oscam-net.h"
#include "oscam-string.h"
#include "oscam-time.h"
#include "oscam-hashtable.h"
#ifdef CS_CACHEEX_AIO
#include "oscam-log.h"
#endif


// CACHE functions **************************************************************+
struct s_pushclient
{
	struct s_client *cl;
	struct s_pushclient *next_push;
};

typedef struct cw_t
{
	uint8_t             cw[16];
	uint8_t             odd_even;            // odd/even byte (0x80 0x81)
	uint8_t             cwc_cycletime;
	uint8_t             cwc_next_cw_cycle;
	uint8_t             got_bad_cwc;         // used by cycle check
	uint16_t            caid;                // first caid received
	uint32_t            prid;                // first prid received
	uint16_t            srvid;               // first srvid received
	struct s_reader     *selected_reader;    // first answering: reader
	struct s_client     *cacheex_src;        // first answering: cacheex client
	uint64_t            grp;                 // updated grp
	uint8_t             csp;                 // updated if answer from csp
	uint8_t             cacheex;             // updated if answer from cacheex
	uint8_t             localcards;          // updated if answer from local cards (or proxy using localcards option)
	uint8_t             proxy;               // updated if answer from local reader
	uint32_t            count;               // count of same cws receved
#ifdef CS_CACHEEX_AIO
	uint8_t				localgenerated;      // flag for local generated CWs
#endif
	// for push out
	pthread_rwlock_t    pushout_client_lock;
	struct s_pushclient *pushout_client;     // list of clients that pushing cw
	// end push out
	node                ht_node;             // node for hash table
	node                ll_node;             // node for linked list
} CW;

typedef struct cache_t
{
	hash_table          ht_cw;
	list                ll_cw;
	struct timeb        upd_time;            // updated time. Update time at each cw got
	struct timeb        first_recv_time;     // time of first cw received
	uint32_t            csp_hash;
	node                ht_node;             // node for hash table
	node                ll_node;             // node for linked list
} ECMHASH;

#ifdef CS_CACHEEX_AIO
typedef struct cw_cache_t
{
	uint8_t             cw[16];
	uint16_t            caid;
	uint32_t            prid;
	uint16_t            srvid;
	struct timeb        first_recv_time;     // time of first cw received
	struct timeb        upd_time;            // updated time. Update time at each cw got
	node				ht_node;
	node				ll_node;
} CW_CACHE;

typedef struct cw_cache_setting_t
{
	int8_t			mode;
	uint16_t		timediff_old_cw;
} CW_CACHE_SETTING;
#endif

static pthread_rwlock_t cache_lock;
#ifdef CS_CACHEEX_AIO
static pthread_rwlock_t cw_cache_lock;
#endif
static hash_table ht_cache;
#ifdef CS_CACHEEX_AIO
static hash_table ht_cw_cache;
#endif
static list ll_cache;
#ifdef CS_CACHEEX_AIO
static list ll_cw_cache;
#endif
static int8_t cache_init_done = 0;

#ifdef CS_CACHEEX_AIO
static int8_t cw_cache_init_done = 0;
static uint32_t lg_cache_size = 0;

void init_cw_cache(void)
{
#ifdef CS_CACHEEX
	if(cfg.cw_cache_size > 0 || cfg.cw_cache_memory > 0)
	{
		init_hash_table(&ht_cw_cache, &ll_cw_cache);
		if (pthread_rwlock_init(&cw_cache_lock,NULL) != 0)
			{ cs_log("Error creating lock cw_cache_lock!"); }
		else
			{ cw_cache_init_done = 1; }
	}
#endif
}
#endif

void init_cache(void)
{
	init_hash_table(&ht_cache, &ll_cache);
	if (pthread_rwlock_init(&cache_lock,NULL) != 0)
		{ cs_log("Error creating lock cache_lock!"); }
	else
		{ cache_init_done = 1; }
}

void free_cache(void)
{
	cleanup_cache(true);
#ifdef CS_CACHEEX_AIO
	cw_cache_cleanup(true);
	ecm_cache_cleanup(true);
	cw_cache_init_done = 0;
	deinitialize_hash_table(&ht_cw_cache);
	pthread_rwlock_destroy(&cw_cache_lock);
#endif
	cache_init_done = 0;
	deinitialize_hash_table(&ht_cache);
	pthread_rwlock_destroy(&cache_lock);
}

#ifdef CS_CACHEEX_AIO
uint32_t cache_size_lg(void)
{
	if(!cache_init_done)
		{ return 0; }

	return lg_cache_size;
}
#endif

uint32_t cache_size(void)
{
	if(!cache_init_done)
		{ return 0; }

	return count_hash_table(&ht_cache);
}

static uint8_t count_sort(CW *a, CW *b)
{
	if (a->count == b->count) return 0;
	return (a->count > b->count) ? -1 : 1; // DESC order by count
}

#ifdef CS_CACHEEX_AIO
static uint8_t time_sort(CW_CACHE *a, CW_CACHE *b)
{
	if (((int64_t)(a->upd_time.time) * 1000ull + (int64_t) a->upd_time.millitm) == ((int64_t)(b->upd_time.time) * 1000ull + (int64_t) b->upd_time.millitm)) return 0;
	return (((int64_t)(a->upd_time.time) * 1000ull + (int64_t) a->upd_time.millitm) > ((int64_t)(b->upd_time.time) * 1000ull + (int64_t) b->upd_time.millitm)) ? -1 : 1;
}
#endif

uint8_t check_is_pushed(void *cwp, struct s_client *cl)
{
	struct s_pushclient *cl_tmp;
	CW* cw = (CW*)cwp;
	bool pushed=false;

	SAFE_RWLOCK_RDLOCK(&cw->pushout_client_lock);
	for (cl_tmp = cw->pushout_client; cl_tmp; cl_tmp = cl_tmp->next_push)
	{
		if(cl_tmp->cl==cl)
		{
			pushed=true;
			break;
		}
	}

	if(!pushed)
	{
		SAFE_RWLOCK_UNLOCK(&cw->pushout_client_lock);
		SAFE_RWLOCK_WRLOCK(&cw->pushout_client_lock);

		struct s_pushclient *new_push_client;
		if(cs_malloc(&new_push_client, sizeof(struct s_pushclient)))
		{
			new_push_client->cl=cl;

			new_push_client->next_push=cw->pushout_client;
			cw->pushout_client=new_push_client;
		}

		SAFE_RWLOCK_UNLOCK(&cw->pushout_client_lock);
		return 0;
	}
	else
	{
		SAFE_RWLOCK_UNLOCK(&cw->pushout_client_lock);
		return 1;
	}
}

uint8_t get_odd_even(ECM_REQUEST *er)
{
	return (er->ecm[0] != 0x80 && er->ecm[0] != 0x81 ? 0 : er->ecm[0]);
}


CW *get_first_cw(ECMHASH *ecmhash, ECM_REQUEST *er)
{
	if(!ecmhash) return NULL;

	node *j;
	CW *cw;

	j = get_first_node_list(&ecmhash->ll_cw);
	while (j) {
		cw = get_data_from_node(j);

		if(cw && cw->odd_even == get_odd_even(er) && !cw->got_bad_cwc)
			return cw;

		j = j->next;
	}

	return NULL;
}

int compare_csp_hash(const void *arg, const void *obj)
{
	uint32_t h = ((const ECMHASH*)obj)->csp_hash;
	return memcmp(arg, &h, 4);
}

static int compare_cw(const void *arg, const void *obj)
{
	return memcmp(arg, ((const CW*)obj)->cw, 16);
}

#ifdef CS_CACHEEX_AIO
static int compare_cw_cache(const void *arg, const void *obj)
{
	return memcmp(arg, ((const CW_CACHE*)obj)->cw, 16);
}
#endif

static bool cwcycle_check_cache(struct s_client *cl, ECM_REQUEST *er, CW *cw)
{
	(void)cl; (void)er; (void)cw;

#ifdef CW_CYCLE_CHECK
	if(cw->got_bad_cwc)
		return 0;

	uint8_t cwc_ct   = cw->cwc_cycletime > 0 ? cw->cwc_cycletime : 0;
	uint8_t cwc_ncwc = cw->cwc_next_cw_cycle < 2 ? cw->cwc_next_cw_cycle : 2;
	if(checkcwcycle(cl, er, NULL, cw->cw, 0, cwc_ct, cwc_ncwc) != 0)
	{
		cs_log_dbg(D_CWC | D_LB, "{client %s, caid %04X, srvid %04X} [check_cache] cyclecheck passed ecm in INT. cache.", (cl ? cl->account->usr : "-"), er->caid, er->srvid);
	}
	else
	{
#ifdef CS_CACHEEX_AIO
		if(!er->localgenerated)
		{
#endif
			cs_log_dbg(D_CWC, "cyclecheck [BAD CW Cycle] from Int. Cache detected.. {client %s, caid %04X, srvid %04X} [check_cache] -> skip cache answer", (cl ? cl->account->usr : "-"), er->caid, er->srvid);
			cw->got_bad_cwc = 1; // no need to check it again
			return 0;
#ifdef CS_CACHEEX_AIO
		}
		else
		{
			cs_log_dbg(D_CWC, "cyclecheck [BAD CW Cycle] from Int. Cache detected.. {client %s, caid %04X, srvid %04X} [check_cache] -> lg-flagged CW -> do nothing", (cl ? cl->account->usr : "-"), er->caid, er->srvid);
		}
#endif
	}
#endif
	return 1;
}

/*
 * This function returns cw (mostly received) in cache for er, or NULL if not found.
 * IMPORTANT:
 * 		- If found, DON'T forget to free returned ecm, because it is a copy useful to get data
 * 		- If found, and cacheex_src client of returned ecm is not NULL, and we want to access it,
 *        remember to check for its validity (client structure is still existent)
 *        E.g.: if(ecm->cacheex_src && is_valid_client(ecm->cacheex_src) && !ecm->cacheex_src->kill)
 *        We don't want make this stuff here to avoid useless cpu time if outside function we would not access to it.
 */
struct ecm_request_t *check_cache(ECM_REQUEST *er, struct s_client *cl)
{
	if(!cache_init_done || !er->csp_hash) return NULL;

	ECM_REQUEST *ecm = NULL;
	ECMHASH *result;
	CW *cw;
	uint64_t grp = cl?cl->grp:0;

	SAFE_RWLOCK_RDLOCK(&cache_lock);

	result = find_hash_table(&ht_cache, &er->csp_hash, sizeof(uint32_t),&compare_csp_hash);
	cw = get_first_cw(result, er);
	if (!cw)
		goto out_err;

	if(cw->csp // csp have no grp!
		|| !grp // csp client(no grp) searching for cache
		|| (grp && cw->grp // ecm group --> only when readers/ex-clients answer (e_found) it
		&& (grp & cw->grp)))
	{
#ifdef CS_CACHEEX
		//if preferlocalcards=2 for this ecm request, we can server ONLY cw from localcards readers until stage<3
		if(er->preferlocalcards==2 && !cw->localcards && er->stage<3){
			goto out_err;
		}

		CWCHECK check_cw = get_cwcheck(er);

		if((!cw->proxy && !cw->localcards) // cw received from ONLY cacheex/csp peers
			&& check_cw.counter>1
			&& cw->count < check_cw.counter
			&& (check_cw.mode == 1 || !er->cacheex_wait_time_expired))
		{
			goto out_err;
		}

#ifdef CS_CACHEEX_AIO
		// client
		if( cl && !cw->localgenerated
			&& !(chk_srvid_localgenerated_only_exception(er)) // service-based exception
			&& (cl->account->cacheex.localgenerated_only
				|| (chk_lg_only(er, &cl->account->cacheex.lg_only_tab))
				) // only lg-flagged CWs
		)
		{
			goto out_err;
		}
#endif
#endif

		if (!cwcycle_check_cache(cl, er, cw))
			goto out_err;

		if (cs_malloc(&ecm, sizeof(ECM_REQUEST)))
		{
			ecm->rc = E_FOUND;
			ecm->rcEx = 0;
			memcpy(ecm->cw, cw->cw, 16);
			ecm->grp = cw->grp;
			ecm->selected_reader = cw->selected_reader;
			ecm->cwc_cycletime = cw->cwc_cycletime;
			ecm->cwc_next_cw_cycle = cw->cwc_next_cw_cycle;
			ecm->cacheex_src = cw->cacheex_src;
#ifdef CS_CACHEEX_AIO
			ecm->localgenerated = (cw->localgenerated) ? 1:0;
#endif
			ecm->cw_count = cw->count;
			ecm->from_cacheex = cw->cacheex; // Kopiowanie flagi cacheex
			ecm->from_csp = cw->csp;         // Kopiowanie flagi csp
		}
	}

out_err:
	SAFE_RWLOCK_UNLOCK(&cache_lock);
	return ecm;
}

#ifdef CS_CACHEEX_AIO
uint16_t get_cacheex_nopushafter(ECM_REQUEST *er)
{
	return caidvaluetab_get_value(&cfg.cacheex_nopushafter_tab, er->caid, 0);
}
#endif

static void cacheex_cache_add(ECM_REQUEST *er, ECMHASH *result, CW *cw, bool add_new_cw)
{
	(void)er; (void)result; (void)cw; (void)add_new_cw;
#ifdef CS_CACHEEX
	er->cw_cache = cw;
	cacheex_cache_push(er);

	// cacheex debug log lines and cw diff stuff
	if(!check_client(er->cacheex_src))
		return;

#ifdef CS_CACHEEX_AIO
	if (D_CACHEEX & cs_dblevel)
	{
		uint8_t remotenodeid[8];
		cacheex_get_srcnodeid(er, remotenodeid);

		if(!add_new_cw)
		{
			debug_ecm(D_CACHEEX| D_CSP, "got duplicate pushed ECM %s from %s - hop %i %s, src-nodeid %" PRIu64 "X", buf, er->from_csp ? "csp" : username(er->cacheex_src), ll_count(er->csp_lastnodes), er->localgenerated ? "(lg)" : "", cacheex_node_id(remotenodeid));
			return;
		}

		debug_ecm(D_CACHEEX|D_CSP, "got pushed ECM %s from %s - hop %i %s, src-nodeid %" PRIu64 "X", buf, er->from_csp ? "csp" : username(er->cacheex_src), ll_count(er->csp_lastnodes), er->localgenerated ? "(lg)" : "", cacheex_node_id(remotenodeid));
	}
	else
	{
#endif
		if(!add_new_cw)
		{
#ifdef CS_CACHEEX_AIO
			debug_ecm(D_CACHEEX| D_CSP, "got duplicate pushed ECM %s from %s - hop %i %s", buf, er->from_csp ? "csp" : username(er->cacheex_src), ll_count(er->csp_lastnodes), er->localgenerated ? "(lg)" : "");
#else
			debug_ecm(D_CACHEEX| D_CSP, "got duplicate pushed ECM %s from %s", buf, er->from_csp ? "csp" : username(er->cacheex_src));
#endif
			return;
		}

#ifdef CS_CACHEEX_AIO
		debug_ecm(D_CACHEEX|D_CSP, "got pushed ECM %s from %s - hop %i %s", buf, er->from_csp ? "csp" : username(er->cacheex_src), ll_count(er->csp_lastnodes), er->localgenerated ? "(lg)" : "");
#else
		debug_ecm(D_CACHEEX|D_CSP, "got pushed ECM %s from %s", buf, er->from_csp ? "csp" : username(er->cacheex_src));
#endif

#ifdef CS_CACHEEX_AIO
	}
#endif

	CW *cw_first = get_first_cw(result, er);
	if(!cw_first)
		return;

	// compare er cw with mostly counted cached cw
	if(memcmp(er->cw, cw_first->cw, sizeof(er->cw)) != 0)
	{
		er->cacheex_src->cwcacheexerrcw++;
		if (er->cacheex_src->account)
			er->cacheex_src->account->cwcacheexerrcw++;

		if (((0x0200| 0x0800) & cs_dblevel)) // avoid useless operations if debug is not enabled
		{
			char cw1[16*3+2], cw2[16*3+2];
			cs_hexdump(0, er->cw, 16, cw1, sizeof(cw1));
			cs_hexdump(0, cw_first->cw, 16, cw2, sizeof(cw2));

			char ip1[20]="", ip2[20]="";
			if (check_client(er->cacheex_src))
				cs_strncpy(ip1, cs_inet_ntoa(er->cacheex_src->ip), sizeof(ip1));
			if (check_client(cw_first->cacheex_src))
				cs_strncpy(ip2, cs_inet_ntoa(cw_first->cacheex_src->ip), sizeof(ip2));
			else if (cw_first->selected_reader && check_client(cw_first->selected_reader->client))
				cs_strncpy(ip2, cs_inet_ntoa(cw_first->selected_reader->client->ip), sizeof(ip2));

#ifdef CS_CACHEEX_AIO
			uint8_t remotenodeid[8];
			cacheex_get_srcnodeid(er, remotenodeid);

			uint8_t fakeF0 = 0, offset = 0;

			if(get_odd_even(er) == 0x81)
				offset = 8;

			if(
				(cw_first->cw[7+offset] != 0x00 && er->cw[7+offset] != 0x00)
				&& (cw_first->cw[7+offset] ^ 0xF0) == er->cw[7+offset]
			)
			{
				fakeF0 = 1;
			}

			debug_ecm(D_CACHEEX| D_CSP, "WARNING: Different CWs %s from %s(%s)<>%s(%s): %s<>%s lg: %i<>%i, hop:%02i, src-nodeid: %" PRIu64 "X%s", buf,
#else
			debug_ecm(D_CACHEEX| D_CSP, "WARNING: Different CWs %s from %s(%s)<>%s(%s): %s<>%s ", buf,
#endif
				er->from_csp ? "csp" : username(er->cacheex_src), ip1,
				check_client(cw_first->cacheex_src)?username(cw_first->cacheex_src):(cw_first->selected_reader?cw_first->selected_reader->label:"unknown/csp"), ip2,
#ifdef CS_CACHEEX_AIO
				cw1, cw2, er->localgenerated, cw_first->localgenerated, er->csp_lastnodes ? ll_count(er->csp_lastnodes) : 0, er->csp_lastnodes ? cacheex_node_id(remotenodeid): 0, fakeF0 ? " [last byte xor 0xF0]" : "");
#else
				cw1, cw2);
#endif
#ifdef WITH_DEBUG
			if(cs_dblevel & D_CACHEEX)
			{
				LL_LOCKITER *li = ll_li_create(er->csp_lastnodes, 0);
				uint8_t *nodeid;
				uint8_t hops = 0;
				while((nodeid = ll_li_next(li)))
				{
					cs_log_dbg(D_CACHEEX, "Different CW-nodelist hop%02u: %" PRIu64 "X", ++hops, cacheex_node_id(nodeid));
				}
				ll_li_destroy(li);
			}
#endif
		}
	}
#endif
}

#ifdef CS_CACHEEX_AIO
CW_CACHE_SETTING get_cw_cache(ECM_REQUEST *er)
{
	int32_t i, timediff_old_cw = 0;
	int8_t mode = 0;

	for(i = 0; i < cfg.cw_cache_settings.cwchecknum; i++)
	{
		CWCHECKTAB_DATA *d = &cfg.cw_cache_settings.cwcheckdata[i];

		if(i == 0 && d->caid <= 0)
		{
			mode = d->mode;
			timediff_old_cw = d->counter;
			continue; //check other, only valid for unset
		}

		if(d->caid == er->caid || d->caid == er->caid >> 8 || ((d->cmask >= 0 && (er->caid & d->cmask) == d->caid) || d->caid == -1))
		{
			if((d->prid >= 0 && d->prid == (int32_t)er->prid) || d->prid == -1)
			{
				if((d->srvid >= 0 && d->srvid == er->srvid) || d->srvid == -1)
				{
					mode = d->mode;
					timediff_old_cw = d->counter;
					break;
				}
			}
		}
	}

	//check for correct values
	if(mode>3 || mode<0) mode=0;
	if(timediff_old_cw<1) timediff_old_cw=0;

	CW_CACHE_SETTING cw_cache_setting;
	memset(&cw_cache_setting, 0, sizeof(CW_CACHE_SETTING));
	cw_cache_setting.mode = mode;
	cw_cache_setting.timediff_old_cw = timediff_old_cw;

	return cw_cache_setting;
}

static bool __attribute__((unused)) cw_cache_check(ECM_REQUEST *er)
{
	if(cw_cache_init_done)
	{
		CW_CACHE_SETTING cw_cache_setting = get_cw_cache(er);
		if(cw_cache_setting.mode > 0)
		{
			CW_CACHE *cw_cache = NULL;
			SAFE_RWLOCK_WRLOCK(&cw_cache_lock);
			cw_cache = find_hash_table(&ht_cw_cache, &er->cw, sizeof(er->cw), &compare_cw_cache);
			// add cw to ht_cw_cache if < cw_cache_size
			if(!cw_cache)
			{
				// cw_cache-size(count/memory) pre-check
				if(
					(cfg.cw_cache_size && (cfg.cw_cache_size > tommy_hashlin_count(&ht_cw_cache)))
					|| 	(cfg.cw_cache_memory && (cfg.cw_cache_memory*1024*1024 > (2 * tommy_hashlin_memory_usage(&ht_cw_cache))))
				)
				{
					if(cs_malloc(&cw_cache, sizeof(CW_CACHE)))
					{
						memcpy(cw_cache->cw, er->cw, sizeof(er->cw));
						cw_cache->caid = er->caid;
						cw_cache->prid = er->prid;
						cw_cache->srvid = er->srvid;
						cs_ftime(&cw_cache->first_recv_time);
						cs_ftime(&cw_cache->upd_time);

						tommy_hashlin_insert(&ht_cw_cache, &cw_cache->ht_node, cw_cache, tommy_hash_u32(0, &er->cw, sizeof(er->cw)));
						tommy_list_insert_tail(&ll_cw_cache, &cw_cache->ll_node, cw_cache);

						SAFE_RWLOCK_UNLOCK(&cw_cache_lock);
						return true;
					}
					else
					{
						SAFE_RWLOCK_UNLOCK(&cw_cache_lock);
						cs_log("[cw_cache] ERROR: NO added HASH to cw_cache!!");
						return false;
					}
				}
				else
				{
					// clean cache call;
					SAFE_RWLOCK_UNLOCK(&cw_cache_lock);
					cw_cache_cleanup(false);
					return false;
				}
			}
			// cw found
			else
			{
				char cw1[16*3+2];
				char cw2[16*3+2];
				int8_t drop_cw = 0;
				int64_t gone_diff = 0;

				gone_diff = comp_timeb(&er->tps, &cw_cache->first_recv_time);

				if(D_CW_CACHE & cs_dblevel)
				{
					cs_hexdump(0, cw_cache->cw, 16, cw1, sizeof(cw1));
					cs_hexdump(0, er->cw, 16, cw2, sizeof(cw2));
				}

				if(cw_cache_setting.timediff_old_cw > 0 && gone_diff > cw_cache_setting.timediff_old_cw) // late (>cw_cache_setting.timediff_old_cw) cw incoming
				{
					// log every dupe cw
					if(cs_dblevel & D_CW_CACHE)
					{
						uint8_t remotenodeid[8];
						cacheex_get_srcnodeid(er, remotenodeid);
						cs_log_dbg(D_CW_CACHE,"[dupe CW] cache: %04X:%06X:%04X:%s | in: %04X:%06X:%04X:%s | diff(now): %"PRIi64" ms > %"PRIu16" - %s - hop %i%s, src-nodeid %" PRIu64 "X", cw_cache->caid, cw_cache->prid, cw_cache->srvid, cw1, er->caid, er->prid, er->srvid, cw2, gone_diff, cw_cache_setting.timediff_old_cw, (er->selected_reader && cs_strlen(er->selected_reader->label)) ? er->selected_reader->label : username(er->cacheex_src), ll_count(er->csp_lastnodes), (er->localgenerated) ? " (lg)" : "", er->csp_lastnodes ? cacheex_node_id(remotenodeid): 0);
					}

					if(cw_cache->srvid == er->srvid && cw_cache->caid == er->caid) // same cw for same caid&srvid
					{
						cs_ftime(&cw_cache->upd_time);
						cs_log_dbg(D_CW_CACHE,"[late CW] cache: %04X:%06X:%04X:%s | in: %04X:%06X:%04X:%s | diff(now): %"PRIi64" ms > %"PRIu16" - %s - hop %i%s", cw_cache->caid, cw_cache->prid, cw_cache->srvid, cw1, er->caid, er->prid, er->srvid, cw2, gone_diff, cw_cache_setting.timediff_old_cw, (er->selected_reader && cs_strlen(er->selected_reader->label)) ? er->selected_reader->label : username(er->cacheex_src), ll_count(er->csp_lastnodes), (er->localgenerated) ? " (lg)" : "");
						drop_cw=1;

					}
					else if(cw_cache->srvid != er->srvid) // same cw for different srvid & late
					{
						cs_ftime(&cw_cache->upd_time);
						cs_log_dbg(D_CW_CACHE,"[dupe&late CW] cache: %04X:%06X:%04X:%s | in: %04X:%06X:%04X:%s| diff(now): %"PRIi64" ms - %s - hop %i%s", cw_cache->caid, cw_cache->prid, cw_cache->srvid, cw1, er->caid, er->prid, er->srvid, cw2, gone_diff, (er->selected_reader && cs_strlen(er->selected_reader->label)) ? er->selected_reader->label : username(er->cacheex_src), ll_count(er->csp_lastnodes), (er->localgenerated) ? " (lg)" : "");
						drop_cw = 1;
					}
					else if(gone_diff > 15000) // same cw later as 15 secs
					{
#ifdef WITH_DEBUG
						if(cs_dblevel & D_CW_CACHE)
						{
							uint8_t remotenodeid[8];
							cacheex_get_srcnodeid(er, remotenodeid);
							cs_log_dbg(D_CW_CACHE,"[late-15sec+ CW] cache: %04X:%06X:%04X:%s | in: %04X:%06X:%04X:%s | diff(now): %"PRIi64" ms > %"PRIu16" - %s - hop %i%s, src-nodeid %" PRIu64 "X", cw_cache->caid, cw_cache->prid, cw_cache->srvid, cw1, er->caid, er->prid, er->srvid, cw2, gone_diff, cw_cache_setting.timediff_old_cw, (er->selected_reader && cs_strlen(er->selected_reader->label)) ? er->selected_reader->label : username(er->cacheex_src), ll_count(er->csp_lastnodes), (er->localgenerated) ? " (lg)" : "", er->csp_lastnodes ? cacheex_node_id(remotenodeid): 0);
						}
#endif
						drop_cw = 1;
					}

					if(cw_cache_setting.mode > 1 && drop_cw)
					{
						// cw_cache->drop_count++;
						cs_log_dbg(D_CW_CACHE,"incoming CW dropped - current cw_cache_size: %i - cw_cache-mem-size: %iMiB", count_hash_table(&ht_cw_cache), 2*(int)tommy_hashlin_memory_usage(&ht_cw_cache)/1024/1024);
						SAFE_RWLOCK_UNLOCK(&cw_cache_lock);
						return false;
					}
				}
			}

			SAFE_RWLOCK_UNLOCK(&cw_cache_lock);
			return true;
		}
	}
	else
	{
		cs_log_dbg(D_CW_CACHE,"[cw_cache] cw_cache_init_done %i cfg.cw_cache_size: %u cfg.cw_cache_memory %u", cw_cache_init_done, cfg.cw_cache_size, cfg.cw_cache_memory);
		return true;
	}
	return true;
}
#endif

void add_cache(ECM_REQUEST *er)
{
	if(!cache_init_done || !er->csp_hash) return;

#ifdef CS_CACHEEX

if(cfg.cacheex_global_maxhop)
{
    int8_t hop = 0;

    if(er->cacheex_src)
    {
        hop = er->cacheex_src->cacheex_maxhop;
    }

    uint16_t percaid_maxhop = caidvaluetab_get_value(
        &cfg.cacheex_maxhop_percaid,
        er->caid,
        cfg.cacheex_maxhop
    );

    int8_t maxhop = (int8_t)percaid_maxhop;

    if(hop > maxhop)
    {
        cs_log_dbg(D_CACHEEX,
            "cacheex: DROP hop %d > maxhop %d (caid %04X)",
            hop, maxhop, er->caid);

        return;
    }
}

#endif
	ECMHASH *result = NULL;
	CW *cw = NULL;
	bool add_new_cw=false;

	SAFE_RWLOCK_WRLOCK(&cache_lock);

	// add csp_hash to cache
	result = find_hash_table(&ht_cache, &er->csp_hash, sizeof(uint32_t), &compare_csp_hash);
	if(!result)
	{
		if(cs_malloc(&result, sizeof(ECMHASH)))
		{
			result->csp_hash = er->csp_hash;
			init_hash_table(&result->ht_cw, &result->ll_cw);
			cs_ftime(&result->first_recv_time);
			add_hash_table(&ht_cache, &result->ht_node, &ll_cache, &result->ll_node, result, &result->csp_hash, sizeof(uint32_t));
		}
		else
		{
			SAFE_RWLOCK_UNLOCK(&cache_lock);
			cs_log("ERROR: NO added HASH to cache!!");
			return;
		}
	}

	cs_ftime(&result->upd_time); // need to be updated at each cw! We use it for deleting this hash when no more cws arrive inside max_cache_time!

	//add cw to this csp hash
	cw = find_hash_table(&result->ht_cw, er->cw, sizeof(er->cw), &compare_cw);

	if(!cw)
	{
		if(count_hash_table(&result->ht_cw) >= 10) // max 10 different cws stored
		{
			SAFE_RWLOCK_UNLOCK(&cache_lock);
			return;
		}

		while(1)
		{
			if(cs_malloc(&cw, sizeof(CW)))
			{
				memcpy(cw->cw, er->cw, sizeof(er->cw));
				cw->odd_even = get_odd_even(er);
				cw->cwc_cycletime = er->cwc_cycletime;
				cw->cwc_next_cw_cycle = er->cwc_next_cw_cycle;
				cw->count= 0;
				cw->csp = 0;
				cw->cacheex = 0;
				cw->localcards=0;
				cw->proxy=0;
				cw->grp = 0;
				cw->caid = er->caid;
				cw->prid = er->prid;
				cw->srvid = er->srvid;
				cw->selected_reader=er->selected_reader;
				cw->cacheex_src=er->cacheex_src;
				cw->pushout_client = NULL;

				while(1)
				{
					if (pthread_rwlock_init(&cw->pushout_client_lock, NULL) == 0)
						break;

					cs_log("Error creating lock pushout_client_lock!");
					cs_sleepms(1);
				}

				add_hash_table(&result->ht_cw, &cw->ht_node, &result->ll_cw, &cw->ll_node, cw, cw->cw, sizeof(er->cw));
				add_new_cw=true;
				break;
			}

			cs_log("ERROR: NO added CW to cache!! Re-trying...");
			cs_sleepms(1);
		}
	}

	// update if answered from csp/cacheex/local_proxy
	if(er->from_cacheex) cw->cacheex = 1;
	if(er->from_csp) cw->csp = 1;
	if(!er->cacheex_src)
	{
		if(is_localreader(er->selected_reader, er)) cw->localcards=1;
		else cw->proxy = 1;
	}

#ifdef CS_CACHEEX_AIO
	// copy flag for local generated CW
	if(er->localgenerated || (er->selected_reader && !is_network_reader(er->selected_reader)))
	{
		cw->localgenerated = 1;
		er->localgenerated = 1;
		// to favorite CWs with this flag while sorting
		if(cw->count < 0x0F000000)
		{
			cw->count |= 0x0F000000;
			lg_cache_size++;
		}
	}
	else
	{
		cw->localgenerated = 0;
	}
#endif

	// always update group and counter
	cw->grp |= er->grp;
	cw->count++;

#ifdef CS_CACHEEX_AIO
	// add count to er for checking @ cacheex_push
	er->cw_count += cw->count;
#endif
	// sort cw_list by counter (DESC order)
	if(cw->count>1)
		sort_list(&result->ll_cw, count_sort);

#ifdef CS_CACHEEX_AIO
	// dont push not flagged CWs - global
	if(!er->localgenerated &&
		(
			!chk_srvid_localgenerated_only_exception(er)
			&& (cfg.cacheex_localgenerated_only || chk_lg_only(er, &cfg.cacheex_lg_only_tab))
		)	)
	{
		cs_log_dbg(D_CACHEEX, "cacheex: push denied, cacheex_localgenerated_only->global");
		SAFE_RWLOCK_UNLOCK(&cache_lock);
		return;
	}

	// dont push CW if time for caid > x  && from local reader | proxy
	if(er->rc < 3 && er->ecm_time && get_cacheex_nopushafter(er) != 0 &&(get_cacheex_nopushafter(er) < er->ecm_time ))
	{
		cs_log_dbg(D_CACHEEX, "cacheex: push denied, cacheex_nopushafter %04X:%u < %i, reader: %s", er->caid, get_cacheex_nopushafter(er), er->ecm_time, er->selected_reader->label);
		SAFE_RWLOCK_UNLOCK(&cache_lock);
		return;
	}

	// no cacheex-push on diff-cw's if no localgenerated flag exist
	if(cfg.cacheex_dropdiffs && (count_hash_table(&result->ht_cw) > 1) && !er->localgenerated)
	{
		cs_log_dbg(D_CACHEEX,"cacheex: diff CW - cacheex push denied src: %s", er->selected_reader->label);
		SAFE_RWLOCK_UNLOCK(&cache_lock);
		return;
	}
#endif

	SAFE_RWLOCK_UNLOCK(&cache_lock);

	cacheex_cache_add(er, result, cw, add_new_cw);
}

#ifdef CS_CACHEEX_AIO
void cw_cache_cleanup(bool force)
{
	if(!cw_cache_init_done)
		{ return; }

	SAFE_RWLOCK_WRLOCK(&cw_cache_lock);

	CW_CACHE *cw_cache;
	node *i, *i_next;

	uint32_t ll_c = 0;
	uint32_t ll_ten_percent = (uint)tommy_list_count(&ll_cw_cache)*0.1; // 10 percent of cache

	if(!force)
		sort_list(&ll_cw_cache, time_sort);

	i = get_first_node_list(&ll_cw_cache);
	while(i)
	{
		i_next = i->next;

		cw_cache = get_data_from_node(i);

		if(!cw_cache)
		{
			i = i_next;
			continue;
		}
		if(!force)
		{
			++ll_c;

			if(ll_c < ll_ten_percent)
			{
				remove_elem_list(&ll_cw_cache, &cw_cache->ll_node);
				remove_elem_hash_table(&ht_cw_cache, &cw_cache->ht_node);
				NULLFREE(cw_cache);
			}
			else{
				break;
			}
		}
		else
		{
			remove_elem_list(&ll_cw_cache, &cw_cache->ll_node);
			remove_elem_hash_table(&ht_cw_cache, &cw_cache->ht_node);
			NULLFREE(cw_cache);
		}

		i = i_next;
	}

	SAFE_RWLOCK_UNLOCK(&cw_cache_lock);
}
#endif

void cleanup_cache(bool force)
{
	ECMHASH *ecmhash;
	CW *cw;
	struct s_pushclient *pc, *nxt;
	node *i,*i_next,*j,*j_next;

	struct timeb now;
	int64_t gone_first, gone_upd;

	if(!cache_init_done)
		{ return; }

	SAFE_RWLOCK_WRLOCK(&cache_lock);

	i = get_first_node_list(&ll_cache);
	while(i)
	{
		i_next = i->next;
		ecmhash = get_data_from_node(i);

		if(!ecmhash)
		{
			i = i_next;
			continue;
		}

		cs_ftime(&now);
		gone_first = comp_timeb(&now, &ecmhash->first_recv_time);
		gone_upd = comp_timeb(&now, &ecmhash->upd_time);

		if(!force && gone_first<=(cfg.max_cache_time*1000)) // not continue, useless check for nexts one!
		{
			break;
		}

		if(force || gone_upd>(cfg.max_cache_time*1000))
		{
			j = get_first_node_list(&ecmhash->ll_cw);
			while(j)
			{
				j_next = j->next;
				cw = get_data_from_node(j);
				if(cw)
				{
					pthread_rwlock_destroy(&cw->pushout_client_lock);
					pc = cw->pushout_client;
					cw->pushout_client=NULL;
					while(pc)
					{
						nxt = pc->next_push;
						NULLFREE(pc);
						pc = nxt;
					}
#ifdef CS_CACHEEX_AIO
					if(cw->count >= 0x0F000000)
					{
						lg_cache_size--;
					}
#endif
					remove_elem_list(&ecmhash->ll_cw, &cw->ll_node);
					remove_elem_hash_table(&ecmhash->ht_cw, &cw->ht_node);
					NULLFREE(cw);
				}
				j = j_next;
			}

			deinitialize_hash_table(&ecmhash->ht_cw);
			remove_elem_list(&ll_cache, &ecmhash->ll_node);
			remove_elem_hash_table(&ht_cache, &ecmhash->ht_node);
			NULLFREE(ecmhash);
		}
		i = i_next;
	}
	SAFE_RWLOCK_UNLOCK(&cache_lock);
}

#ifdef CS_CACHEEX_AIO
void cacheex_get_srcnodeid(ECM_REQUEST *er, uint8_t *remotenodeid)
{
	uint8_t *data;
	data = ll_last_element(er->csp_lastnodes);
	if(data)
	{
		memcpy(remotenodeid, data, 8);
	}
	else
	{
		memset(remotenodeid, 0 , 8);
	}
}
#endif
