/*
 * $Id$
 *
 * 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
 * Fruanhofer 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: Alot 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.
 *
 * 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 "scscf_list.h"
#include "db.h"
#include "../../lib/ims/useful_defs.h"

extern int scscf_entry_expiry; //time for scscf entries to remain the scscf_list

extern struct tm_binds tmb; //Structure with pointers to tm funcs

extern int use_preferred_scscf_uri;
extern str preferred_scscf_uri;

int i_hash_size;
i_hash_slot *i_hash_table = 0;

scscf_capabilities *SCSCF_Capabilities = 0;
int SCSCF_Capabilities_cnt = 0;

/**
 * Refreshes the capabilities list reading them from the db.
 * Drops the old cache and queries the db
 * \todo - IMPLEMENT A WAY TO PUSH AN EXTERNAL EVENT FOR THIS
 * \todo - SOLVE THE LOCKING PROBLEM - THIS IS A WRITER
 * @returns 1 on success, 0 on failure
 */
int I_get_capabilities() {
    int i, j, r;
    /* free the old cache */
    if (SCSCF_Capabilities != 0) {
        for (i = 0; i < SCSCF_Capabilities_cnt; i++) {
            if (SCSCF_Capabilities[i].capabilities)
                shm_free(SCSCF_Capabilities[i].capabilities);
        }
        shm_free(SCSCF_Capabilities);
    }

    SCSCF_Capabilities_cnt = ims_icscf_db_get_scscf(&SCSCF_Capabilities);

    r = ims_icscf_db_get_capabilities(&SCSCF_Capabilities, SCSCF_Capabilities_cnt);

    LM_DBG("DBG:------  S-CSCF Map with Capabilities  begin ------\n");
    if (SCSCF_Capabilities != 0) {
        for (i = 0; i < SCSCF_Capabilities_cnt; i++) {
            LM_DBG("DBG:S-CSCF [%d] <%.*s>\n", SCSCF_Capabilities[i].id_s_cscf, SCSCF_Capabilities[i].scscf_name.len, SCSCF_Capabilities[i].scscf_name.s);
            for (j = 0; j < SCSCF_Capabilities[i].cnt; j++)
                LM_DBG("DBG:       \t [%d]\n", SCSCF_Capabilities[i].capabilities[j]);
        }
    }
    LM_DBG("DBG:------  S-CSCF Map with Capabilities  end ------\n");

    return r;
}

/**
 * Adds the name to the list starting at root, ordered by score.
 * Returns the new root
 */
static inline scscf_entry* I_add_to_scscf_list(scscf_entry *root, str name, int score, int originating) {
    scscf_entry *x, *i;

    //duplicate?
    for (i = root; i; i = i->next)
        if (name.len == i->scscf_name.len &&
                strncasecmp(name.s, i->scscf_name.s, name.len) == 0)
            return root;

    x = new_scscf_entry(name, score, originating);
    if (!x) return root;

    if (!root) {
        return x;
    }
    if (root->score < x->score) {
        x->next = root;
        return x;
    }
    i = root;
    while (i->next && i->next->score > x->score)
        i = i->next;
    x->next = i->next;
    i->next = x;
    return root;
}

/**
 * Initialize the hash with S-CSCF lists
 */
int i_hash_table_init(int hash_size) {
    int i;

    i_hash_size = hash_size;
    i_hash_table = shm_malloc(sizeof (i_hash_slot) * i_hash_size);

    if (!i_hash_table) return 0;

    memset(i_hash_table, 0, sizeof (i_hash_slot) * i_hash_size);

    for (i = 0; i < i_hash_size; i++) {
        i_hash_table[i].lock = lock_alloc();
        if (!i_hash_table[i].lock) {
            LM_ERR("ERR:i_hash_table_init(): Error creating lock\n");
            return 0;
        }
        i_hash_table[i].lock = lock_init(i_hash_table[i].lock);
    }

    return 1;
}

/**
 * Frees memory for scscf list
 */
void free_scscf_list(scscf_list *sl) {
    scscf_entry *i;
    if (!sl) return;
    if (sl->call_id.s) shm_free(sl->call_id.s);
    while (sl->list) {
        i = sl->list->next;
        if (sl->list->scscf_name.s) shm_free(sl->list->scscf_name.s);
        shm_free(sl->list);
        sl->list = i;
    }
    shm_free(sl);
}

/**
 * Returns a list of S-CSCFs that we should try on, based on the
 * capabilities requested
 * \todo - order the list according to matched optionals - r
 * @param scscf_name - the first S-CSCF if specified
 * @param m - mandatory capabilities list
 * @param mcnt - mandatory capabilities list size
 * @param o - optional capabilities list
 * @param ocnt - optional capabilities list size
 * @param orig - indicates originating session case
 * @returns list of S-CSCFs, terminated with a str={0,0}
 */
scscf_entry* I_get_capab_ordered(str scscf_name, int *m, int mcnt, int *o, int ocnt, str *p, int pcnt, int orig) {
    scscf_entry *list = 0;
    int i, r;

    if (scscf_name.len) list = I_add_to_scscf_list(list, scscf_name, INT_MAX, orig);

    for (i = 0; i < pcnt; i++)
        list = I_add_to_scscf_list(list, p[i], INT_MAX - i, orig);

    for (i = 0; i < SCSCF_Capabilities_cnt; i++) {
        r = I_get_capab_match(SCSCF_Capabilities + i, m, mcnt, o, ocnt);
        if (r != -1) {
            list = I_add_to_scscf_list(list, SCSCF_Capabilities[i].scscf_name, r, orig);
            LM_DBG("DBG:I_get_capab_ordered: <%.*s> Added to the list, orig=%d\n",
                    SCSCF_Capabilities[i].scscf_name.len, SCSCF_Capabilities[i].scscf_name.s, orig);
        }
    }
    return list;
}

/**
 * Creates new scscf entry structure
 */
scscf_entry* new_scscf_entry(str name, int score, int orig) {
    scscf_entry *x = 0;
    x = shm_malloc(sizeof (scscf_entry));
    if (!x) {
        LM_ERR("ERR:new_scscf_entry: Error allocating %lx bytes\n",
                sizeof (scscf_entry));
        return 0;
    }
    /* duplicate always the scscf_name because of possible list reloads and scscf_name coming in LIA/UAA */
    if (orig) x->scscf_name.s = shm_malloc(name.len + 5);
    else x->scscf_name.s = shm_malloc(name.len);
    if (!x->scscf_name.s) {
        LM_ERR("ERR:new_scscf_entry: Error allocating %d bytes\n",
                orig ? name.len + 5 : name.len);
        shm_free(x);
        return 0;
    }
    memcpy(x->scscf_name.s, name.s, name.len);
    x->scscf_name.len = name.len;
    if (orig) {
        memcpy(x->scscf_name.s + name.len, ";orig", 5);
        x->scscf_name.len += 5;
    }

    LM_DBG("INFO:new_scscf_entry:  <%.*s>\n", x->scscf_name.len, x->scscf_name.s);

    x->score = score;

    x->start_time = time(0);

    x->next = 0;
    return x;
}

/**
 * Returns the matching rank of a S-CSCF
 * \todo - optimize the search as O(n^2) is hardly desireable
 * @param c - the capabilities of the S-CSCF
 * @param m - mandatory capabilities list requested
 * @param mcnt - mandatory capabilities list size
 * @param o - optional capabilities list
 * @param ocnt - optional capabilities list sizeint I_get_capab_match(ims_icscf_capabilities *c,int *m,int mcnt,int *o,int ocnt)
 * @returns - -1 if mandatory not satisfied, else count of matched optional capab
 */
int I_get_capab_match(scscf_capabilities *c, int *m, int mcnt, int *o, int ocnt) {
    int r = 0, i, j, t = 0;
    for (i = 0; i < mcnt; i++) {
        t = 0;
        for (j = 0; j < c->cnt; j++)
            if (m[i] == c->capabilities[j]) {
                t = 1;
                break;
            }
        if (!t) return -1;
    }
    for (i = 0; i < ocnt; i++) {
        for (j = 0; j < c->cnt; j++)
            if (o[i] == c->capabilities[j]) r++;
    }
    return r;
}

int add_scscf_list(str call_id, scscf_entry *sl) {
    scscf_list *l;
    unsigned int hash = get_call_id_hash(call_id, i_hash_size);

    l = new_scscf_list(call_id, sl);
    if (!l) return 0;

    i_lock(hash);
    l->prev = 0;
    l->next = i_hash_table[hash].head;
    if (l->next) l->next->prev = l;
    i_hash_table[hash].head = l;
    if (!i_hash_table[hash].tail) i_hash_table[hash].tail = l;
    i_unlock(hash);

    return 1;
}

/**
 * Computes the hash for a string.
 */
unsigned int get_call_id_hash(str callid, int hash_size) {
#define h_inc h+=v^(v>>3)
    char* p;
    register unsigned v;
    register unsigned h;

    h = 0;
    for (p = callid.s; p <= (callid.s + callid.len - 4); p += 4) {
        v = (*p << 24)+(p[1] << 16)+(p[2] << 8) + p[3];
        h_inc;
    }
    v = 0;
    for (; p < (callid.s + callid.len); p++) {
        v <<= 8;
        v += *p;
    }
    h_inc;

    h = ((h)+(h >> 11))+((h >> 13)+(h >> 23));
    return (h) % hash_size;
#undef h_inc
}

scscf_list* new_scscf_list(str call_id, scscf_entry *sl) {
    scscf_list *l;

    l = shm_malloc(sizeof (scscf_list));
    if (!l) {
        LM_ERR("ERR:new_scscf_list(): Unable to alloc %lx bytes\n",
                sizeof (scscf_list));
        goto error;
    }
    memset(l, 0, sizeof (scscf_list));

    STR_SHM_DUP(l->call_id, call_id, "shm");
    l->list = sl;

    return l;
error:
    out_of_memory :
    if (l) {
        shm_free(l);
    }
    return 0;
}

/**
 * Locks the required part of hash with S-CSCF lists
 */
void i_lock(unsigned int hash) {

    lock_get(i_hash_table[(hash)].lock);

}

/**
 * UnLocks the required part of hash with S-CSCF lists
 */
void i_unlock(unsigned int hash) {
    lock_release(i_hash_table[(hash)].lock);

}

static str route_hdr_s = {"Route: <", 8};
static str route_hdr_e = {">\r\n", 3};

int I_scscf_select(struct sip_msg* msg, char* str1, char* str2) {
    str call_id, scscf_name = {0, 0};
    struct sip_msg *req;
    int result;
    str hdr = {0, 0};

    call_id = cscf_get_call_id(msg, 0);
    LM_DBG("I_scscf_select() for call-id <%.*s>\n", call_id.len, call_id.s);
    if (!call_id.len)
        return CSCF_RETURN_FALSE;

    scscf_name = take_scscf_entry(call_id);

    if (!scscf_name.len) {
        LM_DBG("no scscf entry for callid [%.*s]\n", call_id.len, call_id.s);
        return CSCF_RETURN_FALSE;
    }

    if (msg->first_line.u.request.method.len == 8 &&
            strncasecmp(msg->first_line.u.request.method.s, "REGISTER", 8) == 0) {
        /* REGISTER fwding */
        if (str1 && str1[0] == '0') {
            /* first time */
            if (rewrite_uri(msg, &(scscf_name)) < 0) {
                LM_ERR("I_UAR_forward: Unable to Rewrite URI\n");
                result = CSCF_RETURN_FALSE;
            } else
                result = CSCF_RETURN_TRUE;
        } else {
            /* subsequent */
            req = msg;
            append_branch(req, &scscf_name, 0, 0, Q_UNSPECIFIED, 0, 0, 0, 0, 0, 0);
            result = CSCF_RETURN_TRUE;
        }
    } else {
        /* Another request */
        result = CSCF_RETURN_TRUE;

        hdr.len = route_hdr_s.len + scscf_name.len + route_hdr_e.len;
        hdr.s = pkg_malloc(hdr.len);
        if (!hdr.s) {
            LM_ERR("ERR:Mw_REQUEST_forward: Error allocating %d bytes\n",
                    hdr.len);
            result = CSCF_RETURN_TRUE;
        }
        hdr.len = 0;
        STR_APPEND(hdr, route_hdr_s);
        STR_APPEND(hdr, scscf_name);
        STR_APPEND(hdr, route_hdr_e);

        if (!cscf_add_header_first(msg, &hdr, HDR_ROUTE_T)) {
            pkg_free(hdr.s);
            result = CSCF_RETURN_TRUE;
        }

        if (msg->dst_uri.s) pkg_free(msg->dst_uri.s);
        STR_PKG_DUP(msg->dst_uri, scscf_name, "pkg");
    }

    return result;
out_of_memory:
    if (scscf_name.s) shm_free(scscf_name.s);
    return CSCF_RETURN_ERROR;
}

/**
 * Takes on S-CSCF name for the respective Call-ID from the respective name list.
 * Don't free the result.s - it is freed later!
 * @param call_id - the id of the call
 * @returns the shm_malloced S-CSCF name if found or empty string if list is empty or does not exists
 */
str take_scscf_entry(str call_id) {
    str scscf = {0, 0};
    scscf_list *l = 0;
    scscf_entry *scscf_entry = 0;
    unsigned int hash = get_call_id_hash(call_id, i_hash_size);

    LM_DBG("Getting scscf entry from list\n");
    
    i_lock(hash);
    l = i_hash_table[hash].head;
    
    //if use_preferred_scscf_uri then check the table for the preferred scscf set
    if(use_preferred_scscf_uri) {
	LM_DBG("use_preferred_scscf_uri is set so will check for preferred_scscf_uri first [%.*s]\n", preferred_scscf_uri.len, preferred_scscf_uri.s);
	while (l) {
	    if (l->call_id.len == call_id.len &&
		    strncasecmp(l->call_id.s, call_id.s, call_id.len) == 0) {
		scscf_entry = l->list;
		while (scscf_entry) {
		    LM_DBG("scscf_entry [%.*s]\n", scscf_entry->scscf_name.len, scscf_entry->scscf_name.s);
		    if (strncasecmp(scscf_entry->scscf_name.s, preferred_scscf_uri.s, preferred_scscf_uri.len) == 0) {
			LM_DBG("scscf_entry matches\n");
			scscf = scscf_entry->scscf_name;
			break;
		    }
		    scscf_entry = scscf_entry->next;
		}
		
		break;
	    }
	    l = l->next;
	}
    }
    
    // if scscf has not yet been set then find the first scscf that matches
    if(scscf.len <= 0 ) {
	LM_DBG("scscf has not been set so we just look for first match\n");
	while (l) {
	    if (l->call_id.len == call_id.len &&
		    strncasecmp(l->call_id.s, call_id.s, call_id.len) == 0) {
		if (l->list) {
		    LM_DBG("scscf_entry [%.*s]\n", l->list->scscf_name.len, l->list->scscf_name.s);
		    scscf = l->list->scscf_name;
		}
		break;
	    }
	    l = l->next;
	}
    }
    i_unlock(hash);
    return scscf;
}

int I_scscf_drop(struct sip_msg* msg, char* str1, char* str2) {
    str call_id;
    //print_scscf_list(L_DBG);
    call_id = cscf_get_call_id(msg, 0);
    LM_DBG("DBG:I_scscf_drop(): <%.*s>\n", call_id.len, call_id.s);
    if (!call_id.len)
        return CSCF_RETURN_FALSE;

    del_scscf_list(call_id);
    return CSCF_RETURN_TRUE;
}

void del_scscf_list(str call_id) {
    scscf_list *l = 0;
    unsigned int hash = get_call_id_hash(call_id, i_hash_size);

    i_lock(hash);
    l = i_hash_table[hash].head;
    while (l) {
        if (l->call_id.len == call_id.len &&
                strncasecmp(l->call_id.s, call_id.s, call_id.len) == 0) {
            if (l->prev) l->prev->next = l->next;
            else i_hash_table[hash].head = l->next;
            if (l->next) l->next->prev = l->prev;
            else i_hash_table[hash].tail = l->prev;
            i_unlock(hash);
            free_scscf_list(l);
            return;
        }
        l = l->next;
    }
    i_unlock(hash);
}

void print_scscf_list(int log_level) {
    scscf_list *l;
    int i;
    scscf_entry *sl;
    LM_DBG("INF:----------  S-CSCF Lists begin --------------\n");
    for (i = 0; i < i_hash_size; i++) {
        i_lock(i);
        l = i_hash_table[i].head;
        while (l) {
            LM_DBG("INF:[%4d] Call-ID: <%.*s> \n", i,
                    l->call_id.len, l->call_id.s);
            sl = l->list;
            while (sl) {
                LM_DBG("INF:         Score:[%4d] S-CSCF: <%.*s> \n",
                        sl->score,
                        sl->scscf_name.len, sl->scscf_name.s);
                sl = sl->next;
            }
            l = l->next;
        }
        i_unlock(i);
    }
    LM_DBG("INF:----------  S-CSCF Lists end   --------------\n");

}

/**
 * Transactional SIP response - tries to create a transaction if none found.
 * @param msg - message to reply to
 * @param code - the Status-code for the response
 * @param text - the Reason-Phrase for the response
 * @returns the tmb.t_reply() result
 */
int cscf_reply_transactional(struct sip_msg *msg, int code, char *text) {
    unsigned int hash, label;
    if (tmb.t_get_trans_ident(msg, &hash, &label) < 0) {

        LM_DBG("INF:cscf_reply_transactional: Failed to get SIP transaction - creating new one\n");
        if (tmb.t_newtran(msg) < 0)
            LM_DBG("INF:cscf_reply_transactional: Failed creating SIP transaction\n");
    }
    return tmb.t_reply(msg, code, text);
}

int cscf_reply_transactional_async(struct cell* t, struct sip_msg *msg, int code, char *text) {
    return tmb.t_reply_trans(t, msg, code, text);
}

/**
 * Timeout routine called every x seconds and determines if scscf_list entries should be expired
 * @param msg - message to reply to
 * @param code - the Status-code for the response
 * @param text - the Reason-Phrase for the response
 * @returns the tmb.t_reply() result
 */

void ims_icscf_timer_routine() {
    //run through scscf_list and decide if they should be removed!
    scscf_list *l, *tmp;
    int i;
    scscf_entry *sl;

    int delete_list = -1;

    LM_DBG("INF: ims_icscf timer routine");
    //run through all entries and remove the whole list if one entry has expired
    for (i = 0; i < i_hash_size; i++) {
        i_lock(i);
        l = i_hash_table[i].head;
        while (l) {

            LM_DBG("INF:[%4d] Call-ID: <%.*s> \n", i,
                    l->call_id.len, l->call_id.s);

            sl = l->list;
            while (sl) {

                LM_DBG("INF: Score:[%4d] Start_time [%" TIME_T_FMT "] S-CSCF: <%.*s> \n",
                        sl->score,
                        TIME_T_CAST(sl->start_time),
                        sl->scscf_name.len, sl->scscf_name.s);
                time_t now = time(0);
                time_t time_elapsed = now - sl->start_time;
                if (time_elapsed > scscf_entry_expiry) {

                    LM_DBG("Scscf entry expired: Time now %" TIME_T_FMT " Start time %" TIME_T_FMT " - elapsed %" TIME_T_FMT "\n",
                            TIME_T_CAST(now), TIME_T_CAST(sl->start_time), TIME_T_CAST(time_elapsed));
                    delete_list = 1; //if any of the entries in this list have expired remove the whole list!

                }
                sl = sl->next;
            }

            if (delete_list == 1) {
                //if any of the entries in this list have expired remove the whole list!
                //remove the list for call_id
                tmp = l->next;
                if (l->prev) l->prev->next = l->next;
                else i_hash_table[i].head = l->next;
                if (l->next) l->next->prev = l->prev;
                else i_hash_table[i].tail = l->prev;
                free_scscf_list(l);
                l = tmp;
                delete_list = -1;
            } else {
                l = l->next;
            }
        }
        i_unlock(i);
    }
}
