/**
 *			GPAC - Multimedia Framework C SDK
 *
 *			Authors: Pierre Souchay, Jean Le Feuvre, Romain Bouqueau
 *			Copyright (c) Telecom ParisTech 2010-2012, Romain Bouqueau
 *					All rights reserved
 *
 *  This file is part of GPAC
 *
 *  GPAC is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation; either version 2, or (at your option)
 *  any later version.
 *
 *  GPAC 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#define _GNU_SOURCE

#include <gpac/internal/m3u8.h>
#include <gpac/network.h>

/********** accumulated_attributes **********/

typedef struct _s_accumulated_attributes {
	//TODO: store as a structure with: { attribute, version, mandatory }
	char *title;
	char *mediaURL;
	double duration_in_seconds;
	int bandwidth;
	int width, height;
	int stream_id;
	char *codecs;
	char *language;
	MediaType type;
	union {
		char *audio;
		char *video;
		char *subtitle;
		char *closed_captions;
	} group;
	int target_duration_in_seconds;
	int min_media_sequence;
	int current_media_seq;
	int version;
	int compatibility_version; /*compute version required by the M3U8 content*/
	Bool is_master_playlist;
	Bool is_media_segment;
	Bool is_playlist_ended;
	u64 playlist_utc_timestamp;
	u64 byte_range_start, byte_range_end;
	u64 init_byte_range_start, init_byte_range_end;
	PlaylistElementDRMMethod key_method;
	char *init_url;
	char *key_url;
	bin128 key_iv;
	Bool independent_segments;
} s_accumulated_attributes;


/********** playlist_element **********/

GF_Err playlist_element_del(PlaylistElement * e);

static GF_Err cleanup_list_of_elements(GF_List *list) {
	GF_Err result = GF_OK;
	if (list == NULL)
		return result;
	while (gf_list_count(list)) {
		PlaylistElement *pl = (PlaylistElement *) gf_list_get(list, 0);
		if (pl)
			result |= playlist_element_del(pl);
		gf_list_rem(list, 0);
	}
	gf_list_del(list);
	return result;
}

/**
 * Deletes an Playlist element
 */
GF_Err playlist_element_del(PlaylistElement * e) {
	GF_Err result = GF_OK;
	if (e == NULL)
		return result;
	if (e->title) {
		gf_free(e->title);
	}
	if (e->codecs) {
		gf_free(e->codecs);
	}
	if (e->language) {
		gf_free(e->language);
	}
	if (e->audio_group) {
		gf_free(e->audio_group);
	}
	if (e->video_group) {
		gf_free(e->video_group);
	}
	if (e->key_uri) {
		gf_free(e->key_uri);
	}
	if (e->init_segment_url) {
		gf_free(e->init_segment_url);
	}
	memset(e->key_iv, 0, sizeof(bin128) );
	if (e->url) 
		gf_free(e->url);

	switch (e->element_type) {
	case TYPE_UNKNOWN:
	case TYPE_MEDIA:
		break;
	case TYPE_PLAYLIST:
		assert(e->element.playlist.elements);
		result |= cleanup_list_of_elements(e->element.playlist.elements);
	default:
		break;
	}
	gf_free(e);
	return result;
}

/**
 * Creates an Playlist element.
 * This element can be either a playlist of a stream according to first parameter.
\param NULL if element could not be created. Elements will be deleted recursively.
 */
static PlaylistElement* playlist_element_new(PlaylistElementType element_type, const char *url, s_accumulated_attributes *attribs)
{
	PlaylistElement *e;
	GF_SAFEALLOC(e, PlaylistElement);
	if (e == NULL)
		return NULL;

	e->media_type = attribs->type;
	e->duration_info = attribs->duration_in_seconds;
	e->byte_range_start = attribs->byte_range_start;
	e->byte_range_end = attribs->byte_range_end;

	e->title = (attribs->title ? gf_strdup(attribs->title) : NULL);
	e->codecs = (attribs->codecs ? gf_strdup(attribs->codecs) : NULL);
	e->language = (attribs->language ? gf_strdup(attribs->language) : NULL);
	e->drm_method = attribs->key_method;
	e->init_segment_url = attribs->init_url ? gf_strdup(attribs->init_url) : NULL;
	e->init_byte_range_start = attribs->init_byte_range_start;
	e->init_byte_range_end = attribs->init_byte_range_end;
	e->key_uri = (attribs->key_url ? gf_strdup(attribs->key_url) : NULL);
	memcpy(e->key_iv, attribs->key_iv, sizeof(bin128));

	e->utc_start_time = attribs->playlist_utc_timestamp;

	assert(url);
	e->url = gf_strdup(url);
	e->bandwidth = 0;
	e->element_type = element_type;
	if (element_type == TYPE_PLAYLIST) {
		e->element.playlist.is_ended = GF_FALSE;
		e->element.playlist.target_duration = attribs->duration_in_seconds;
		e->element.playlist.current_media_seq = 0;
		e->element.playlist.media_seq_min = 0;
		e->element.playlist.media_seq_max = 0;
		e->element.playlist.elements = gf_list_new();
		if (NULL == (e->element.playlist.elements)) {
			if (e->title)
				gf_free(e->title);
			if (e->codecs)
				gf_free(e->codecs);
			if (e->language)
				gf_free(e->language);
			if (e->audio_group)
				gf_free(e->audio_group);
			if (e->video_group)
				gf_free(e->video_group);
			if (e->url)
				gf_free(e->url);
			if (e->init_segment_url)
				gf_free(e->init_segment_url);
			if (e->key_uri)
				gf_free(e->key_uri);
			e->url = NULL;
			e->title = NULL;
			e->codecs = NULL;
			e->language = NULL;
			e->audio_group = NULL;
			e->video_group = NULL;
			e->key_uri = NULL;
			memset(e->key_iv, 0, sizeof(bin128));
			gf_free(e);
			return NULL;
		}
	} else {
		/* Nothing to do, media is an empty structure */
	}
	assert(e->bandwidth == 0);
	assert(e->url);
	return e;
}


/********** stream **********/

/**
 * Creates a new stream properly initialized
 */
static Stream* stream_new(int stream_id) {
	Stream *program = (Stream *) gf_malloc(sizeof(Stream));
	if (program == NULL) {
		return NULL;
	}
	program->stream_id = stream_id;
	program->variants = gf_list_new();
	if (program->variants == NULL) {
		gf_free(program);
		return NULL;
	}
	return program;
}

/**
 * Deletes the specified stream
 */
static GF_Err stream_del(Stream *stream) {
	GF_Err e = GF_OK;
	if (stream == NULL)
		return e;
	if (stream->variants) {
		while (gf_list_count(stream->variants)) {
			GF_List *l = gf_list_get(stream->variants, 0);
			cleanup_list_of_elements(l);
			gf_list_rem(stream->variants, 0);
		}
		gf_list_del(stream->variants);
	}
	stream->variants = NULL;
	gf_free(stream);
	return e;
}



static GFINLINE int string2num(const char *s) {
	u64 ret=0, i, shift=2;
	u8 hash[GF_SHA1_DIGEST_SIZE];
	gf_sha1_csum((u8*)s, (u32)strlen(s), hash);
	assert(shift*GF_SHA1_DIGEST_SIZE < 64);
	for (i=0; i<GF_SHA1_DIGEST_SIZE; ++i)
		ret += (ret << shift) + hash[i];
	return (int)(ret % MEDIA_TYPE_AUDIO);
}


#define GROUP_ID_TO_PROGRAM_ID(type, group_id) (\
	MEDIA_TYPE_##type + \
	string2num(group_id) \
	) \
 
static Bool safe_start_equals(const char *attribute, const char *line) {
	size_t len, atlen;
	if (line == NULL)
		return GF_FALSE;
	len = strlen(line);
	atlen = strlen(attribute);
	if (len < atlen)
		return GF_FALSE;
	return (0 == strncmp(attribute, line, atlen));
}


static void reset_attributes(s_accumulated_attributes *attributes)
{
	memset(attributes, 0, sizeof(s_accumulated_attributes));
	attributes->type = MEDIA_TYPE_UNKNOWN;
	attributes->min_media_sequence = 1;
	attributes->version = 1;
	attributes->compatibility_version = 0;
	attributes->key_method = DRM_NONE;
}

static char** extract_attributes(const char *name, const char *line, const int num_attributes) {
	int sz, i, curr_attribute, start;
	char **ret;
	u8 quote = 0;
	int len = (u32) strlen(line);
	start = (u32) strlen(name);
	if (len <= start)
		return NULL;
	if (!safe_start_equals(name, line))
		return NULL;
	ret = gf_calloc((num_attributes + 1), sizeof(char*));
	curr_attribute = 0;
	for (i=start; i<=len; i++) {
		if (line[i] == '\0' || (!quote && line[i] == ',')  || (line[i] == quote)) {
			u32 spaces = 0;
			sz = i - start;
			if (quote && (line[i] == quote))
				sz++;
			
			while (line[start+spaces] == ' ')
				spaces++;
			if ((sz-spaces<=1) && (line[start+spaces]==',')) {
				//start = i+1;
			} else {
				if (!strncmp(&line[start+spaces], "\t", sz-spaces) || !strncmp(&line[start+spaces], "\n", sz-spaces)) {
				} else {
					ret[curr_attribute] = gf_calloc( (1+sz-spaces), sizeof(char));
					strncpy(ret[curr_attribute], &(line[start+spaces]), sz-spaces);
					curr_attribute++;
				}
			}
			start = i+1;
			
			if (start == len) {
				return ret;
			}
		}
		if ((line[i] == '\'') || (line[i] == '"'))  {
			if (quote) {
				quote = 0;
			} else {
				quote = line[i];
			}
		}
	}
	if (curr_attribute == 0) {
		gf_free(ret);
		return NULL;
	}
	return ret;
}

#define M3U8_COMPATIBILITY_VERSION(v) \
	if (v > attributes->compatibility_version) \
		attributes->compatibility_version = v;

/**
 * Parses the attributes and accumulate into the attributes structure
 */
static char** parse_attributes(const char *line, s_accumulated_attributes *attributes) {
	int int_value, i;
	char **ret;
	char *end_ptr;
	if (line == NULL)
		return NULL;
	if (!safe_start_equals("#EXT", line))
		return NULL;
	if (safe_start_equals("#EXT-X-ENDLIST", line)) {
		attributes->is_playlist_ended = GF_TRUE;
		M3U8_COMPATIBILITY_VERSION(1);
		return NULL;
	}
	/* reset not accumated attributes */
	attributes->type = MEDIA_TYPE_UNKNOWN;

	ret = extract_attributes("#EXT-X-TARGETDURATION:", line, 1);
	if (ret) {
		/* #EXT-X-TARGETDURATION:<seconds> */
		if (ret[0]) {
			int_value = (s32) strtol(ret[0], &end_ptr, 10);
			if (end_ptr != ret[0]) {
				attributes->target_duration_in_seconds = int_value;
			}
		}
		M3U8_COMPATIBILITY_VERSION(1);
		return ret;
	}
	ret = extract_attributes("#EXT-X-MEDIA-SEQUENCE:", line, 1);
	if (ret) {
		/* #EXT-X-MEDIA-SEQUENCE:<number> */
		if (ret[0]) {
			int_value = (s32)strtol(ret[0], &end_ptr, 10);
			if (end_ptr != ret[0]) {
				attributes->min_media_sequence = int_value;
				attributes->current_media_seq = int_value;
			}
		}
		M3U8_COMPATIBILITY_VERSION(1);
		return ret;
	}
	ret = extract_attributes("#EXT-X-VERSION:", line, 1);
	if (ret) {
		/* #EXT-X-VERSION:<number> */
		if (ret[0]) {
			int_value = (s32)strtol(ret[0], &end_ptr, 10);
			if (end_ptr != ret[0]) {
				attributes->version = int_value;
			}
			//although technically it is mandated for v2 or more, don't complain if set for v1
			M3U8_COMPATIBILITY_VERSION(1);
		}
		return ret;
	}
	ret = extract_attributes("#EXTINF:", line, 2);
	if (ret) {
		M3U8_COMPATIBILITY_VERSION(1);
		/* #EXTINF:<duration>,<title> */
		attributes->is_media_segment = GF_TRUE;
		if (ret[0]) {
			double double_value = strtod(ret[0], &end_ptr);
			if (end_ptr != ret[0]) {
				attributes->duration_in_seconds = double_value;
			}
			if (strstr(ret[0], ".") || (double_value > (int)double_value)) {
				M3U8_COMPATIBILITY_VERSION(3);
			}
		}
		if (ret[1]) {
			if (attributes->title) gf_free(attributes->title);
			attributes->title = gf_strdup(ret[1]);
		}
		return ret;
	}
	ret = extract_attributes("#EXT-X-KEY:", line, 4);
	if (ret) {
		/* #EXT-X-KEY:METHOD=<method>[,URI="<URI>"] */
		const char *method = "METHOD=";
		const size_t method_len = strlen(method);
		if (safe_start_equals(method, ret[0])) {
			if (strncmp(ret[0]+method_len, "NONE", 4)) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] EXT-X-KEY not supported.\n", line));
			}
			attributes->key_method = DRM_AES_128;
			if (ret[1] != NULL && safe_start_equals("URI=\"", ret[1])) {
				int_value = (u32) strlen(ret[1]);
				if (ret[1][int_value-1] == '"') {
					if (attributes->key_url) gf_free(attributes->key_url);
					attributes->key_url = gf_strdup(&(ret[1][4]));
				}
			}
		}
		M3U8_COMPATIBILITY_VERSION(1);
		return ret;
	}
	ret = extract_attributes("#EXT-X-PROGRAM-DATE-TIME:", line, 1);
	if (ret) {
		/* #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ> */
		if (ret[0]) attributes->playlist_utc_timestamp = gf_net_parse_date(ret[0]);
		M3U8_COMPATIBILITY_VERSION(1);
		return ret;
	}
	ret = extract_attributes("#EXT-X-ALLOW-CACHE:", line, 1);
	if (ret) {
		/* #EXT-X-ALLOW-CACHE:<YES|NO> */
		GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-ALLOW-CACHE not supported.\n", line));
		M3U8_COMPATIBILITY_VERSION(1);
		return ret;
	}
	ret = extract_attributes("#EXT-X-PLAYLIST-TYPE", line, 1);
	if (ret) {
		if (ret[0] && !strcmp(ret[0], "VOD")) attributes->is_playlist_ended = GF_TRUE;
		M3U8_COMPATIBILITY_VERSION(3);
		return ret;
	}
	ret = extract_attributes("#EXT-X-MAP", line, 4);
	if (ret) {
		/* #EXT-X-MAP:URI="<URI>"] */
		i=0;
		while (ret[i] != NULL) {
			char *val = ret[i];
			if (val[0]==':') val++;
			if (safe_start_equals("URI=\"", val)) {
				char *uri = val + 5;
				int_value = (u32) strlen(uri);
				if (uri[int_value-1] == '"') {
					if (attributes->init_url) gf_free(attributes->init_url);
					attributes->init_url = gf_strdup(uri);
					attributes->init_url[int_value-1]=0;
				}
			}
			else if (safe_start_equals("BYTERANGE=\"", val)) {
				u64 begin, size;
				val+=10;
				if (sscanf(val, "\""LLU"@"LLU"\"", &size, &begin) == 2) {
					if (size) {
						attributes->init_byte_range_start = begin;
						attributes->init_byte_range_end = begin + size - 1;
					} else {
						GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid byte range %s\n", val));
					}
				}
			}
			i++;
		}
		M3U8_COMPATIBILITY_VERSION(3);
		return ret;
	}
	ret = extract_attributes("#EXT-X-STREAM-INF:", line, 10);
	if (ret) {
		/* #EXT-X-STREAM-INF:[attribute=value][,attribute=value]* */
		i = 0;
		attributes->is_master_playlist = GF_TRUE;
		M3U8_COMPATIBILITY_VERSION(1);
		while (ret[i] != NULL) {
			char *utility;
			if (safe_start_equals("BANDWIDTH=", ret[i])) {
				utility = &(ret[i][10]);
				int_value = (s32) strtol(utility, &end_ptr, 10);
				if (end_ptr != utility)
					attributes->bandwidth = int_value;
			} else if (safe_start_equals("PROGRAM-ID=", ret[i])) {
				utility = &(ret[i][11]);
				int_value = (s32) strtol(utility, &end_ptr, 10);
				if (end_ptr != utility)
					attributes->stream_id = int_value;
			} else if (safe_start_equals("CODECS=\"", ret[i])) {
				int_value = (u32) strlen(ret[i]);
				if (ret[i][int_value-1] == '"') {
					if (attributes->codecs) gf_free(attributes->codecs);
					attributes->codecs = gf_strdup(&(ret[i][7]));
				}
			} else if (safe_start_equals("RESOLUTION=", ret[i])) {
				u32 w, h;
				utility = &(ret[i][11]);
				if ((sscanf(utility, "%dx%d", &w, &h)==2) || (sscanf(utility, "%dx%d,", &w, &h)==2)) {
					attributes->width = w;
					attributes->height = h;
				}
				M3U8_COMPATIBILITY_VERSION(2);
			} else if (safe_start_equals("AUDIO=", ret[i])) {
				assert(attributes->type == MEDIA_TYPE_UNKNOWN);
				attributes->type = MEDIA_TYPE_AUDIO;
				if (attributes->group.audio) gf_free(attributes->group.audio);
				attributes->group.audio = gf_strdup(ret[i] + 6);
				M3U8_COMPATIBILITY_VERSION(4);
			} else if (safe_start_equals("VIDEO=", ret[i])) {
				assert(attributes->type == MEDIA_TYPE_UNKNOWN);
				attributes->type = MEDIA_TYPE_VIDEO;
				if (attributes->group.video) gf_free(attributes->group.video);
				attributes->group.video = gf_strdup(ret[i] + 6);
				M3U8_COMPATIBILITY_VERSION(4);
			}
			i++;
		}
		if (!attributes->bandwidth) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-STREAM-INF: no BANDWIDTH found. Ignoring the line.\n"));
			return NULL;
		}
		return ret;
	}
	ret = extract_attributes("#EXT-X-DISCONTINUITY", line, 0);
	if (ret) {
		/* #EXT-X-DISCONTINUITY */
		GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] EXT-X-DISCONTINUITY not supported.\n", line));
		M3U8_COMPATIBILITY_VERSION(1);
		return ret;
	}
	ret = extract_attributes("#EXT-X-BYTERANGE:", line, 1);
	if (ret) {
		/* #EXT-X-BYTERANGE:<begin@end> */
		if (ret[0]) {
			u64 begin, size;
			if (sscanf(ret[0], LLU"@"LLU, &size, &begin) == 2) {
				if (size) {
					attributes->byte_range_start = begin;
					attributes->byte_range_end = begin + size - 1;
				} else {
					GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid byte range %s\n", ret[0]));
				}
			}
		}
		M3U8_COMPATIBILITY_VERSION(4);
		return ret;
	}
	ret = extract_attributes("#EXT-X-MEDIA:", line, 14);
	if (ret) {
		/* #EXT-X-MEDIA:[TYPE={AUDIO,VIDEO}],[URI],[GROUP-ID],[LANGUAGE],[NAME],[DEFAULT={YES,NO}],[AUTOSELECT={YES,NO}] */
		M3U8_COMPATIBILITY_VERSION(4);
		attributes->is_master_playlist = GF_TRUE;
		i = 0;
		while (ret[i] != NULL) {
			if (safe_start_equals("TYPE=", ret[i])) {
				if (!strncmp(ret[i]+5, "AUDIO", 5)) {
					attributes->type = MEDIA_TYPE_AUDIO;
				} else if (!strncmp(ret[i]+5, "VIDEO", 5)) {
					attributes->type = MEDIA_TYPE_VIDEO;
				} else if (!strncmp(ret[i]+5, "SUBTITLES", 9)) {
					attributes->type = MEDIA_TYPE_SUBTITLES;
				} else if (!strncmp(ret[i]+5, "CLOSED-CAPTIONS", 15)) {
					attributes->type = MEDIA_TYPE_CLOSED_CAPTIONS;
				} else {
					GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unsupported #EXT-X-MEDIA:TYPE=%s\n", ret[i]+5));
				}
			} else if (safe_start_equals("URI=\"", ret[i])) {
				size_t len;
				if (attributes->mediaURL) gf_free(attributes->mediaURL);
				attributes->mediaURL = gf_strdup(ret[i]+5);
				len = strlen(attributes->mediaURL);
				if (len && (attributes->mediaURL[len-1] == '"')) {
					attributes->mediaURL[len-1] = '\0';
				} else {
					GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Misformed #EXT-X-MEDIA:URI=%s. Quotes are incorrect.\n", ret[i]+5));
				}
			} else if (safe_start_equals("GROUP-ID=", ret[i])) {
				if (attributes->type == MEDIA_TYPE_AUDIO) {
					if (attributes->group.audio) gf_free(attributes->group.audio);
					attributes->group.audio = gf_strdup(ret[i]+9);
					attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(AUDIO, attributes->group.audio);
				} else if (attributes->type == MEDIA_TYPE_VIDEO) {
					if (attributes->group.video) gf_free(attributes->group.video);
					attributes->group.video = gf_strdup(ret[i]+9);
					attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(VIDEO, attributes->group.video);
				} else if (attributes->type == MEDIA_TYPE_SUBTITLES) {
					if (attributes->group.subtitle) gf_free(attributes->group.subtitle);
					attributes->group.subtitle = gf_strdup(ret[i]+9);
					attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(SUBTITLES, attributes->group.subtitle);
				} else if (attributes->type == MEDIA_TYPE_CLOSED_CAPTIONS) {
					if (attributes->group.closed_captions) gf_free(attributes->group.closed_captions);
					attributes->group.closed_captions = gf_strdup(ret[i]+9);
					attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(CLOSED_CAPTIONS, attributes->group.closed_captions);
				} else if (attributes->type == MEDIA_TYPE_UNKNOWN) {
					GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:GROUP-ID=%s. Ignoring the line.\n", ret[i]+9));
					return NULL;
				}
			} else if (safe_start_equals("LANGUAGE=\"", ret[i])) {
				size_t len;
				if (attributes->language) gf_free(attributes->language);
				attributes->language = gf_strdup(ret[i]+9);
				len = strlen(attributes->language);
				if (len && (attributes->language[len-1] == '"')) {
					attributes->language[len-1] = '\0';
				} else {
					GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Misformed #EXT-X-MEDIA:LANGUAGE=%s. Quotes are incorrect.\n", ret[i]+5));
				}
			} else if (safe_start_equals("NAME=", ret[i])) {
				GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-MEDIA:NAME not supported\n"));
				//attributes->name = gf_strdup(ret[i]+5);
			} else if (safe_start_equals("DEFAULT=", ret[i])) {
				GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-MEDIA:DEFAULT not supported\n"));
				if (!strncmp(ret[i]+8, "YES", 3)) {
					//TODO
				} else if (!strncmp(ret[i]+8, "NO", 2)) {
					//TODO
				} else {
					GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:DEFAULT=%s\n", ret[i]+8));
				}
			} else if (safe_start_equals("AUTOSELECT=", ret[i])) {
				GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-MEDIA:AUTOSELECT not supported\n"));
				if (!strncmp(ret[i]+11, "YES", 3)) {
					//TODO
				} else if (!strncmp(ret[i]+11, "NO", 2)) {
					//TODO
				} else {
					GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:AUTOSELECT=%s\n", ret[i]+11));
				}
			}

			i++;
		}

		if (attributes->type == MEDIA_TYPE_UNKNOWN) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: TYPE is missing. Ignoring the line.\n"));
			return NULL;
		}
		if (attributes->type == MEDIA_TYPE_CLOSED_CAPTIONS && attributes->mediaURL) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: TYPE is CLOSED-CAPTIONS but URI is present. Ignoring the URI.\n"));
			gf_free(attributes->mediaURL);
			attributes->mediaURL = NULL;
		}
		if ((attributes->type == MEDIA_TYPE_AUDIO && !attributes->group.audio)
		        || (attributes->type == MEDIA_TYPE_VIDEO && !attributes->group.video)) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: missing GROUP-ID attribute. Ignoring the line.\n"));
			return NULL;
		}
		if (!attributes->stream_id) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: no ID was computed. Check previous errors. Ignoring the line.\n"));
			return NULL;
		}

		return ret;
	}
	if (!strncmp(line, "#EXT-X-INDEPENDENT-SEGMENTS", strlen("#EXT-X-INDEPENDENT-SEGMENTS") )) {
		attributes->independent_segments = GF_TRUE;
		M3U8_COMPATIBILITY_VERSION(1);
		return NULL;
	}
	if (!strncmp(line, "#EXT-X-I-FRAME-STREAM-INF", strlen("#EXT-X-I-FRAME-STREAM-INF") )) {
		//todo extract I/intra rate for speed adaptation
		return NULL;
	}
	GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unknown line in M3U8 file %s\n", line));
	return NULL;
}

/**
 * Creates a new MasterPlaylist
\param NULL if MasterPlaylist element could not be allocated
 */
MasterPlaylist* master_playlist_new()
{
	MasterPlaylist *pl = (MasterPlaylist*)gf_malloc(sizeof(MasterPlaylist));
	if (pl == NULL)
		return NULL;
	pl->streams = gf_list_new();
	if (!pl->streams) {
		gf_free(pl);
		return NULL;
	}
	pl->current_stream = -1;
	pl->playlist_needs_refresh = GF_TRUE;
	return pl;
}


/********** master_playlist **********/

GF_Err gf_m3u8_master_playlist_del(MasterPlaylist **playlist) {
	if ((playlist == NULL) || (*playlist == NULL))
		return GF_OK;
	assert((*playlist)->streams);
	while (gf_list_count((*playlist)->streams)) {
		Stream *p = gf_list_get((*playlist)->streams, 0);
		assert(p);
		while (gf_list_count(p->variants)) {
			PlaylistElement *pl = gf_list_get(p->variants, 0);
			assert(pl);
			playlist_element_del(pl);
			gf_list_rem(p->variants, 0);
		}
		gf_list_del(p->variants);
		p->variants = NULL;
		stream_del(p);
		gf_list_rem((*playlist)->streams, 0);
	}
	gf_list_del((*playlist)->streams);
	(*playlist)->streams = NULL;
	gf_free(*playlist);
	*playlist = NULL;

	return GF_OK;
}

static Stream* master_playlist_find_matching_stream(const MasterPlaylist *pl, const u32 stream_id) {
	u32 count, i;
	assert(pl);
	assert(pl->streams);
	assert(stream_id >= 0);
	count = gf_list_count(pl->streams);
	for (i=0; i<count; i++) {
		Stream *cur = gf_list_get(pl->streams, i);
		assert(cur);
		if (stream_id == cur->stream_id) {
			/* We found the program */
			return cur;
		}
	}
	return NULL;
}


/********** sub_playlist **********/

#define M3U8_BUF_SIZE 2048

GF_EXPORT
GF_Err gf_m3u8_parse_master_playlist(const char *file, MasterPlaylist **playlist, const char *baseURL)
{
#ifdef GPAC_ENABLE_COVERAGE
	if (gf_sys_is_cov_mode()) {
		string2num("coverage");
	}
#endif
	return gf_m3u8_parse_sub_playlist(file, playlist, baseURL, NULL, NULL);
}

GF_Err declare_sub_playlist(char *currentLine, const char *baseURL, s_accumulated_attributes *attribs, PlaylistElement *sub_playlist, MasterPlaylist **playlist, Stream *in_stream)
{
	u32 i, iv, count;

	char *fullURL = currentLine;

	if (attribs->is_master_playlist && attribs->is_media_segment) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("[M3U8] Media segment tag MUST NOT appear in a Master Playlist\n"));
		return GF_BAD_PARAM;
	}

	if (gf_url_is_local(fullURL)) {
		fullURL = gf_url_concatenate(baseURL, fullURL);
		assert(fullURL);
	}

	GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] declaring %s %s\n", attribs->is_master_playlist ? "sub-playlist" : "media segment", fullURL));

	memset(attribs->key_iv, 0, sizeof(bin128) );
	iv = gf_htonl(attribs->current_media_seq);
	memcpy(attribs->key_iv + 12, (const void *) &iv, sizeof(iv));

	{
		PlaylistElement *curr_playlist = sub_playlist;
		/* First, we have to find the matching stream */
		Stream *stream = in_stream;
		if (!in_stream)
			stream = master_playlist_find_matching_stream(*playlist, attribs->stream_id);
		/* We did not found the stream, we create it */
		if (stream == NULL) {
			stream = stream_new(attribs->stream_id);
			if (stream == NULL) {
				/* out of memory */
				gf_m3u8_master_playlist_del(playlist);
				return GF_OUT_OF_MEM;
			}
			gf_list_add((*playlist)->streams, stream);
			/* take the first regular variant stream */
			if ((*playlist)->current_stream < 0 && stream->stream_id < MEDIA_TYPE_AUDIO)
				(*playlist)->current_stream = stream->stream_id;
		}

		/* OK, we have a stream, we have to choose the elements within the same stream variant */
		assert(stream);
		assert(stream->variants);
		count = gf_list_count(stream->variants);

		if (!curr_playlist) {
			for (i=0; i<(s32)count; i++) {
				PlaylistElement *i_playlist_element = gf_list_get(stream->variants, i);
				assert(i_playlist_element);
				if (stream->stream_id < MEDIA_TYPE_AUDIO) {
					/* regular stream (EXT-X-STREAM-INF) */
					// Two stream are identical only if they have the same URL
					if (attribs->is_media_segment || !strcmp(i_playlist_element->url, fullURL)) {
						curr_playlist = i_playlist_element;
						break;
					}
				} else {
					/* group streams (EXT-X-MEDIA) */
					//TODO: add renditions and compare depending on context parameters
				}
			}
		}

		/* We are the Master Playlist */
		if (attribs->is_master_playlist) {
			if (curr_playlist != NULL) {
				//playlist has already been defined - this happens when the same video playlist is defined several times with different audio codecs ...
				gf_free(fullURL);
				return GF_OK;
			}
			curr_playlist = playlist_element_new(TYPE_PLAYLIST, fullURL, attribs);
			if (curr_playlist == NULL) {
				/* out of memory */
				gf_m3u8_master_playlist_del(playlist);
				return GF_OUT_OF_MEM;
			}
			assert(fullURL);
			if (curr_playlist->url)
				gf_free(curr_playlist->url);
			curr_playlist->url = gf_strdup(fullURL);
			if (curr_playlist->title)
				gf_free(curr_playlist->title);
			curr_playlist->title = attribs->title ? gf_strdup(attribs->title) : NULL;
			if (curr_playlist->codecs)
				gf_free(curr_playlist->codecs);
			curr_playlist->codecs = attribs->codecs ? gf_strdup(attribs->codecs) : NULL;
			if (curr_playlist->audio_group) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[M3U8] Warning: found an AUDIO group in the master playlist."));
			}
			if (curr_playlist->video_group) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[M3U8] Warning: found an VIDEO group in the master playlist."));
			}
			gf_list_add(stream->variants, curr_playlist);
			curr_playlist->width = attribs->width;
			curr_playlist->height = attribs->height;
		} else {
			/* Normal Playlist */
			assert((*playlist)->streams);
			if (curr_playlist == NULL) {
				/* This is a "normal" playlist without any element in it */
				PlaylistElement *subElement;
				assert(baseURL);
				curr_playlist = playlist_element_new(TYPE_PLAYLIST, baseURL, attribs);
				if (curr_playlist == NULL) {
					/* out of memory */
					gf_m3u8_master_playlist_del(playlist);
					return GF_OUT_OF_MEM;
				}
				assert(curr_playlist->element.playlist.elements);
				assert(fullURL);
				assert(curr_playlist->url && !curr_playlist->codecs);
				curr_playlist->codecs = NULL;
				subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
				if (subElement == NULL) {
					gf_m3u8_master_playlist_del(playlist);
					playlist_element_del(curr_playlist);
					return GF_OUT_OF_MEM;
				}
				gf_list_add(curr_playlist->element.playlist.elements, subElement);
				gf_list_add(stream->variants, curr_playlist);
				curr_playlist->element.playlist.computed_duration += subElement->duration_info;
				assert(stream);
				assert(stream->variants);
				assert(curr_playlist);
			} else {
				PlaylistElement *subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
				if (curr_playlist->element_type != TYPE_PLAYLIST) {
					curr_playlist->element_type = TYPE_PLAYLIST;
					if (!curr_playlist->element.playlist.elements)
						curr_playlist->element.playlist.elements = gf_list_new();
				}
				if (subElement == NULL) {
					gf_m3u8_master_playlist_del(playlist);
					playlist_element_del(curr_playlist);
					return GF_OUT_OF_MEM;
				}
				gf_list_add(curr_playlist->element.playlist.elements, subElement);
				curr_playlist->element.playlist.computed_duration += subElement->duration_info;
			}
		}

		curr_playlist->element.playlist.current_media_seq = attribs->current_media_seq;
		/* We first set the default duration for element, aka targetDuration */
		if (attribs->target_duration_in_seconds > 0) {
			curr_playlist->element.playlist.target_duration = attribs->target_duration_in_seconds;
			curr_playlist->duration_info = attribs->target_duration_in_seconds;
		}
		if (attribs->duration_in_seconds) {
			if (curr_playlist->duration_info == 0) {
				/* we set the playlist duration info as the duration of a segment, only if it's not set
				There are cases of playlist with the last segment with a duration different from the others
				(example: Apple bipbop test)*/
				curr_playlist->duration_info = attribs->duration_in_seconds;
			}
		}
		curr_playlist->element.playlist.media_seq_min = attribs->min_media_sequence;
		curr_playlist->element.playlist.media_seq_max = attribs->current_media_seq;
		if (attribs->bandwidth > 1)
			curr_playlist->bandwidth = attribs->bandwidth;
		if (attribs->is_playlist_ended)
			curr_playlist->element.playlist.is_ended = GF_TRUE;
	}
	/* Cleanup all line-specific fields */
	if (attribs->title) {
		gf_free(attribs->title);
		attribs->title = NULL;
	}
	attribs->duration_in_seconds = 0;
	attribs->playlist_utc_timestamp = 0;
	attribs->bandwidth = 0;
	attribs->stream_id = 0;
	if (attribs->codecs != NULL) {
		gf_free(attribs->codecs);
		attribs->codecs = NULL;
	}
	if (attribs->language != NULL) {
		gf_free(attribs->language);
		attribs->language = NULL;
	}
	if (attribs->group.audio != NULL) {
		gf_free(attribs->group.audio);
		attribs->group.audio = NULL;
	}
	if (attribs->group.video != NULL) {
		gf_free(attribs->group.video);
		attribs->group.video = NULL;
	}
	if (fullURL != currentLine) {
		gf_free(fullURL);
	}
	return GF_OK;
}

GF_Err gf_m3u8_parse_sub_playlist(const char *file, MasterPlaylist **playlist, const char *baseURL, Stream *in_stream, PlaylistElement *sub_playlist)
{
	int i, currentLineNumber;
	FILE *f = NULL;
	u8 *m3u8_payload;
	u32 m3u8_size, m3u8pos;
	char currentLine[M3U8_BUF_SIZE];
	char **attributes = NULL;
	s_accumulated_attributes attribs;

	if (!strncmp(file, "gmem://", 7)) {
		GF_Err e = gf_blob_get_data(file, &m3u8_payload,  &m3u8_size);
		if (e) {
			GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot Open m3u8 source %s for reading\n", file));
			return e;
		}
	} else {
		f = gf_fopen(file, "rt");
		if (!f) {
			GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot open m3u8 file %s for reading\n", file));
			return GF_URL_ERROR;
		}
	}
	if (*playlist == NULL) {
		*playlist = master_playlist_new();
		if (!(*playlist)) {
			if (f) gf_fclose(f);
			return GF_OUT_OF_MEM;
		}
	}
	currentLineNumber = 0;
	reset_attributes(&attribs);
	m3u8pos = 0;
	while (1) {
		char *eof;
		int len;
		if (f) {
			if (!gf_fgets(currentLine, sizeof(currentLine), f))
				break;
		} else {
			u32 __idx = 0;
			if (m3u8pos >= m3u8_size)
				break;
			while (1) {
				assert(__idx < M3U8_BUF_SIZE);

				currentLine[__idx] = m3u8_payload[m3u8pos];
				__idx++;
				m3u8pos++;
				if ((currentLine[__idx-1]=='\n') || (currentLine[__idx-1]=='\r') || (m3u8pos >= m3u8_size)) {
					currentLine[__idx]=0;
					break;
				}
			}
		}
		currentLineNumber++;
		eof = strchr(currentLine, '\r');
		if (eof)
			eof[0] = '\0';
		eof = strchr(currentLine, '\n');
		if (eof)
			eof[0] = '\0';
		len = (u32) strlen(currentLine);
		if (len < 1)
			continue;
		if (currentLineNumber == 1) {
			/* Playlist MUST start with #EXTM3U */
			if (len < 7 || (strncmp("#EXTM3U", currentLine, 7) != 0)) {
				gf_fclose(f);
				gf_m3u8_master_playlist_del(playlist);
				GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("Failed to parse M3U8 File, it should start with #EXTM3U, but was : %s\n", currentLine));
				return GF_STREAM_NOT_FOUND;
			}
			continue;
		}
		if (currentLine[0] == '#') {
			/* A comment or a directive */
			if (strncmp("#EXT", currentLine, 4) == 0) {
				attributes = parse_attributes(currentLine, &attribs);
				if (attributes == NULL) {
					GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8]Comment at line %d : %s\n", currentLineNumber, currentLine));
				} else {
					GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] Directive at line %d: \"%s\", attributes=", currentLineNumber, currentLine));
					i = 0;
					while (attributes[i] != NULL) {
						GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, (" [%d]='%s'", i, attributes[i]));
						gf_free(attributes[i]);
						attributes[i] = NULL;
						i++;
					}
					GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("\n"));
					gf_free(attributes);
					attributes = NULL;
				}
				if (attribs.is_playlist_ended) {
					(*playlist)->playlist_needs_refresh = GF_FALSE;
				}
				if (attribs.independent_segments) {
					(*playlist)->independent_segments = GF_TRUE;
				}
				if (attribs.mediaURL) {
					GF_Err e = declare_sub_playlist(attribs.mediaURL, baseURL, &attribs, sub_playlist, playlist, in_stream);
					gf_free(attribs.mediaURL);
					attribs.mediaURL = NULL;
					if (e != GF_OK) {
						if (f) gf_fclose(f);
						return e;
					}
				}
			}
		} else {
			/*file encountered: sub-playlist or segment*/
			GF_Err e = declare_sub_playlist(currentLine, baseURL, &attribs, sub_playlist, playlist, in_stream);
			attribs.current_media_seq += 1;
			if (e != GF_OK) {
				if (f) gf_fclose(f);
				return e;
			}

			//do not reset all attributes but at least set width/height/codecs to NULL, otherwise we may miss detection
			//of audio-only playlists in av sequences

			attribs.width = attribs.height = 0;
			if (attribs.codecs) {
				gf_free(attribs.codecs);
				attribs.codecs = NULL;
			}
			if (attribs.group.audio) {
				gf_free(attribs.group.audio);
				attribs.group.audio=NULL;
			}
			if (attribs.language) {
				gf_free(attribs.language);
				attribs.language=NULL;
			}
			if (attribs.title) {
				gf_free(attribs.title);
				attribs.title=NULL;
			}
			if (attribs.key_url) {
				gf_free(attribs.key_url);
				attribs.key_url=NULL;
			}
			if (attribs.init_url) {
				gf_free(attribs.init_url);
				attribs.init_url = NULL;
			}
			if (attribs.mediaURL) {
				gf_free(attribs.mediaURL);
				attribs.mediaURL = NULL;
			}
		}
	}
	if (f) gf_fclose(f);

	for (i=0; i<(int)gf_list_count((*playlist)->streams); i++) {
		u32 j;
		Stream *prog = gf_list_get((*playlist)->streams, i);
		prog->computed_duration = 0;
		for (j=0; j<gf_list_count(prog->variants); j++) {
			PlaylistElement *ple = gf_list_get(prog->variants, j);
			if (ple->element_type == TYPE_PLAYLIST) {
				if (ple->element.playlist.computed_duration > prog->computed_duration)
					prog->computed_duration = ple->element.playlist.computed_duration;
			}
		}

	}
	if (attribs.key_url)
		gf_free(attribs.key_url);
	if (attribs.init_url)
		gf_free(attribs.init_url);
	if (attribs.codecs)
		gf_free(attribs.codecs);
	if (attribs.group.audio)
		gf_free(attribs.group.audio);
	if (attribs.language)
		gf_free(attribs.language);
	if (attribs.title)
		gf_free(attribs.title);
	if (attribs.mediaURL)
		gf_free(attribs.mediaURL);

	if (attribs.version < attribs.compatibility_version) {
		GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] Version %d specified but tags from version %d detected\n", attribs.version, attribs.compatibility_version));
	}
	return GF_OK;
}
