/*
 *			GPAC - MPEG-4 Systems C Development Kit
 *
 *			Copyright (c) Jean Le Feuvre 2000-2005 
 *					All rights reserved
 *
 *  This file is part of GPAC / command-line mp4 toolbox
 *
 *  GPAC 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, 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 General Public License for more details.
 *   
 *  You should have received a copy of the GNU General Public License
 *  along with GNU Make; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. 
 *
 */


#include <gpac/m4_author.h>
#include <gpac/m4_bifs.h>
#include <gpac/m4_scenegraph.h>


#ifndef M4_READ_ONLY

extern u32 swf_flags;
extern Float swf_flatten_angle;


void convert_file_info(char *inName, u32 trackID)
{
	char szType[5];
	M4Err e;
	u32 i;
	Bool found;
	M4TrackImporter import;
	memset(&import, 0, sizeof(M4TrackImporter));
	import.in_name = inName;
	import.flags = M4TI_PROBE_ONLY;
	e = MP4T_ImportMedia(&import);
	if (e) {
		fprintf(stdout, "Error probing file %s: %s\n", inName, M4ErrToString(e));
		return;
	}
	if (trackID) {
		fprintf(stdout, "Import probing results for track %s#%d:\n", inName, trackID);
	} else {
		fprintf(stdout, "Import probing results for %s:\n", inName);
		if (!import.nb_tracks) {
			fprintf(stdout, "File has no selectable tracks\n");
			return;
		}
		fprintf(stdout, "File has %d tracks\n", import.nb_tracks);
	}
	found = 0;
	for (i=0; i<import.nb_tracks; i++) {
		if (trackID && (trackID != import.tk_info[i].track_num)) continue;
		if (!trackID) fprintf(stdout, "\tTrack %d type: ", import.tk_info[i].track_num);
		else fprintf(stdout, "Track type: ");

		switch (import.tk_info[i].type) {
		case M4_VisualMediaType: fprintf(stdout, "Video\n"); break;
		case M4_AudioMediaType: fprintf(stdout, "Audio\n"); break;
		case M4_TimedTextMediaType: fprintf(stdout, "Text\n"); break;
		case M4_BIFSMediaType: fprintf(stdout, "BIFS\n"); break;
		default: fprintf(stdout, "Other (4CC: %s)\n", FCC_TO_STR(import.tk_info[i].type, szType)); break;
		}
		if (!trackID) continue;
		fprintf(stdout, "Import Capabilities:\n");
		if (import.tk_info[i].flags & M4TI_USE_DATAREF) fprintf(stdout, "\tCan use data referencing\n");
		if (import.tk_info[i].flags & M4TI_NO_FRAME_DROP) fprintf(stdout, "\tCan use fixed FPS import\n");
		if (import.tk_info[i].flags & M4TI_FORCE_PACKED) fprintf(stdout, "\tCan force packed bitstream import\n");
		if (import.tk_info[i].flags & M4TI_OVERRIDE_FPS) fprintf(stdout, "\tCan override source frame rate\n");
		if (import.tk_info[i].flags & (M4TI_SBR_IMPLICIT|M4TI_SBR_EXPLICIT)) fprintf(stdout, "\tCan use AAC-SBR signaling\n");
		if (import.tk_info[i].flags & M4TI_FORCE_MPEG4) fprintf(stdout, "\tCan force MPEG-4 Systems stream description\n");
		if (import.tk_info[i].flags & M4TI_3GPP_AGGREGATION) fprintf(stdout, "\tCan use 3GPP frame aggregation\n");
		if (import.tk_info[i].flags & M4TI_NO_DURATION) fprintf(stdout, "\tCannot use duration-based import\n");
		found = 1;
		break;
	}
	fprintf(stdout, "\n");
	if (!found && trackID) fprintf(stdout, "Cannot find track %d in file\n", trackID);
}

M4Err import_file(M4File *dest, char *inName, u32 import_flags, Double force_fps, u32 frames_per_sample)
{
	u32 track_id, i, delay, timescale, track;
	Bool do_audio, do_video, do_all;
	char szLan[4];
	M4Err e;
	M4TrackImporter import;
	char *ext, szName[1000];

	memset(&import, 0, sizeof(M4TrackImporter));

	strcpy(szName, inName);
	ext = strrchr(inName, '.');
	if (!ext) {
		fprintf(stdout, "Unknown input file type\n");
		return M4BadParam;
	}

	szLan[0] = szLan[3] = 0;
	delay = 0;
	ext = strchr(szName, ';');
	while (ext) {
		char *ext2 = strchr(ext+1, ';');
		if (ext2) ext2[0] = 0;
		if (!strnicmp(ext+1, "lang=", 5)) strncpy(szLan, ext+6, 3);
		else if (!strnicmp(ext+1, "delay=", 6)) delay = atoi(ext+7);

		if (ext2) ext2[0] = ';';
		ext2 = ext+1;
		ext[0] = 0;
		ext = strchr(ext+1, ';');
	}

	/*check duration import*/
	ext = strrchr(szName, '%');
	if (ext) {
		import.duration = (u32) (atof(ext+1) * 1000);
		ext[0] = 0;
	}
	
	/*select switches for av containers import*/
	do_audio = do_video = 0;
	track_id = 0;
	do_all = 1;
	ext = strrchr(szName, '#');
	if (ext) {
		if (!strnicmp(ext, "#audio", 6)) do_audio = 1;
		else if (!strnicmp(ext, "#video", 6)) do_video = 1;
		else if (!strnicmp(ext, "#trackID=", 9)) track_id = atoi(&ext[9]);
		else track_id = atoi(&ext[1]);
		ext[0] = 0;
	}
	if (do_audio || do_video || track_id) do_all = 0;

	import.in_name = szName;
	import.flags = M4TI_PROBE_ONLY;
	e = MP4T_ImportMedia(&import);
	if (e) return e;

	import.dest = dest;
	import.video_fps = force_fps;
	import.frames_per_sample = frames_per_sample;
	import.flags = import_flags;


	if (!import.nb_tracks) {
		u32 count, o_count;
		o_count = M4_GetTrackCount(import.dest);
		e = MP4T_ImportMedia(&import);
		if (e) return e;
		count = M4_GetTrackCount(import.dest);
		timescale = M4_GetTimeScale(dest);
		for (i=o_count; i<count; i++) {
			if (szLan[0]) M4_SetMediaLanguage(import.dest, i+1, szLan);
			if (delay) {
				u32 tk_dur;
				M4_RemoveEditSegments(import.dest, i+1);
				tk_dur = (u32) M4_GetTrackDuration(import.dest, i+1);
				M4_AppendEditSegment(import.dest, i+1, (timescale*delay)/1000, 0, M4_EDIT_EMPTY);
				M4_AppendEditSegment(import.dest, i+1, tk_dur, 0, M4_EDIT_NORMAL);
			}
		}
	} else {

		for (i=0; i<import.nb_tracks; i++) {
			import.trackID = import.tk_info[i].track_num;
			if (do_all) e = MP4T_ImportMedia(&import);
			else if (track_id && (track_id==import.trackID)) {
				track_id = 0;
				e = MP4T_ImportMedia(&import);
			}
			else if (do_audio && (import.tk_info[i].type==M4_AudioMediaType)) {
				do_audio = 0;
				e = MP4T_ImportMedia(&import);
			}
			else if (do_video && (import.tk_info[i].type==M4_VisualMediaType)) {
				do_video = 0;
				e = MP4T_ImportMedia(&import);
			}
			else continue;
			if (e) return e;

			timescale = M4_GetTimeScale(dest);
			track = M4_GetTrackByID(import.dest, import.final_trackID);
			if (szLan[0]) M4_SetMediaLanguage(import.dest, track, szLan);
			if (delay) {
				u32 tk_dur;
				M4_RemoveEditSegments(import.dest, track);
				tk_dur = (u32) M4_GetTrackDuration(import.dest, track);
				M4_AppendEditSegment(import.dest, track, (timescale*delay)/1000, 0, M4_EDIT_EMPTY);
				M4_AppendEditSegment(import.dest, track, tk_dur, 0, M4_EDIT_NORMAL);
			}
		}
		if (track_id) fprintf(stdout, "WARNING: Track ID %d not found in file\n", track_id);
		else if (do_video) fprintf(stdout, "WARNING: Video track not found\n");
		else if (do_audio) fprintf(stdout, "WARNING: Audio track not found\n");
	}
	return e;
}

typedef struct
{
	u32 tk;
	Bool has_non_raps;
	u32 last_sample;
	u32 sample_count;
	u32 time_scale;
	u32 firstDTS, lastDTS;
	u32 dst_tk;
	/*set if media can be duplicated at split boundaries - only used for text tracks and provate tracks, this assumes all
	samples are RAP*/
	Bool can_duplicate;
	/*controls import by time rather than by sample (otherwise we would have to remove much more samples video vs audio for example*/
	Bool first_sample_done;
	u32 stop_state;
} TKInfo;

M4Err split_isomedia_file(M4File *mp4, Double split_dur, u32 split_size_kb, char *inName, Double InterleavingTime, Double chunk_start_time)
{
	u32 i, count, nb_tk, needs_rap_sync, cur_file, conv_type, nb_tk_done, nb_samp, nb_done, di;
	Double max_dur, cur_file_time;
	Bool do_add, all_duplicatable, size_exceeded;
	M4File *dest;
	M4Sample *samp;
	M4Err e;
	TKInfo *tks, *tki;
	char *ext, szName[1000], szFile[1000];
	Double chunk_start = (Double) chunk_start_time;

	strcpy(szName, inName);
	ext = strrchr(szName, '.');
	if (ext) ext[0] = 0;
	ext = strrchr(inName, '.');
	
	dest = NULL;

	conv_type = 0;
	switch (MP4T_GuessSpecification(mp4)) {
	case FOUR_CHAR_INT('I','S','M','A'): conv_type = 1; break;
	case GPP_V4_File: 
	case GPP_V5_File:
	case GPP_V6_File:
	case GPP_GEN_File:
	case GPP2_V1_File:
		conv_type = 2; 
		break;
	}
	if (!stricmp(ext, ".3gp") || !stricmp(ext, ".3g2")) conv_type = 2;

	count = M4_GetTrackCount(mp4);
	SAFEALLOC(tks, sizeof(TKInfo)*count);

	max_dur = 0;
	nb_tk = 0;
	all_duplicatable = 1;
	needs_rap_sync = 0;
	nb_samp = 0;
	for (i=0; i<count; i++) {
		u32 mtype;
		Double dur;
		char szType[5];
		tks[nb_tk].tk = i+1;
		tks[nb_tk].can_duplicate = 0;

		mtype = M4_GetMediaType(mp4, i+1);
		switch (mtype) {
		/*we duplicate text samples at boundaries*/
		case M4_TimedTextMediaType:
			tks[nb_tk].can_duplicate = 1;
		case M4_AudioMediaType:
			break;
		case M4_VisualMediaType:
			if (M4_GetSampleCount(mp4, i+1)>1) {
				break;
			}
			continue;
		case M4_HintMediaType:
		case M4_BIFSMediaType:
		case M4_OCRMediaType:
		case M4_ODMediaType:
		case M4_OCIMediaType:
		case M4_IPMPMediaType:
		case M4_MPEGJMediaType:
		case M4_MPEG7MediaType:
		case M4_FlashMediaType:
		case M4_MPEG21MediaType:
			fprintf(stdout, "WARNING: Track ID %d (type %s) not handled by spliter - skipping\n", M4_GetTrackID(mp4, i+1), FCC_TO_STR(mtype, szType));
			continue;
		default:
			/*for all other track types, only split if more than one sample*/
			if (M4_GetSampleCount(mp4, i+1)==1) {
				fprintf(stdout, "WARNING: Track ID %d (type %s) not handled by spliter - skipping\n", M4_GetTrackID(mp4, i+1), FCC_TO_STR(mtype, szType));
				continue;
			}
			tks[nb_tk].can_duplicate = 1;
		}

		tks[nb_tk].sample_count = M4_GetSampleCount(mp4, i+1);
		nb_samp += tks[nb_tk].sample_count;
		tks[nb_tk].last_sample = 0;
		tks[nb_tk].firstDTS = 0;
		tks[nb_tk].time_scale = M4_GetMediaTimeScale(mp4, i+1);
		tks[nb_tk].has_non_raps = M4_HasSyncPoints(mp4, i+1);
		/*seen that on some 3gp files from nokia ...*/
		if (mtype==M4_AudioMediaType) tks[nb_tk].has_non_raps = 0;
		
		dur = (Double) (s64) M4_GetMediaDuration(mp4, i+1);
		dur /= tks[nb_tk].time_scale;
		if (max_dur<dur) max_dur=dur;

		if (tks[nb_tk].has_non_raps) {
			/*we don't support that*/
			if (needs_rap_sync) {
				fprintf(stdout, "More than one track has non-sync points - cannot split file\n");
				free(tks);
				return M4NotSupported;
			}
			needs_rap_sync = nb_tk+1;
		}
		if (!tks[nb_tk].can_duplicate) all_duplicatable = 0;
		nb_tk++;
	}
	if (!nb_tk) {
		fprintf(stdout, "No suitable tracks found for spliting file\n");
		free(tks);
		return M4NotSupported;
	}
	if (chunk_start>=max_dur) {
		fprintf(stdout, "Input file shorter than split start offset\n");
		free(tks);
		return M4NotSupported;
	}
	if (max_dur<=split_dur) {
		fprintf(stdout, "Input file shorter than split duration\n");
		free(tks);
		return M4NotSupported;
	}
	if (needs_rap_sync) {
		tki = &tks[needs_rap_sync-1];
		if ((M4_GetSyncPointCount(mp4, tki->tk)==1) && (chunk_start != 0.0f)) {
			fprintf(stdout, "Not enough Random Access points in input file - cannot split\n");
			free(tks);
			return M4NotSupported;
		}
	}
	split_size_kb *= 1024;
	cur_file_time = 0;

	if (chunk_start>0) {
		if (needs_rap_sync) {
			u32 sample_num;
			Double start = chunk_start;
			tki = &tks[needs_rap_sync-1];

			start = M4_GetSampleDTS(mp4, tki->tk, tki->sample_count);
			start /= tki->time_scale;
			if (start<chunk_start) {
				tki->stop_state = 2;
				needs_rap_sync = 0;
			} else  {
				e = M4_GetSampleForTime(mp4, tki->tk, (u32) (chunk_start*tki->time_scale), &di, M4_SearchSyncBackward, &samp, &sample_num);
				if (e!=M4OK) {
					fprintf(stdout, "Cannot locate RAP in track ID %d for chunk extraction from %02.2f sec\n", M4_GetTrackID(mp4, tki->tk), chunk_start);
					free(tks);
					return M4NotSupported;
				}
				start = samp->DTS;
				start /= tki->time_scale;
				M4_DeleteSample(&samp);
				fprintf(stdout, "Adjusting chunk start time to previous random access at %02.2f sec\n", start);
				split_dur += (chunk_start - start);
				chunk_start = start;
			}
		}
		/*sync all tracks*/
		for (i=0; i<nb_tk; i++) {
			tki = &tks[i];
			while (tki->last_sample<tki->sample_count) {
				Double time;
				u32 dts;
				dts = M4_GetSampleDTS(mp4, tki->tk, tki->last_sample+1);
				time = dts;
				time /= tki->time_scale;
				if (time>=chunk_start) {
					/*rewind one sample (text tracks & co)*/
					if (tki->can_duplicate && tki->last_sample) {
						tki->last_sample--;
						tki->firstDTS = (u32) (chunk_start*tki->time_scale);
					} else {
						tki->firstDTS = dts;
					}
					break;
				}
				tki->last_sample++;
			}
		}
		cur_file_time = chunk_start;
	}

	dest = NULL;
	nb_done = 0;
	nb_tk_done = 0;
	cur_file = 0;
	while (nb_tk_done<nb_tk) {
		Double last_rap_sample_time, max_dts, file_split_dur;
		Bool is_last;

		if (chunk_start>=0) {
			sprintf(szFile, "%s_%d_%d%s", szName, (u32) chunk_start, (u32) (chunk_start+split_dur), ext);
		} else {
			sprintf(szFile, "%s_%03d%s", szName, cur_file+1, ext);
		}
		dest = M4_MovieOpen(szFile, M4_WRITE_EDIT);
		/*clone all tracks*/
		for (i=0; i<nb_tk; i++) {
			tki = &tks[i];
			/*track done - we remove the track from destination, an empty video track could cause pbs to some players*/
			if (tki->stop_state==2) continue;

			e = M4_CloneTrack(mp4, tki->tk, dest, &tki->dst_tk);
			if (e) {
				fprintf(stdout, "Error cloning track %d\n", tki->tk);
				goto err_exit;
			}
			/*use non-packet CTS offsets (faster add/remove)*/
			if (M4_HasTimeOffsets(mp4, tki->tk)) {
				M4_SetCTSPackMode(dest, tki->dst_tk, 1);
			}
		}
		do_add = 1;
		is_last = 0;
		last_rap_sample_time = 0;
		file_split_dur = split_dur;

		size_exceeded = 0;
		max_dts = 0;
		while (do_add) {
			Double time;
			u32 nb_over;
			/*perfom basic de-interleaving to make sure we're not importing too much of a given track*/
			u32 nb_add = 0;
			/*add one sample of each track*/
			for (i=0; i<nb_tk; i++) {
				Double t;
				u32 dts;
				tki = &tks[i];
				
				if (tki->stop_state) 
					continue;
				if (tki->last_sample==tki->sample_count) 
					continue;
			
				/*get sample info, see if we need to check it (basic de-interleaver)*/
				dts = M4_GetSampleDTS(mp4, tki->tk, tki->last_sample+1);

				/*reinsertion (timed text)*/
				if (dts < tki->firstDTS) {
					samp = M4_GetSample(mp4, tki->tk, tki->last_sample+1, &di);
					samp->DTS = 0;
					e = M4_AddSample(dest, tki->dst_tk, di, samp);
					M4_DeleteSample(&samp);
					tki->last_sample += 1;
					dts = M4_GetSampleDTS(mp4, tki->tk, tki->last_sample+1);
				}
				dts -= tki->firstDTS;


				t = dts;
				t /= tki->time_scale;
				if (tki->first_sample_done) {
					if (t>max_dts) continue;
				} else {
					/*here's the trick: only take care of a/v media for deinterleaving, and ad other media
					only if thir dts is less than the max AV dts found. Otherwise with some text streams we will end up importing
					too much video and corrupting the last sync point indication*/
					if (!tki->can_duplicate && (t>max_dts)) max_dts = t;
					tki->first_sample_done = 1;
				}
				samp = M4_GetSample(mp4, tki->tk, tki->last_sample+1, &di);
				samp->DTS -= tki->firstDTS;

				nb_add += 1;

				if (tki->has_non_raps && samp->IsRAP) {
					M4Sample *next_rap;
					u32 next_rap_num, sdi;
					last_rap_sample_time = samp->DTS;
					last_rap_sample_time /= tki->time_scale;
					e = M4_GetSampleForTime(mp4, tki->tk, samp->DTS+tki->firstDTS+2, &sdi, M4_SearchSyncForward, &next_rap, &next_rap_num);
					if (e==M4EOF) is_last = 1;
					if (next_rap) {
						if (!next_rap->IsRAP) 
							is_last = 1;
						M4_DeleteSample(&next_rap);
					}
				}
				tki->lastDTS = samp->DTS;
				e = M4_AddSample(dest, tki->dst_tk, di, samp);
				M4_DeleteSample(&samp);
				tki->last_sample += 1;
				MP4T_OnProgress("Splitting", nb_done, nb_samp);
				nb_done++;
				if (e) {
					fprintf(stdout, "Error cloning track %d sample %d\n", tki->tk, tki->last_sample);
					goto err_exit;
				}
			}

			/*test by size/duration*/
			nb_over = 0;

			/*test by file size: same as duration test, only dynamically increment import duration*/
			if (split_size_kb) {
				u64 est_size = M4_EstimateFileSize(dest);
				/*while below desired size keep importing*/
				if (est_size<split_size_kb) 
					file_split_dur = (Double) M4_MAX_FLOAT;
				else {
					size_exceeded = 1;
				}
			}

			for (i=0; i<nb_tk; i++) {
				tki = &tks[i];
				if (tki->stop_state) {
					nb_over++;
					continue;
				}
				time = tki->lastDTS;
				time /= tki->time_scale;
				if (size_exceeded || (tki->last_sample==tki->sample_count) || (!tki->can_duplicate && (time>file_split_dur)) ) {
					nb_over++;
					tki->stop_state = 1;
					if (tki->last_sample<tki->sample_count) is_last = 0;
					if ((!tki->can_duplicate || all_duplicatable) && (tki->last_sample==tki->sample_count)) is_last = 1;
				}
				/*special tracks (not audio, not video)*/
				else if (tki->can_duplicate) {
					u32 dts = M4_GetSampleDTS(mp4, tki->tk, tki->last_sample+1);
					time = dts - tki->firstDTS;
					time /= tki->time_scale;
					if (time>file_split_dur) {
						nb_over++;
						tki->stop_state = 1;
					}
				}
				if (!nb_add && (!max_dts || (tki->lastDTS <= 1 + tki->time_scale*max_dts))) 
					tki->first_sample_done = 0;
			}
			if (nb_over==nb_tk) do_add = 0;
		}

		/*remove samples - first figure out smallest duration*/
		file_split_dur = (Double) M4_MAX_FLOAT;
		for (i=0; i<nb_tk; i++) {
			Double time;
			tki = &tks[i];
			/*track done*/
			if ((tki->stop_state==2) || (!is_last && (tki->sample_count == tki->last_sample)) ) {
				if (tki->has_non_raps) last_rap_sample_time = 0;
				continue;
			}

			if (tki->lastDTS) {
				time = tki->lastDTS;
				time /= tki->time_scale;
				if ((!tki->can_duplicate || all_duplicatable) && time<file_split_dur) file_split_dur = time;
			}
		}

		/*don't split if eq to copy...*/
		if (is_last && !cur_file) {
			fprintf(stdout, "Cannot split file (Not enough sync samples, duration too large or size too big)\n");
			goto err_exit;
		}


		/*if not last chunk and longer duration adjust to previous RAP point*/
		if ( (size_exceeded || !split_size_kb) && (file_split_dur>split_dur) ) {
			/*if larger than last RAP, rewind till it*/
			if (last_rap_sample_time && (last_rap_sample_time<file_split_dur) ) {
				file_split_dur = last_rap_sample_time;
				is_last = 0;
			}
		}

		nb_tk_done = 0;
		if (!is_last) {
			for (i=0; i<nb_tk; i++) {
				Double time;
				u32 last_samp;
				tki = &tks[i];
				while (1) {
					u32 dts;
					last_samp = M4_GetSampleCount(dest, tki->dst_tk);
					if (!last_samp) break;

					dts = M4_GetSampleDTS(dest, tki->dst_tk, last_samp);
					time = dts;
					time /= tki->time_scale;
					/*done*/
					if (tki->last_sample==tki->sample_count) {
						tki->stop_state=2;
						break;
					}
					if (time<file_split_dur) break;

					M4_RemoveSample(dest, tki->dst_tk, last_samp);
					tki->last_sample--;
					assert(tki->last_sample);
					nb_done--;
					MP4T_OnProgress("Splitting", nb_done, nb_samp);
				}
				if (tki->last_sample<tki->sample_count) {
					u32 dts;
					tki->stop_state = 0;
					dts = M4_GetSampleDTS(mp4, tki->tk, tki->last_sample+1);
					time = dts - tki->firstDTS;
					time /= tki->time_scale;
					/*re-insert prev sample*/
					if (tki->can_duplicate && (time>file_split_dur)) {
						tki->last_sample--;
						dts = M4_GetSampleDTS(mp4, tki->tk, tki->last_sample);
						tki->firstDTS += (u32) (file_split_dur*tki->time_scale);
						M4_SetLastSampleDuration(dest, tki->dst_tk, tki->firstDTS - dts);
					} else {
						tki->firstDTS = dts;
					}
					tki->first_sample_done = 0;
				} else {
					nb_tk_done++;
				}

			}
		}

		if (chunk_start>=0) {
			fprintf(stdout, "Extracting chunk %s - duration %02.2f seconds\n", szFile, file_split_dur);
		} else {
			fprintf(stdout, "Storing split-file %s - duration %02.2f seconds\n", szFile, file_split_dur);
		}

		/*repack CTSs*/
		for (i=0; i<nb_tk; i++) {
			tki = &tks[i];
			if (tki->stop_state == 2) continue;
			if (!M4_GetSampleCount(dest, tki->dst_tk)) {
				M4_RemoveTrack(dest, tki->dst_tk);
				continue;
			}
			if (M4_HasTimeOffsets(mp4, tki->tk)) {
				M4_SetCTSPackMode(dest, tki->dst_tk, 0);
			}
		}
		/*check chapters*/
		do_add = 1;
		for (i=0; i<M4_GetChapterCount(mp4, 0); i++) {
			char *name;
			u64 chap_time;
			M4_GetChapter(mp4, 0, i+1, &chap_time, (const char **) &name);
			max_dts = (Double) (s64) chap_time;
			max_dts /= 1000;
			if (max_dts<cur_file_time) continue;
			if (max_dts>cur_file_time+file_split_dur) break;
			max_dts-=cur_file_time;
			chap_time = (u64) (max_dts*1000);
			M4_AddChapter(dest, 0, chap_time, name);
			/*add prev*/
			if (do_add && i) {
				M4_GetChapter(mp4, 0, i, &chap_time, (const char **) &name);
				M4_AddChapter(dest, 0, 0, name);
				do_add = 0;
			}
		}
		cur_file_time += file_split_dur;

		if (conv_type==1) MP4T_MakeISMA(dest, 1, 0, 0, NULL, NULL);
		else if (conv_type==2) MP4T_Make3GPP(dest, NULL, NULL);
		if (InterleavingTime) {
			MP4T_MakeInterleaved(dest, InterleavingTime);
		} else {
			M4_SetStorageMode(dest, M4_STREAMABLE);
		}

		M4_ClonePLIndications(mp4, dest);
		e = M4_MovieCloseEx(dest, MP4T_OnProgress, "Writing");
		dest = NULL;
		if (e) fprintf(stdout, "Error storing file %s\n", M4ErrToString(e));
		if (is_last || (chunk_start >=0) ) break;
		cur_file++;
	}
	MP4T_OnProgress("Splitting", nb_samp, nb_samp);
err_exit:
	if (dest) M4_MovieDelete(dest);
	free(tks);
	return e;
}

M4Err cat_multiple_files(M4File *dest, char *fileName, u32 import_flags, Double force_fps, u32 frames_per_sample, char *tmp_dir);

M4Err cat_isomedia_file(M4File *dest, char *fileName, u32 import_flags, Double force_fps, u32 frames_per_sample, char *tmp_dir)
{
	u32 i, j, count, nb_tracks, nb_samp, nb_done;
	M4File *orig;
	M4Err e;
	char szType[5];
	Float ts_scale;
	Double dest_orig_dur;
	u32 dst_tk, tk_id, mtype, insert_dts;
	M4Sample *samp;

	if (strchr(fileName, '*')) return cat_multiple_files(dest, fileName, import_flags, force_fps, frames_per_sample, tmp_dir);

	e = M4OK;
	if (!M4_ProbeIsoMediaFile(fileName)) {
		orig = M4_MovieOpen("temp", M4_WRITE_EDIT);
		if (tmp_dir) M4_SetTempDir(orig, tmp_dir);
		e = import_file(orig, fileName, import_flags, force_fps, frames_per_sample);
		if (e) return e;
	} else {
		orig = M4_MovieOpen(fileName, M4_OPEN_READ);
	}

	nb_samp = 0;
	nb_tracks = M4_GetTrackCount(orig);
	for (i=0; i<nb_tracks; i++) {
		char szType[5];
		u32 mtype = M4_GetMediaType(orig, i+1);
		switch (mtype) {
		case M4_AudioMediaType:
		case M4_TimedTextMediaType:
		case M4_VisualMediaType:
			nb_samp+= M4_GetSampleCount(orig, i+1);
			break;
		case M4_HintMediaType:
		case M4_BIFSMediaType:
		case M4_OCRMediaType:
		case M4_ODMediaType:
		case M4_OCIMediaType:
		case M4_IPMPMediaType:
		case M4_MPEGJMediaType:
		case M4_MPEG7MediaType:
		case M4_FlashMediaType:
		case M4_MPEG21MediaType:
			fprintf(stdout, "WARNING: Track ID %d (type %s) not handled by concatenation - removing from destination\n", M4_GetTrackID(orig, i+1), FCC_TO_STR(mtype, szType));
			continue;
		default:
			/*for all other track types, only split if more than one sample*/
			if (M4_GetSampleCount(dest, i+1)==1) {
				fprintf(stdout, "WARNING: Track ID %d (type %s) not handled by concatenation - removing from destination\n", M4_GetTrackID(orig, i+1), FCC_TO_STR(mtype, szType));
				continue;
			}
			break;
		}
	}
	if (!nb_samp) {
		fprintf(stdout, "No suitable media tracks to cat in %s - skipping\n", fileName);
		goto err_exit;
	}
	
	dest_orig_dur = (Double) (s64) M4_GetDuration(dest);
	dest_orig_dur /= M4_GetTimeScale(dest);

	fprintf(stdout, "Appending file %s\n", fileName);
	nb_done = 0;
	for (i=0; i<nb_tracks; i++) {
		u32 last_DTS;
		Bool use_ts_dur = 1;
		mtype = M4_GetMediaType(orig, i+1);
		switch (mtype) {
		case M4_TimedTextMediaType:
			use_ts_dur = 0;
		case M4_AudioMediaType:
		case M4_VisualMediaType:
			break;
		case M4_HintMediaType:
		case M4_BIFSMediaType:
		case M4_OCRMediaType:
		case M4_ODMediaType:
		case M4_OCIMediaType:
		case M4_IPMPMediaType:
		case M4_MPEGJMediaType:
		case M4_MPEG7MediaType:
		case M4_FlashMediaType:
		case M4_MPEG21MediaType:
			continue;
		default:
			/*for all other track types, only split if more than one sample*/
			if (M4_GetSampleCount(dest, i+1)==1) continue;
			use_ts_dur = 0;
			break;
		}
		dst_tk = 0;
		tk_id = M4_GetTrackID(orig, i+1);
		dst_tk = M4_GetTrackByID(dest, tk_id);
		if (dst_tk) {
			if (!M4_IsSameStreamDescription(orig, i+1, dest, dst_tk)) dst_tk = 0;
			else if (mtype==M4_VisualMediaType) {
				u32 w, h, ow, oh;
				M4_GetVisualEntrySize(orig, i+1, 1, &ow, &oh);
				M4_GetVisualEntrySize(dest, dst_tk, 1, &w, &h);
				if ((ow!=w) || (oh!=h)) dst_tk = 0;
			}
		}

		if (!dst_tk) {
			for (j=0; j<M4_GetTrackCount(dest); j++) {
				if (mtype != M4_GetMediaType(dest, j+1)) continue;
				if (M4_IsSameStreamDescription(orig, i+1, dest, j+1)) {
					if (mtype==M4_VisualMediaType) {
						u32 w, h, ow, oh;
						M4_GetVisualEntrySize(orig, i+1, 1, &ow, &oh);
						M4_GetVisualEntrySize(dest, j+1, 1, &w, &h);
						if ((ow==w) && (oh==h)) {
							dst_tk = j+1;
							break;
						}
					} else {
						dst_tk = j+1;
						break;
					}
				}
			}
		}
		/*looks like a new file*/
		if (!dst_tk) {
			fprintf(stdout, "No suitable destination track found - creating new one (type %s)\n", FCC_TO_STR(mtype, szType));
			e = M4_CloneTrack(orig, i+1, dest, &dst_tk);
			if (e) goto err_exit;
			M4_ClonePLIndications(orig, dest);
		}
		count = M4_GetSampleCount(dest, dst_tk);
		if (use_ts_dur && (count>1)) {
			insert_dts = 2*M4_GetSampleDTS(dest, dst_tk, count) - M4_GetSampleDTS(dest, dst_tk, count-1);
		} else {
			insert_dts = (u32) M4_GetMediaDuration(dest, dst_tk);
			if (!count) insert_dts = 0;
		}

		ts_scale = (Float) M4_GetMediaTimeScale(dest, dst_tk);
		ts_scale /= M4_GetMediaTimeScale(orig, i+1);

		last_DTS = 0;
		count = M4_GetSampleCount(orig, i+1);
		for (j=0; j<count; j++) {
			u32 di;
			samp = M4_GetSample(orig, i+1, j+1, &di);
			last_DTS = samp->DTS;
			samp->DTS =  (u32) (samp->DTS * ts_scale) + insert_dts;
			samp->CTS_Offset =  (u32) (samp->CTS_Offset * ts_scale);
			e = M4_AddSample(dest, dst_tk, di, samp);
			M4_DeleteSample(&samp);
			if (e) goto err_exit;
			MP4T_OnProgress("Appending", nb_done, nb_samp);
			nb_done++;
		}
		if (!use_ts_dur) {
			insert_dts = (u32) M4_GetMediaDuration(orig, i+1);
			insert_dts -= last_DTS;
			M4_SetLastSampleDuration(dest, dst_tk, insert_dts);
		}
	}
	MP4T_OnProgress("Appending", nb_samp, nb_samp);

	/*check chapters*/
	for (i=0; i<M4_GetChapterCount(orig, 0); i++) {
		char *name;
		Double c_time;
		u64 chap_time;
		M4_GetChapter(orig, 0, i+1, &chap_time, (const char **) &name);
		c_time = (Double) (s64) chap_time;
		c_time /= 1000;
		c_time += dest_orig_dur;

		/*check last file chapter*/
		if (!i && M4_GetChapterCount(dest, 0)) {
			const char *last_name;
			u64 last_chap_time;
			M4_GetChapter(dest, 0, M4_GetChapterCount(dest, 0), &last_chap_time, &last_name);
			/*last and first chapters are the same, don't duplicate*/
			if (last_name && name && !stricmp(last_name, name)) continue;
		}

		chap_time = (u64) (c_time*1000);
		M4_AddChapter(dest, 0, chap_time, name);
	}


err_exit:
	M4_MovieDelete(orig);
	return e;
}

typedef struct
{
	char szPath[M4_MAX_PATH];
	char szRad1[1024], szRad2[1024], szOpt[200];
	M4File *dest;
	u32 import_flags;
	Double force_fps;
	u32 frames_per_sample;
	char *tmp_dir;
} CATEnum;

Bool cat_enumerate(void *cbk, char *szName, char *szPath)
{
	M4Err e;
	u32 len_rad1;
	char szFileName[M4_MAX_PATH];
	CATEnum *cat_enum = (CATEnum *)cbk;
	len_rad1 = strlen(cat_enum->szRad1);
	if (strnicmp(szName, cat_enum->szRad1, len_rad1)) return 0;
	if (strlen(cat_enum->szRad2) && !strstr(szName + len_rad1, cat_enum->szRad2) ) return 0;

	strcpy(szFileName, szName);
	strcat(szFileName, cat_enum->szOpt);

	e = cat_isomedia_file(cat_enum->dest, szFileName, cat_enum->import_flags, cat_enum->force_fps, cat_enum->frames_per_sample, cat_enum->tmp_dir);
	if (e) return 1;
	return 0;
}

M4Err cat_multiple_files(M4File *dest, char *fileName, u32 import_flags, Double force_fps, u32 frames_per_sample, char *tmp_dir)
{
	CATEnum cat_enum;
	char *sep;

	cat_enum.dest = dest;
	cat_enum.import_flags = import_flags;
	cat_enum.force_fps = force_fps;
	cat_enum.frames_per_sample = frames_per_sample;
	cat_enum.tmp_dir = tmp_dir;

	strcpy(cat_enum.szPath, fileName);
	sep = strrchr(cat_enum.szPath, M4_PATH_SEPARATOR);
	if (!sep) sep = strrchr(cat_enum.szPath, '/');
	if (!sep) {
		strcpy(cat_enum.szPath, ".");
		strcpy(cat_enum.szRad1, fileName);
	} else {
		strcpy(cat_enum.szRad1, sep+1);
		sep[0] = 0;
	}
	sep = strchr(cat_enum.szRad1, '*');
	strcpy(cat_enum.szRad2, sep+1);
	sep[0] = 0;
	sep = strchr(cat_enum.szRad2, '%');
	if (!sep) sep = strchr(cat_enum.szRad2, '#');
	if (!sep) sep = strchr(cat_enum.szRad2, ';');
	strcpy(cat_enum.szOpt, "");
	if (sep) {
		strcpy(cat_enum.szOpt, sep);
		sep[0] = 0;
	}
	return DIR_Enum(cat_enum.szPath, 0, cat_enumerate, &cat_enum);
}


/*
		MPEG-4 encoding
*/

M4Err EncodeFile(char *in, M4File *mp4, char *logFile, char *mediaSource, u32 flags, u32 rap_freq) 
{
	M4Err e;
	M4ContextLoader load;
	M4SceneManager *ctx;
	LPSCENEGRAPH sg;
	
	sg = NewSceneGraph();
	ctx = NewSceneManager(sg);
	memset(&load, 0, sizeof(M4ContextLoader));
	load.fileName = in;
	load.ctx = ctx;
	load.cbk = "Parsing";
	load.OnProgress = MP4T_OnProgress;
	load.swf_import_flags = swf_flags;
	load.swf_flatten_limit = swf_flatten_angle;
	/*since we're encoding we must get MPEG4 nodes only*/
	load.flags = M4CL_MPEG4_STRICT;
	e = M4SM_LoaderInit(&load);
	if (e) {
		fprintf(stdout, "Cannot load context - %s\n", M4ErrToString(e));
		goto err_exit;
	}
	e = M4SM_LoaderRun(&load);
	M4SM_LoaderDone(&load);

	if (e) {
		fprintf(stdout, "Error loading file %s\n", M4ErrToString(e));
		goto err_exit;
	} else {
		e = M4SM_EncodeFile(ctx, mp4, logFile, mediaSource, flags, rap_freq);
	}

	M4_SetMovieVersionInfo(mp4, MP4_V2_File, 1);
	M4_ModifyAlternateBrand(mp4, ISO_Media_File, 1);

err_exit:
	M4SM_Delete(ctx);
	SG_Delete(sg);
	return e;
}

/*
		MPEG-4 chunk encoding
*/

static u32 GetNbBits(u32 MaxVal)
{
	u32 k=0;
	while ((s32) MaxVal > ((1<<k)-1) ) k+=1;
	return k;
}

M4Err EncodeBIFSChunk(M4SceneManager *ctx, char *bifsOutputFile, M4Err (*AUCallback)(M4Sample *))
{
	M4Err			e;
	char *data;
	u32 data_len;
	LPBIFSENC		bifsenc;
	InitialObjectDescriptor *iod;
	u32				i, j, count;
	M4StreamContext *sc;
	ESDescriptor	*esd;
	Bool is_in_iod, delete_desc, encode_names;
	BIFSConfigDescriptor bcfg;
	M4AUContext		*au;
	M4Sample		*samp;
	M4File *dest;
	char szF[M4_MAX_PATH], *ext;

	strcpy(szF, bifsOutputFile);
	ext = strrchr(szF, '.');
	if (ext) ext[0] = 0;
	strcat(szF, ".bifs");
	//strcat(szF, ".mp4");


	/* step3: encoding all AUs in ctx->streams starting at AU index 1 (0 is SceneReplace from previous context) */
	/*
	from here, the coding is mainly the same as in M4SM_EncodeBIFS (see SceneManager.c)
	*/
	bifsenc = BIFS_NewEncoder(ctx->scene_graph);
	e = M4OK;

	iod = (InitialObjectDescriptor *) ctx->root_od;
	/*if no iod check we only have one bifs*/
	if (!iod) {
		count = 0;
		for (i=0; i<ChainGetCount(ctx->streams); i++) {
			sc = ChainGetEntry(ctx->streams, i);
			if (sc->streamType == M4ST_OD) count++;
		}
		if (!iod && count>1) return M4NotSupported;
	}

//	dest = M4_MovieOpen(szF, M4_OPEN_WRITE);
//	if (!dest) return M4_GetLastError(NULL);

	count = ChainGetCount(ctx->streams);

	for (i=0; i<ChainGetCount(ctx->streams); i++) {
		u32 track, nbb;
		M4StreamContext *sc = ChainGetEntry(ctx->streams, i);
		esd = NULL;
		if (sc->streamType != M4ST_SCENE) continue;

		delete_desc = 0;
		esd = NULL;
		is_in_iod = 1;
		if (iod) {
			is_in_iod = 0;
			for (j=0; j<ChainGetCount(iod->ESDescriptors); j++) {
				esd = ChainGetEntry(iod->ESDescriptors, j);
				if (esd->decoderConfig && esd->decoderConfig->streamType == M4ST_SCENE) {
					if (!sc->ESID) sc->ESID = esd->ESID;
					if (sc->ESID == esd->ESID) {
						is_in_iod = 1;
						break;
					}
				}
				/*special BIFS direct import from NHNT*/
				else if (ChainGetCount(iod->ESDescriptors)==1) {
					sc->ESID = esd->ESID;
					is_in_iod = 1;
					break;
				}
				esd = NULL;
			}
		}
		// if (!esd && sc->ESID) esd = M4SM_LocateESD(ctx, sc->ESID);

		if (!esd) {
			delete_desc = 1;
			esd = OD_NewESDescriptor(2);
			OD_DeleteDescriptor((Descriptor **) &esd->decoderConfig->decoderSpecificInfo);
			esd->ESID = sc->ESID;
			esd->decoderConfig->streamType = M4ST_SCENE;
		}

		/*should NOT happen (means inputctx is not properly setup)*/
		if (!esd->decoderConfig->decoderSpecificInfo) {
			memset(&bcfg, 0, sizeof(BIFSConfigDescriptor));
			bcfg.tag = BIFSConfig_Tag;
		}
		/*regular retrieve from ctx*/
		else if (esd->decoderConfig->decoderSpecificInfo->tag == BIFSConfig_Tag) {
			memcpy(&bcfg, (BIFSConfigDescriptor *)esd->decoderConfig->decoderSpecificInfo, sizeof(BIFSConfigDescriptor));
		}
		/*should not happen either (unless loading from MP4 in which case BIFSc is not decoded)*/
		else {
			OD_GetBIFSConfig(esd->decoderConfig->decoderSpecificInfo, esd->decoderConfig->objectTypeIndication, &bcfg);
		}
		/*NO CHANGE TO BIFSC otherwise the generated update will not match the input context*/
		nbb = GetNbBits(ctx->max_node_id);
		if (bcfg.nodeIDbits<nbb) fprintf(stdout, "Warning: BIFSConfig.NodeIDBits TOO SMALL\n");
		nbb = GetNbBits(ctx->max_route_id);
		if (bcfg.routeIDbits<nbb) fprintf(stdout, "Warning: BIFSConfig.RouteIDBits TOO SMALL\n");
		nbb = GetNbBits(ctx->max_proto_id);
		if (bcfg.protoIDbits<nbb) fprintf(stdout, "Warning: BIFSConfig.ProtoIDBits TOO SMALL\n");

		/*this is the real pb, not stored in cfg or file level, set at EACH replaceScene*/
		encode_names = 0;

		/* The BIFS Config that is passed here should be the BIFSConfig from the IOD */
		BIFS_NewStream(bifsenc, sc->ESID, &bcfg, encode_names, 0);

		/*setup MP4 track*/
		if (!esd->slConfig) esd->slConfig = (SLConfigDescriptor *) OD_NewDescriptor(SLConfigDescriptor_Tag);
		if (sc->timeScale) esd->slConfig->timestampResolution = sc->timeScale;
		if (!esd->slConfig->timestampResolution) esd->slConfig->timestampResolution = 1000;
//		track = M4_NewTrack(dest, sc->ESID, M4_BIFSMediaType, esd->slConfig->timestampResolution );
//		M4_SetTrackEnabled(dest, track, 1);
//		if (!sc->ESID) sc->ESID = M4_GetTrackID(dest, track);
		esd->ESID = sc->ESID;
		BIFS_GetStreamConfig(bifsenc, sc->ESID, &data, &data_len);

		if (esd->decoderConfig->decoderSpecificInfo) OD_DeleteDescriptor((Descriptor **) &esd->decoderConfig->decoderSpecificInfo);
		esd->decoderConfig->decoderSpecificInfo = (DefaultDescriptor *) OD_NewDescriptor(DecoderSpecificInfo_Tag);
		esd->decoderConfig->decoderSpecificInfo->data = data;
		esd->decoderConfig->decoderSpecificInfo->dataLength = data_len;
		esd->decoderConfig->objectTypeIndication = BIFS_GetVersion(bifsenc, sc->ESID);		

		
		/*create stream description*/
//		M4_NewStreamDescription(dest, track, esd, NULL, NULL, &di);
//		if (is_in_iod) M4_AddTrackToRootOD(dest, track);

		for (j=1; j<ChainGetCount(sc->AUs); j++) {
			au = ChainGetEntry(sc->AUs, j);
			samp = M4_NewSample();
			/*in case using XMT*/
//			if (au->timing_sec) au->timing = (u32) (au->timing_sec * esd->slConfig->timestampResolution);
//			samp->DTS = au->timing;
			e = BIFS_EncodeAU(bifsenc, sc->ESID, au->commands, &samp->data, &samp->dataLength);
			if (1)
			{
				FILE *f = fopen("test2.bifs", "wb");
				fwrite(samp->data, 1, samp->dataLength, f);
				fclose(f);
				//AUCallback(samp);
			} else {
				M4_AddSample(dest, track, 1, samp);
			}
		}
	}
//	if (dest) {
//		if (e) M4_MovieDelete(dest);
//		else M4_MovieClose(dest);
//	}
	return e;
}


/**
 * @chunkFile BT chunk to be encoded
 * @bifs output file name for the BIFS data
 * @inputContext initial BT upon which the chunk is based (shall not be NULL)
 * @outputContext: file name to dump the context after applying the new chunk to the input context 
                   can be NULL, without .bt
 * @logFile: can be NULL
 */
M4Err EncodeFileChunk(char *chunkFile, char *bifs, char *inputContext, char *outputContext) 
{
	M4Err			e;
	LPSCENEGRAPH	sg;
	M4SceneManager	*ctx;
	M4ContextLoader load;

	/*Step 1: create context and load input*/
	sg = NewSceneGraph();
	ctx = NewSceneManager(sg);
	memset(&load, 0, sizeof(M4ContextLoader));
	load.fileName = inputContext;
	load.ctx = ctx;
	/*since we're encoding we must get MPEG4 nodes only*/
	load.flags = M4CL_MPEG4_STRICT;
	e = M4SM_LoaderInit(&load);
	if (!e) e = M4SM_LoaderRun(&load);
	M4SM_LoaderDone(&load);
	if (e) {
		fprintf(stdout, "Cannot load context %s - %s\n", inputContext, M4ErrToString(e));
		goto exit;
	}

	/* Step 2: make sure we have only ONE RAP for each stream*/
	e = M4SM_MakeRandomAccess(ctx);
	if (e) goto exit;
	
	/*Step 3: loading the chunk into the context*/
	memset(&load, 0, sizeof(M4ContextLoader));
	load.fileName = chunkFile;
	load.ctx = ctx;
	load.flags = M4CL_MPEG4_STRICT | M4CL_CTX_IS_LOADED;
	e = M4SM_LoaderInit(&load);
	if (!e) e = M4SM_LoaderRun(&load);
	M4SM_LoaderDone(&load);
	if (e) {
		fprintf(stdout, "Cannot load chunk context %s - %s\n", chunkFile, M4ErrToString(e));
		goto exit;
	}

	/* Assumes that the first AU contains only one command a SceneReplace and 
	   that is not part of the current chunk */
	/* Last argument is a callback to pass the encoded AUs: not needed here 
	   Saving is not handled correctly */
	e = EncodeBIFSChunk(ctx, bifs, NULL); 
	if (e) goto exit;


	if (outputContext) {
		u32 d_mode, do_enc;
		char szF[M4_MAX_PATH], *ext;

		/*make random access for storage*/
		e = M4SM_MakeRandomAccess(ctx);
		if (e) goto exit;
		
		/*check if we dump to BT, XMT or encode to MP4*/
		strcpy(szF, outputContext);
		ext = strrchr(szF, '.');
		d_mode = M4SM_DUMP_BT;
		do_enc = 0;
		if (ext) {
			if (!stricmp(ext, ".xmt") || !stricmp(ext, ".xmta")) d_mode = M4SM_DUMP_XMTA;
			else if (!stricmp(ext, ".mp4")) do_enc = 1;
			ext[0] = 0;
		}

		if (do_enc) {
			M4File *mp4;
			strcat(szF, ".mp4");
			mp4 = M4_MovieOpen(szF, M4_WRITE_EDIT);
			e = M4SM_EncodeFile(ctx, mp4, NULL, NULL, 0, 0);
			if (e) M4_MovieDelete(mp4);
			else M4_MovieClose(mp4);
		}
		else e = M4SM_DumpToText(ctx, szF, d_mode);
	}

exit:
	if (ctx) {
		sg = ctx->scene_graph;
		M4SM_Delete(ctx);
		SG_Delete(sg);
	}
	return e;
}

#endif

