/*
 * (llc_conn.c)-Driver routines for connection component.
 *
 * Copyright (c) 1997 by Procom Technology,Inc.
 *
 * This program can be redistributed or modified under the terms of the 
 * GNU General Public License as published by the Free Software Foundation.
 * This program is distributed without any warranty or implied warranty
 * of merchantability or fitness for a particular purpose.
 *
 * See the GNU General Public License for more details.
 *
 */
 

#define LLC_CONN_C

#include <linux/netdevice.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <net/cm_types.h>
#include <net/cm_mm.h>
#include <net/cm_dll.h>
#include <net/cm_frame.h>
#include <net/llc_if.h>
#include <net/llc_sap.h>
#include <net/llc_conn.h>
#include <net/llc_main.h>
#include <net/llc_glob.h>
#include <net/llc_c_ev.h>
#include <net/llc_c_ac.h>
#include <net/llc_c_st.h>
#include <net/llc_mac.h>
#include <net/llc_pdu.h>
#include <net/llc_s_ev.h>
#include <net/lan_hdrs.h>
#include <net/llc_rout.h>
#include <net/llc_dbg.h>

#ifdef LLC_CONN_DBG
  #define  DBG_MSG(body) { printk body; }
#else
  #define  DBG_MSG(body)  ;
#endif

static us16        conn_send_pdus (connection_t * conn);
static us16        conn_next_state (connection_t * conn,
                                                conn_state_event_t * event);
static us16        conn_rtn_event (connection_t * conn, 
                                                conn_state_event_t * event);
static us16        execute_conn_transition_actions (connection_t * conn,
                           conn_state_transition_t * transition,
                                                conn_state_event_t * event);
static conn_state_transition_t *
                   qualify_conn_event (connection_t * conn,
                                                conn_state_event_t * event);


          /* Offset table on connection states transition diagram */
us16		    Offset_table[NBR_CONN_STATES][NBR_CONN_EV];



/*
 * Function : conn_get_event 
 * 
 * Description : 
 *  this function gets one event from event pool of connection and returnes it.
 * 
 * Parameters :
 *  connection_t *conn : pointer to connection that event must extract from it's
 *  pool.
 *  void **event : pointer to allocated event (output argument).
 * 
 * Returns : 
 *  0 : success.
 *  1 : failure. 
 */
us16 
conn_get_event (connection_t * conn, void ** event)
{
	us16 rc = 1;        

	/* verify connection is valid, active and open */
	if (conn->state != CONN_OUT_OF_SVC) {
		/* get event structure to build a station event */
		dll_remove (&conn->avail_events, event, DLL_WHERE_HEAD);
		((conn_state_event_t *)*event)->flag = NO;
		((conn_state_event_t *)*event)->ind_prim = NULL;
		((conn_state_event_t *)*event)->cfm_prim = NULL;
		rc = 0;
	}
	if (rc){
		FDBG_ERR_MSG(("conn_get_event failed : rc=%d\n",rc));
	}
	return (rc);
}


/*
 * Function : conn_send_event 
 * 
 * Description : 
 *  this function sends an event to connection state machine. after processing
 *  event (executing it's actions and changing state), upper layer will be 
 *  indicated or confirmed, if needed.
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 *  void *event : pointer to occured event.
 * 
 * Returns : 
 *  0 : success.
 *  1 : failure. 
 */

us16 
conn_send_event (connection_t * conn, void * event)
{
	us8 flag;
	prim_if_block_t *ind_prim,*cfm_prim;
	int rc=0;
		/* sending event to state machine */
	rc = conn_service (conn,event);

	flag = ((conn_state_event_t *)event)->flag;
	ind_prim = ((conn_state_event_t *)event)->ind_prim;
	cfm_prim = ((conn_state_event_t *)event)->cfm_prim;
	conn_rtn_event (conn, event);

	if (flag == YES) {  /* indicate or confirm required */
		if (ind_prim != NULL) { /* indication required */
			((sap_t *) conn->parent_sap)->indicate (ind_prim);   
		}  
		if (cfm_prim != NULL) { /* confirmation required */
				/* data confirm has preconditions */
			if ( cfm_prim->primitive != DATA_PRIM ) {  
				((sap_t *) conn->parent_sap)->confirm (cfm_prim);  
			} else if (data_accept_state(conn->state)==0) {
				/* in this state , we can send I pdu */
				if (conn->data_ret_val != 0 &&
							 conn->busy == YES ) { 
					conn->data_ret_val = -ERESTART;
				} else {
					rc = ((sap_t *) conn->parent_sap)->confirm (cfm_prim);  
					if ( rc ) { 
					/* confirmation didn't accept by upper layer */
						conn->failed_data_req = YES;
					}
				}    
			} else {
				conn->failed_data_req = YES;
			} 
		}     
	}
	return (rc);
}



us16 
conn_service (connection_t * conn, void *event)
{
	return conn_next_state(conn, (conn_state_event_t *)event);
}


us16 
conn_send_pdu (connection_t * conn, frame_t * pdu)
{
		/* queue PDU to send to MAC layer */
	dll_add (&conn->mac_pdu_q, (void *) pdu, DLL_WHERE_TAIL);

	conn_send_pdus (conn);
	return (0);
}

/*
 * Function : conn_rtn_pdu 
 * 
 * Description : 
 *  this function sends recieved data pdu to upper layer (by using indicate
 *  function). 
 *  this function prepares service parameters ( prim and prim_data). calling
 *  indication function will be done in conn_send_event.
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 *  frame_t *pdu : recieved data frame.
 *  void *event : pointer to occured event.
 * 
 * Returns : 
 *  0 
 */



us16
conn_rtn_pdu (connection_t *conn, frame_t *pdu, void *event)
{  
	prim_if_block_t *prim = &Ind_prim;
	prim_data_u *prim_data = Ind_prim.data;

	prim_data->data.connect_handle = (us32) conn;
	prim_data->data.priority = 0;
	prim_data->data.unit = (us8 *) pdu;
	prim_data->data.link_no = conn->link_no;
	prim->data = prim_data;
	prim->primitive = DATA_PRIM;
	prim->sap = conn->parent_sap;
	((conn_state_event_t *)event)->flag = YES; 

     /* saving prepared prim in event for future use in conn_send_event  */
	((conn_state_event_t *)event)->ind_prim = prim; 

	return (0);
}

/*
 * Function : conn_resend_i_pdu_as_cmd 
 * 
 * Description : 
 *  re-send all unacknowledged I PDUs, starting with the NR; send
 *  first as command PDU with P bit equal first_p_bit; if more than one
 *  send subsequent as command PDUs with P bit equal zero (0). 
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 *  nr : NR 
 *  first_p_bit : p_bit value of first pdu.
 * 
 * Returns : 
 *  0 
 */
us16 
conn_resend_i_pdu_as_cmd (connection_t * conn, us8 nr, us8 first_p_bit)
{
	frame_t *pdu_frame;
	pdu_sn_t *pdu;
	us16 nbr_unack_pdus;
	us8 howmany_resend=0;

	conn_remove_acked_pdus(conn,nr,&nbr_unack_pdus); 
	if (nbr_unack_pdus) {
       /*
        * process unack PDUs only if unack queue is not empty;
        * remove appropriate PDUs, fix them up, and put them on
        * mac_pdu_q. 
        */
		while (dll_remove (&conn->pdu_unack_q, (void **) &pdu_frame,
	                                                     DLL_WHERE_HEAD)==0){
			pdu = (pdu_sn_t *) pdu_frame->llc_hdr;
			pdu_set_cmd_rsp (pdu_frame, LLC_PDU_CMD);
			pdu_set_pf_bit (pdu_frame, first_p_bit);
			dll_add (&conn->mac_pdu_q, (void *) pdu_frame, DLL_WHERE_TAIL);
			first_p_bit = 0;
			conn->vS = LLC_I_GET_NS(pdu);
			howmany_resend ++;
		}
		if ( howmany_resend > 0 ){
			conn->vS = (conn->vS + 1) % LLC_2_SEQ_NBR_MODULO;
		}
		/* any PDUs to re-send are queued up; start sending to MAC */
		conn_send_pdus (conn);
	}

   	return (0);         
}


/*
 * Function : conn_resend_i_pdu_as_rsp 
 * 
 * Description : 
 *  re-send all unacknowledged I PDUs, starting with the NR; send
 *  first as response PDU with F bit equal first_f_bit; if more than one
 *  send subsequent as response PDUs with F bit equal zero (0). 
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 *  nr : NR 
 *  first_f_bit : f_bit value of first pdu.
 * 
 * Returns : 
 *  0 
 */

us16 
conn_resend_i_pdu_as_rsp (connection_t * conn, us8 nr, us8 first_f_bit)
{  

	frame_t *pdu_frame;
	pdu_sn_t *pdu;
	us16 nbr_unack_pdus;
	us8 howmany_resend=0;

	conn_remove_acked_pdus(conn,nr,&nbr_unack_pdus); 
	if (nbr_unack_pdus) {
       /*
        * process unack PDUs only if unack queue is not empty;
        * remove appropriate PDUs, fix them up, and put them on
        * mac_pdu_q 
        */
		while (dll_remove (&conn->pdu_unack_q, (void **) &pdu_frame,
                                                     DLL_WHERE_HEAD)==0){
			pdu = (pdu_sn_t *) pdu_frame->llc_hdr;
			pdu_set_cmd_rsp (pdu_frame, LLC_PDU_RSP);
			pdu_set_pf_bit (pdu_frame, first_f_bit);
			dll_add (&conn->mac_pdu_q, (void *) pdu_frame, DLL_WHERE_TAIL);
			first_f_bit = 0;
			conn->vS = LLC_I_GET_NS(pdu);
			howmany_resend ++;
		}
		if ( howmany_resend > 0 ){
			conn->vS = (conn->vS + 1) % LLC_2_SEQ_NBR_MODULO;
		}
		/* any PDUs to re-send are queued up; start sending to MAC */
		conn_send_pdus (conn);
	}

	return (0);         
}

/*
 * Function : conn_remove_acked_pdus 
 * 
 * Description : 
 *  removes acknowledged pdus from transmit queue (pdu_unack_q).
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 *  nr : NR 
 *  how_many_unacked : size of pdu_unack_q after removing acked pdus.
 * 
 * Returns : 
 *  number of pdus that removed from queue. 
 */


us16 
conn_remove_acked_pdus (connection_t * conn, us8 nr,
                                               us16 * how_many_unacked)
{
	int q_len,pdu_pos,i;
	frame_t *pdu_frame;
	pdu_sn_t *pdu;
	us16 rc;
	us16 nbr_acked=0;

	q_len = dll_query (&conn->pdu_unack_q);
	if (q_len){
		dll_peek (&conn->pdu_unack_q, (void **) &pdu_frame, 
								DLL_WHERE_HEAD);
		pdu = (pdu_sn_t *)pdu_frame->llc_hdr;

                	/* finding position of last acked pdu in queue */
		pdu_pos = ((int)LLC_2_SEQ_NBR_MODULO + (int)nr - 
				(int)LLC_I_GET_NS(pdu)) % LLC_2_SEQ_NBR_MODULO; 

		for (i=0 ; i < pdu_pos && i < q_len ; i++){   
			rc = dll_remove (&conn->pdu_unack_q, (void **) &pdu_frame, 
                                                           DLL_WHERE_HEAD);
			if (!rc && pdu_frame->free_frame_flag == YES)
				frame_skb_free(pdu_frame);
			nbr_acked ++;
		}
	}
	*how_many_unacked = dll_query(&conn->pdu_unack_q);
	return (nbr_acked);     
}

/*
 * Function : conn_send_pdus 
 * 
 * Description : 
 *  sends queued pdus to MAC layer for transmition.
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 * 
 * Returns : 
 *  0 
 */

static 
us16 conn_send_pdus (connection_t * conn)
{
	us16 rc;
	frame_t *pdu_frame;
	pdu_sn_t *pdu;

	while (dll_remove (&conn->mac_pdu_q, (void **) &pdu_frame, 
							DLL_WHERE_HEAD) == 0) {
		pdu = (pdu_sn_t *) pdu_frame->llc_hdr;
		if (!LLC_PDU_TYPE_IS_I(pdu)){
			if (is_loopback(pdu_frame->dev) != 0)
				dll_add (&conn->pdu_unack_q, (void *) pdu_frame,
								 DLL_WHERE_TAIL);
		}
		rc = mac_send_pdu (pdu_frame);
		if (rc) {
			FDBG_ERR_MSG((KERN_ERR "\nconn_send_pdus : can't 
								send pdu"));
			if (pdu_frame->free_frame_flag == YES){
				frame_skb_free(pdu_frame);
			}
		}
	}
	return (0);
}

/*
 * Function : conn_rtn_event 
 * 
 * Description : 
 *  free allocated event and returnes it to event pool of connection.
 * 
 * Parameters :
 *  connection_t *conn : pointer to active connection.
 *  conn_state_event_t *event : freed event.
 * 
 * Returns : 
 *  0 
 */


static us16 
conn_rtn_event (connection_t * conn, conn_state_event_t * event)
{
	pdu_sn_t *pdu;

	if ( event->type == CONN_EV_TYPE_PDU) {

            /* free the frame that binded to this event */
		pdu = (pdu_sn_t *)event->data.pdu.frame->llc_hdr;
		if ( LLC_PDU_TYPE_IS_I(pdu) || (event->flag == NO || 
	                                           event->ind_prim==NULL) ){ 
			frame_skb_free(event->data.pdu.frame);
		}
	}
	   /* free event structure to free list of the same */
	return (!dll_add (&conn->avail_events, event, DLL_WHERE_TAIL));
}


us16 
conn_return_event (connection_t * conn, void * event)
{
	conn_rtn_event(conn,(conn_state_event_t *)event);  
	return (0);
}


/*
 * Function : conn_next_state 
 * 
 * Description : 
 *  This function finds transition that matches with happend event, then
 *  executes related actions and finally changes state of connection.  
 * 
 * Parameters :
 *  connection_t *conn : pointer to connection.
 *  conn_state_event_t *event : happend event.
 * 
 * Returns : 
 *  0 : success.
 *  1 : failure. 
 */

static us16 
conn_next_state (connection_t * conn, conn_state_event_t * event)
{
	us16 rc = 1;
	conn_state_transition_t *transition;

	if (conn->state <= NBR_CONN_STATES) {
		transition = qualify_conn_event (conn, event);
		if (transition) {
			rc = execute_conn_transition_actions (conn, transition, 
									event);
			if (!rc && transition->next_state != NO_STATE_CHANGE) {
				conn->state = transition->next_state;
			}
		} else {
			rc = 0;
		}
	}

	return (rc);
}


/*
 * Function : qualify_conn_event 
 * 
 * Description : 
 *  This function finds transition that matches with happend event.
 * 
 * Parameters :
 *  connection_t *conn : pointer to connection.
 *  sap_state_event_t *event : happend event.
 * 
 * Returns : 
 *  pointer to found transition : success.
 *  NULL : failure.
 *   
 */

static conn_state_transition_t * 
qualify_conn_event (connection_t *conn, conn_state_event_t *event)
{
	conn_state_t *curr_state;
	conn_state_transition_t **next_transition;
	conn_event_qfyr_t *next_qualifier;

	curr_state = &Connection_state_table [conn->state - 1]; 

       /*
   	* search thru events for this state until list exhausted or until 
    	* no more
    	*/
	for (next_transition = curr_state->transitions+
			find_offset(conn->state-1,event->type); 
				(*next_transition)->event; next_transition++) {
		if (!((*next_transition)->event) (conn, event)) {
           /*
            * got POSSIBLE event match; the event may require qualification
            * based on the values of a number of state flags; if all
            * qualifications are met (i.e., if all qualifying functions
            * return success, or 0, then this is THE event we're looking for
            */
			for (next_qualifier = (*next_transition)->event_qualifiers;
				next_qualifier && *next_qualifier && 
					!((*next_qualifier) (conn, event));
                                                           next_qualifier++);
				if (!next_qualifier || !(*next_qualifier)) {
  	             /* 
  	              * all qualifiers executed successfully; this is our
  	              * transition; return it so we can perform the associated
  	              * actions and change the state
  	              */
					return (*next_transition);
				}
		}
	}

	return ((conn_state_transition_t *) NULL);
}


/*
 * Function : execute_conn_transition_actions 
 * 
 * Description : 
 *  This function executes actions that is related to happend event. 
 * 
 * Parameters :
 *  connection_t *conn : pointer to connection.
 *  conn_state_transition_t *transition : pointer to transition that it's
 *  ations must be performed. 
 *  conn_state_event_t *event : happend event.
 * 
 * Returns : 
 *  0 : success.
 *  1 : failure of at least one action.
 *   
 */

static us16 
execute_conn_transition_actions (connection_t * conn,
                               conn_state_transition_t * transition,
                                              conn_state_event_t * event)
{
	us16 rc = 0;  
	conn_action_t *next_action;

	for (next_action = transition->event_actions; next_action && 
						*next_action; next_action++) {
		if ((*next_action) (conn, event)) {
			rc = 1;
			FDBG_ERR_MSG((KERN_ERR "\nat least one action (conn) failed\n"));
		}
	}
	return (rc);
}



static us16 
find_conn_match (void * this, void * match_value)
{
	us16 rc = 1;
	connection_t *conn = (connection_t *) this;
	address_t *remote_addr = (address_t *) match_value;

	if (!memcmp (conn->remote_dl_addr.mac, remote_addr->mac, 
								MAC_ADDR_LEN)) {
		rc = 0;
	}
	return (rc);
}

/*
 * Function : find_conn 
 * 
 * Description : 
 *  This function search connection list of the SAP and finds connection
 *  based on remote MAC and SAP address. 
 * 
 * Parameters :
 *  SAP_t *sap : pointer to SAP.
 *  address_t *remote_addr : address of remote LLC (MAC+SAP).
 *  void **conn_ptr : found connection.
 * 
 * Returns : 
 *  0 : success.
 *  1 : connection not found.
 *   
 */

us16 
find_conn (sap_t *sap, address_t *remote_addr, void **conn_ptr)
{
	us16 rc;
	rc = dll_match (&sap->connection_list, find_conn_match,
                               (void *) remote_addr, (void **) conn_ptr,
                                         (DLL_MATCH_PEEK | DLL_MATCH_ONE));
	return rc;
}

/*
 * Function : data_accept_state  
 * 
 * Description : 
 *  This function designates that in this state we can send data or not.
 * 
 * Parameters :
 *  us8 state : state of connection. 
 * 
 * Returns : 
 *  0 : OK.
 *  1 : No,we can't.
 *   
 */

us8 
data_accept_state (us8 state)
{
	if ( state != CONN_STATE_NORMAL && state != CONN_STATE_BUSY &&
                                             state != CONN_STATE_REJECT ){
	          /* data_conn_refuse */ 
		return 1;
	}
	return 0;
}

/*
 * Function : find_next_offset  
 * 
 * Description : 
 *  This function finds offset of next category of transitions in transition
 *  table.
 * 
 * Parameters :
 *  conn_state_t *state : pointer to state table. 
 *  us16 offset : start offset.
 * 
 * Returns : 
 *  start index of next category.
 *   
 */

us16 
find_next_offset(conn_state_t *state, us16 offset)
{
	us16 cnt=0;
	conn_state_transition_t ** next_transition;

	for (next_transition = state->transitions+offset;
                             (*next_transition)->event; next_transition++){
		cnt ++;
	}
	return cnt;
}

/*
 * Function : build_offset_table  
 * 
 * Description : 
 *  This function fills offset table of connection state transition table 
 *  (Offset_table).
 * 
 * Parameters :
 *  none.
 * 
 * Returns : 
 *  none.
 *   
 */

void 
build_offset_table(void)
{
	conn_state_t *curr_state;
	us16 state,ev_type,next_offset;

	memset(Offset_table, 0, sizeof(Offset_table));
	for (state = 0; state < NBR_CONN_STATES; state++) {
		curr_state = &Connection_state_table[state];
		next_offset = 0; 
		for (ev_type = 0; ev_type < NBR_CONN_EV; ev_type++) {
			Offset_table[state][ev_type] = next_offset;
			next_offset += find_next_offset(curr_state, 
							next_offset) + 1;
		}
	} 
}

/*
 * Function : find_offset  
 * 
 * Description : 
 *  This function finds start offset of desired category of transitions.
 * 
 * Parameters :
 *  us16 state : state of connection.
 *  us16 event_type : type of happend event.
 * 
 * Returns : 
 *  desired start offset. 
 *   
 */

us16 
find_offset(us16 state, us16 event_type)
{
    /* at this stage, Offset_table[..][2] is not important. it is
       for init_pf_cycle and I don't know what is it.
    */
	switch (event_type){
		case CONN_EV_TYPE_PRIM :
			return Offset_table[state][0]; 
		case CONN_EV_TYPE_PDU :
			return Offset_table[state][4];
		case CONN_EV_TYPE_SIMPLE :
			return Offset_table[state][1];
		case CONN_EV_TYPE_P_TIMER :
		case CONN_EV_TYPE_ACK_TIMER :
		case CONN_EV_TYPE_REJ_TIMER :
		case CONN_EV_TYPE_BUSY_TIMER :
			return Offset_table[state][3];
		default :
			return 0;
	}
}
