/*
 * Copyright (C) 2012 Smile Communications, jason.penton@smilecoms.com
 * Copyright (C) 2012 Smile Communications, richard.good@smilecoms.com
 *
 * The initial version of this code was written by Dragos Vingarzan
 * (dragos(dot)vingarzan(at)fokus(dot)fraunhofer(dot)de and the
 * Fraunhofer FOKUS Institute. It was and still is maintained in a separate
 * branch of the original SER. We are therefore migrating it to
 * Kamailio/SR and look forward to maintaining it from here on out.
 * 2011/2012 Smile Communications, Pty. Ltd.
 * ported/maintained/improved by
 * Jason Penton (jason(dot)penton(at)smilecoms.com and
 * Richard Good (richard(dot)good(at)smilecoms.com) as part of an
 * effort to add full IMS support to Kamailio/SR using a new and
 * improved architecture
 *
 * NB: A lot of this code was originally part of OpenIMSCore,
 * FhG Fokus.
 * Copyright (C) 2004-2006 FhG Fokus
 * Thanks for great work! This is an effort to
 * break apart the various CSCF functions into logically separate
 * components. We hope this will drive wider use. We also feel
 * that in this way the architecture is more complete and thereby easier
 * to manage in the Kamailio/SR environment
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Kamailio 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 2 of the License, or
 * (at your option) any later version
 *
 * Kamailio 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#include "diameter.h"

#include "timer.h"
#include "peermanager.h"

#include "globals.h"
#include "peerstatemachine.h"

peer_list_t *peer_list = 0;		/**< list of peers */
gen_lock_t *peer_list_lock = 0; /**< lock for the list of peers */

extern dp_config *config; /**< Configuration for this diameter peer 	*/
extern char *dp_states[];
AAAMsgIdentifier *hopbyhop_id = 0; /**< Current id for Hop-by-hop */
AAAMsgIdentifier *endtoend_id = 0; /**< Current id for End-to-end */
gen_lock_t *msg_id_lock; /**< lock for the message identifier changes */

/**
 * Initializes the Peer Manager.
 * The initial list of peers is taken from the configuration provided
 * @param config - configuration for initial peers
 * @returns 1
 */
int peer_manager_init(dp_config *config)
{
	int i;
	peer *p;
	LM_DBG("Peer Manager initialization...\n");
	peer_list = shm_malloc(sizeof(peer_list_t));
	peer_list->head = 0;
	peer_list->tail = 0;
	peer_list_lock = lock_alloc();
	peer_list_lock = lock_init(peer_list_lock);

	hopbyhop_id = shm_malloc(sizeof(AAAMsgIdentifier));
	endtoend_id = shm_malloc(sizeof(AAAMsgIdentifier));
	msg_id_lock = lock_alloc();
	msg_id_lock = lock_init(msg_id_lock);

	*hopbyhop_id = kam_rand();
	*endtoend_id = (time(0) & 0xFFF) << 20;
	*endtoend_id |= kam_rand() & 0xFFFFF;

	for(i = 0; i < config->peers_cnt; i++) {
		p = new_peer(config->peers[i].fqdn, config->peers[i].realm,
				config->peers[i].port, config->peers[i].src_addr,
				config->peers[i].proto, config->peers[i].vrf);
		if(!p)
			continue;
		p->is_dynamic = 0;
		add_peer(p);
	}

	add_timer(1, 0, &peer_timer, 0);

	return 1;
}

/**
 * Destroys the Peer Manager and disconnects all peer sockets.
 */
void peer_manager_destroy()
{
	peer *foo, *bar;
	lock_get(peer_list_lock);
	foo = peer_list->head;
	while(foo) {
		if(foo->I_sock > 0)
			close(foo->I_sock);
		if(foo->R_sock > 0)
			close(foo->R_sock);
		bar = foo->next;
		free_peer(foo, 1);
		foo = bar;
	}

	/*	lock_get(msg_id_lock);	*/
	shm_free(hopbyhop_id);
	shm_free(endtoend_id);
	lock_destroy(msg_id_lock);
	lock_dealloc((void *)msg_id_lock);

	shm_free(peer_list);
	lock_destroy(peer_list_lock);
	lock_dealloc((void *)peer_list_lock);
	LM_DBG("Peer Manager destroyed\n");
}

/**
 * Logs the list of peers
 * @param level - log level to print to
 */
void log_peer_list()
{
	if(debug_heavy) {
		/* must have lock on peer_list_lock when calling this!!! */
		peer *p;
		int i;

		LM_DBG("--- Peer List: ---\n");
		for(p = peer_list->head; p; p = p->next) {
			LM_DBG("State of peer: %s FQDN: %.*s Port: %d Is dynamic %c\n",
					dp_states[p->state], p->fqdn.len, p->fqdn.s, p->port,
					p->is_dynamic ? 'X' : ' ');
			for(i = 0; i < p->applications_cnt; i++)
				LM_DBG("Application ID: %d, Application Vendor: %d \n",
						p->applications[i].id, p->applications[i].vendor);
		}
		LM_DBG("------------------\n");
	}
}

/**
 * Adds a peer to the peer list
 * @param p - peer to add
 */
void add_peer(peer *p)
{
	if(!p)
		return;
	lock_get(peer_list_lock);
	p->next = 0;
	p->prev = peer_list->tail;
	if(!peer_list->head)
		peer_list->head = p;
	if(peer_list->tail)
		peer_list->tail->next = p;
	peer_list->tail = p;
	lock_release(peer_list_lock);
}

/**
 * Removes a peer from the peer list
 * @param p - the peer to remove
 */
void remove_peer(peer *p)
{
	peer *i;
	if(!p)
		return;
	i = peer_list->head;
	while(i && i != p)
		i = i->next;
	if(i) {
		if(i->prev)
			i->prev->next = i->next;
		else
			peer_list->head = i->next;
		if(i->next)
			i->next->prev = i->prev;
		else
			peer_list->tail = i->prev;
	}
}

/**
 * Finds a peer based on the TCP socket.
 * @param sock - socket to look for
 * @returns the peer* or NULL if not found
 */
peer *get_peer_from_sock(int sock)
{
	peer *i;
	lock_get(peer_list_lock);

	i = peer_list->head;
	while(i && i->I_sock != sock && i->R_sock != sock)
		i = i->next;
	lock_release(peer_list_lock);
	return i;
}

/**
 * Finds a peer based on the FQDN and Realm.
 * @param fqdn - the FQDN to look for
 * @param realm - the Realm to look for
 * @returns the peer* or NULL if not found
 */
peer *get_peer_from_fqdn(str fqdn, str realm)
{
	peer *i;
	str dumb = {0, 0};

	lock_get(peer_list_lock);
	i = peer_list->head;
	while(i) {
		if(fqdn.len == i->fqdn.len
				&& strncasecmp(fqdn.s, i->fqdn.s, fqdn.len) == 0)
			break;
		i = i->next;
	}
	lock_release(peer_list_lock);
	if(!i && config->accept_unknown_peers) {
		i = new_peer(fqdn, realm, 3868, dumb, dumb, dumb);
		if(i) {
			i->is_dynamic = 1;
			touch_peer(i);
			add_peer(i);
		}
	}
	return i;
}

/**
 * Finds a peer based on the FQDN.
 * @param fqdn - the FQDN to look for
 * @returns the peer* or NULL if not found
 */
peer *get_peer_by_fqdn(str *fqdn)
{
	peer *i;
	lock_get(peer_list_lock);
	i = peer_list->head;
	while(i) {
		if(fqdn->len == i->fqdn.len
				&& strncasecmp(fqdn->s, i->fqdn.s, fqdn->len) == 0)
			break;
		i = i->next;
	}
	lock_release(peer_list_lock);
	return i;
}

/**
 * Timer function for peer management.
 * This is registered as a timer by peer_manager_init() and gets called every
 * #PEER_MANAGER_TIMER seconds. Then it looks on what changed and triggers events.
 * @param now - time of call
 * @param ptr - generic pointer for timers - not used
 */
int peer_timer(time_t now, void *ptr)
{
	peer *p, *n;
	int i;
	LM_DBG("taking care of peers...\n");
	lock_get(peer_list_lock);
	p = peer_list->head;
	while(p) {
		lock_get(p->lock);
		n = p->next;

		if(p->disabled && p->state != Closed && p->state != Closing) {
			LM_DBG("Peer [%.*s] has been disabled [state: %d] - shutting "
				   "down\n",
					p->fqdn.len, p->fqdn.s, p->state);
			if(p->state == I_Open)
				sm_process(p, Stop, 0, 1, p->I_sock);
			if(p->state == R_Open)
				sm_process(p, Stop, 0, 1, p->R_sock);
			lock_release(p->lock);
			p = n;
			continue;
		}

		if(p->activity + config->tc <= now) {
			LM_DBG("Peer %.*s State %d \n", p->fqdn.len, p->fqdn.s, p->state);
			switch(p->state) {
				/* initiating connection */
				case Closed:
					if(p->is_dynamic && config->drop_unknown_peers) {
						remove_peer(p);
						free_peer(p, 1);
						break;
					}
					if(!p->disabled) {
						touch_peer(p);
						sm_process(p, Start, 0, 1, 0);
					}
					break;
					/* timeouts */
				case Wait_Conn_Ack:
				case Wait_I_CEA:
				case Closing:
				case Wait_Returns:
				case Wait_Conn_Ack_Elect:
					touch_peer(p);
					sm_process(p, Timeout, 0, 1, 0);
					break;
					/* inactivity detected */
				case I_Open:
				case R_Open:
					if(p->waitingDWA) {
						p->waitingDWA = 0;
						if(p->state == I_Open)
							sm_process(p, I_Peer_Disc, 0, 1, p->I_sock);
						if(p->state == R_Open)
							sm_process(p, R_Peer_Disc, 0, 1, p->R_sock);
						LM_WARN("Inactivity on peer [%.*s] and no DWA, Closing "
								"peer...\n",
								p->fqdn.len, p->fqdn.s);
					} else {
						p->waitingDWA = 1;
						Snd_DWR(p);
						touch_peer(p);
						if(debug_heavy) {
							LM_DBG("Inactivity on peer [%.*s], sending DWR... "
								   "- if we don't get a reply, the peer will "
								   "be closed\n",
									p->fqdn.len, p->fqdn.s);
						}
					}
					break;
					/* ignored states */
					/* unknown states */
				default:
					LM_ERR("Peer %.*s inactive  in state %d\n", p->fqdn.len,
							p->fqdn.s, p->state);
			}
		}
		lock_release(p->lock);
		p = n;
	}
	lock_release(peer_list_lock);
	log_peer_list();
	i = config->tc / 5;
	if(i <= 0)
		i = 1;
	return i;
}

/**
 * Generates the next Hop-by-hop identifier.
 * @returns the new identifier to be used in messages
 */
inline AAAMsgIdentifier next_hopbyhop()
{
	AAAMsgIdentifier x;
	lock_get(msg_id_lock);
	*hopbyhop_id = (*hopbyhop_id) + 1;
	x = *hopbyhop_id;
	lock_release(msg_id_lock);
	return x;
}

/**
 * Generates the next End-to-end identifier.
 * @returns the new identifier to be used in messages
 */
inline AAAMsgIdentifier next_endtoend()
{
	AAAMsgIdentifier x;
	lock_get(msg_id_lock);
	*endtoend_id = (*endtoend_id) + 1;
	x = *endtoend_id;
	lock_release(msg_id_lock);
	return x;
}
