/*  
    (C) 2012-13 Digital Devices GmbH. 

    This file is part of the octoserve SAT>IP server.

    Octoserve 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 3 of the License, or
    (at your option) any later version.

    Octoserve 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 octoserve.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "octoserve.h"

extern uint32_t debug;

static uint16_t cs16(uint8_t *p, uint32_t len)
{
	uint32_t cs;

	cs = (p[0] << 8) | p[1];
	p += 4;
	len -= 4;
	for (; len > 0; len -= 2, p += 2) 
		cs += (p[0] << 8) | p[1];
	if (len)
		cs += p[0];

	while (cs >> 16)
		cs = (cs & 0xffff) + (cs >> 16);
	return ~cs;
}

void proc_igmp(struct octoserve *os, uint8_t *b, int l, uint8_t *macheader)
{
	int hl = (b[0] & 0xf) * 4, pl, rl, i, j, ns, al;
	uint8_t cmd, *r, *s, type, *p;
	uint16_t n;
	char sd[64], mc[256], *mac = 0;
	uint16_t cs;

	if (macheader)
		mac = macheader + 6;
	if (hl + 1 > l)
		return;
	
//22:45:32.073241 IP edge > 239.7.1.1: igmp v2 report 239.7.1.1
//0x0000:  0100 5e07 0101 089e 01b2 03c9 0800 46c0
//0x0010:  0020 0000 4000 0102 3159 c0a8 020e ef07
//0x0020:  0101 9404 0000 1600 f9f6 ef07 0101
	dump(b, l);
	sprintf(sd, "%03d.%03d.%03d.%03d -> %03d.%03d.%03d.%03d", 
		b[12], b[13], b[14], b[15],
		b[16], b[17], b[18], b[19]);
	
	p = b + hl;
	pl = l - hl;
	if (pl < 4)
		return;
	if (cs16(p, pl) != ((p[2] << 8) | p[3])) {
		dbgprintf(DEBUG_IGMP, "IGMP CS error\n");
#if 0
		if (debug & DEBUG_IGMP)
			dump(b, l);
#endif
		return;
	}

	cmd = b[hl];
	switch (cmd) {
	case 0x11:
	{
		uint32_t a1, a2;
		uint8_t *s;
		
		if (pl < 12)
			return;
		dbgprintf(DEBUG_IGMP, "IGMP: query by %s\n", sd);
		s = (uint8_t *) &(((struct sockaddr_in *) &os->ssdp.sadr)->sin_addr);
		a1 = (b[12] << 24) | (b[13] << 16) | (b[14] << 8) | b[15];
		a2 = (s[0] << 24) | (s[1] << 16) | (s[2] << 8) | s[3];
		dbgprintf(DEBUG_IGMP, "%08x < %08x ? %s\n", a1, a2,
			  (a1 < a2) ? "yes" : "no");
		if (a1 < a2) {
			/* somebody else with lower IP is already sending queries */
			os->igmp_mode = 3;
			mtime(&os->igmp_time);
			os->igmp_tag++;
			os->igmp_timeout = b[hl + 1] / 10 + 1;
			dbgprintf(DEBUG_IGMP, "IGMP slave, tag = %u, timeout = %u\n",
				  os->igmp_tag, os->igmp_timeout);
		} else
			dbgprintf(DEBUG_IGMP, "IGMP master, tag = %u\n", os->igmp_tag);
		break;
	}
	case 0x12:
		if (pl < 8)
			return;
		dbgprintf(DEBUG_IGMP, "IGMPV1: %s : %d.%d.%d.%d\n", sd,
			  b[hl + 4], b[hl + 5], b[hl + 6], b[hl + 7]);
		mc_join(os, b + 12, mac, &b[hl + 4]);
		break;
	case 0x16:
		if (pl < 8)
			return;
		dbgprintf(DEBUG_IGMP, "IGMPV2: %s : %d.%d.%d.%d\n", sd,
			  b[hl + 4], b[hl + 5], b[hl + 6], b[hl + 7]);
		mc_join(os, b + 12, mac, &b[hl + 4]);
		break;
	case 0x17:
		if (pl < 8)
			return;
		dbgprintf(DEBUG_IGMP, "IGMPL: %s : %d.%d.%d.%d\n", sd,
			  b[hl + 4], b[hl + 5], b[hl + 6], b[hl + 7]);
		mc_leave(os, b + 12, &b[hl + 4]);
		break;
	case 0x22:
		if (pl < 8)
			return;
		pl -= 8;
		//dump(b + hl, l - hl);
		n = (b[hl + 6] << 8) | b[hl + 7];
		dbgprintf(DEBUG_IGMP, "IGMPV3: %s, %d records:\n", sd, n);
		for (i = 0, r = &b[hl + 8]; i < n; i++, r += rl, pl -= rl) {
			if (pl < 4)
				return;
			ns = (r[2] << 8) | r[3];
			al = r[1] * 4;
			rl = 8 + ns * 4 + al;
			if (pl < rl)
				return;
			type = r[0];
			dbgprintf(DEBUG_IGMP, "type %d, %d sources, %d aux:\n", type, ns, al);
			if (ns) {
				dbgprintf(DEBUG_IGMP, "cannot handle sources\n");
				continue;
			}
			if (type == 2) {
				dbgprintf(DEBUG_IGMP, "IGMPV3: %s subscribed to %d\n", sd, ns);
				mc_join(os, b + 12, mac, &r[4]);
			}
			if (type == 4) {
				dbgprintf(DEBUG_IGMP, "IGMPV3: %s, change to Exclude %d (Join)\n",
					  sd, ns);
				mc_join(os, b + 12, mac, &r[4]);
			}
			if (type == 3) {
				dbgprintf(DEBUG_IGMP, "IGMPV3: %s, change to Include %d (Leave)\n",
					  sd, ns);
				mc_leave(os, b + 12, &r[4]);
			}
			//printf("MC: %d.%d.%d.%d\n", r[4], r[5], r[6], r[7]);
			for (j = 0, s = &r[8]; j < ns; j++, s += 4) {
				dbgprintf(DEBUG_IGMP, "%d.%d.%d.%d\n", s[0], s[1], s[2], s[3]);
			}
		}
		break;
	}
	//dump(b + hl, l - hl);
}

static void calc_cs(uint8_t *m, uint32_t len)
{
	uint32_t cs  = 0, i;
	
	for (i = 0; i < len - 1; i += 2)
		cs += (m[i] << 8) | m[i + 1];
	if (i < len)
		cs += m[i] << 8;
	cs = (cs >> 16) + (cs & 0xffff);
	cs = (cs >> 16) + (cs & 0xffff);
	cs ^= 0xffff;
	m[2] = cs >> 8;
	m[3] = cs & 0xff;
}

/*22:42:32.973024 IP octonet > all-systems.mcast.net: igmp query v3
0x0000:  0100 5e00 0001 5484 7b00 141a 0800 4600
0x0010:  0024 6682 4000 0102 db58 c0a8 024f e000
0x0020:  0001 9404 0000 1164 ec1e 0000 0000 027d
0x0030:  0000 0000 0000 0000 0000 0000
22:41:49.696510 IP fuckoff > all-systems.mcast.net: igmp query v2
0x0000:  0100 5e00 0001 30b5 c2aa 24b6 0800 46c0
0x0010:  0020 0000 4000 0102 40a6 c0a8 02c8 e000
0x0020:  0001 9404 0000 1164 ee9b 0000 0000 0000
0x0030:  0000 0000 0000 0000 0000 0000
*/

void send_igmp_query(struct octoserve *os, uint8_t *group, uint8_t timeout)
{
	uint8_t msg[] = { 0x11, 0x64, 0x00, 0x00, 
			  0x00, 0x00, 0x00, 0x00,
			  0x02, 0x7d, 0x00, 0x00
	};
	struct sockaddr_in insadr;
	uint8_t zero = 0, ra[4] = { 0x94, 0x04, 0x00, 0x00 };
	
	memset(&insadr, 0, sizeof(insadr));
	insadr.sin_family = AF_INET;
	insadr.sin_port = IPPROTO_IGMP;
	//insadr.sin_addr.s_addr = inet_addr("224.0.0.22");
	insadr.sin_addr.s_addr = inet_addr("224.0.0.1");

	if (group) {
		msg[4] = group[0];
		msg[5] = group[1];
		msg[6] = group[2];
		msg[7] = group[3];
		memcpy(&insadr.sin_addr.s_addr, group, 4);
	}
	if (timeout) 
		msg[1] = timeout;

	calc_cs(msg, sizeof(msg));
	
	setsockopt(os->igmp_sock, IPPROTO_IP, IP_OPTIONS, ra, sizeof(ra));
	sendto(os->igmp_sock, msg, sizeof(msg), 0, (struct sockaddr *) &insadr, sizeof(insadr));
	dbgprintf(DEBUG_IGMP, "Queried group %03u.%03u.%03u.%03u\n",
		  msg[4],  msg[5], msg[6], msg[7]);
}

void check_igmp(struct octoserve *os)
{
	time_t tdiff, t;

	tdiff = mtime(&t) - os->igmp_time;
	switch (os->igmp_mode) {
	case 0:
		if (tdiff > 124) {
			os->igmp_timeout = 10;
			dbgprintf(DEBUG_IGMP,
				  "%u: IGMP master query, tag = %u, timeout = %u\n",
				  t, os->igmp_tag, os->igmp_timeout);
			os->igmp_tag++;
			send_igmp_query(os, 0, 0);
			os->igmp_time = t;
			os->igmp_mode = 1;
#if 0
			if (os->igmp_robust) {
				os->igmp_robust--;
				os->igmp_time -= 94;
			}
#endif
		}
		break;
	case 1:
		if (tdiff > os->igmp_timeout) {
			dbgprintf(DEBUG_IGMP, "%u: IGMP timeout, tag = %u, tdiff = %u\n",
				  t, os->igmp_tag, tdiff);
			os->igmp_mode = 0;
		}
		break;
	case 2:
		/* check if query master timed out */
		if (tdiff < 255)
			return;
		/* yes, so we will have to query from now on */
		os->igmp_mode = 0;
		os->igmp_time = t - 94;
		os->igmp_robust = 1;
		break;
	case 3:
		if (tdiff > os->igmp_timeout) {
			dbgprintf(DEBUG_IGMP, "%u: IGMP timeout, tag = %u\n", t, os->igmp_tag);
			os->igmp_mode = 2;
		}
		break;
	}
}
