// =================================================================================================
// ADOBE SYSTEMS INCORPORATED
// Copyright 2006 Adobe Systems Incorporated
// All Rights Reserved
//
// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
// of the Adobe license agreement accompanying it.
// =================================================================================================

#include <cstdlib>

#include "MPEG4_Handler.hpp"
#include "ISOBaseMedia_Support.hpp"
#include "MOOV_Support.hpp"

#include "UnicodeConversions.hpp"
#include "MD5.h"

#if XMP_WinBuild
	#pragma warning ( disable : 4996 )	// '...' was declared deprecated
#endif

using namespace std;

// =================================================================================================
/// \file MPEG4_Handler.cpp
/// \brief File format handler for MPEG-4, a flavor of the ISO Base Media File Format.
///
/// This handler ...
///
// =================================================================================================

// The basic content of a timecode sample description table entry. Does not include trailing boxes.

#pragma pack ( push, 1 )

struct stsdBasicEntry {
	XMP_Uns32 entrySize;
	XMP_Uns32 format;
	XMP_Uns8  reserved_1 [6];
	XMP_Uns16 dataRefIndex;
	XMP_Uns32 reserved_2;
	XMP_Uns32 flags;
	XMP_Uns32 timeScale;
	XMP_Uns32 frameDuration;
	XMP_Uns8  frameCount;
	XMP_Uns8  reserved_3;
};

#pragma pack ( pop )

// =================================================================================================

// ! The buffer and constants are both already big endian.
#define Get4CharCode(buffPtr) (*((XMP_Uns32*)(buffPtr)))

// =================================================================================================

static inline bool IsClassicQuickTimeBox ( XMP_Uns32 boxType )
{
	if ( (boxType == ISOMedia::k_moov) || (boxType == ISOMedia::k_mdat) || (boxType == ISOMedia::k_pnot) ||
		 (boxType == ISOMedia::k_free) || (boxType == ISOMedia::k_skip) || (boxType == ISOMedia::k_wide) ) return true;
	return false;
}	// IsClassicQuickTimeBox

// =================================================================================================

// Pairs of 3 letter ISO 639-2 codes mapped to 2 letter ISO 639-1 codes from:
//   http://www.loc.gov/standards/iso639-2/php/code_list.php
// Would have to write an "==" operator to use std::map, must compare values not pointers.
// ! Not fully sorted, do not use a binary search.

static XMP_StringPtr kKnownLangs[] =
	{ "aar", "aa", "abk", "ab", "afr", "af", "aka", "ak", "alb", "sq", "sqi", "sq", "amh", "am",
	  "ara", "ar", "arg", "an", "arm", "hy", "hye", "hy", "asm", "as", "ava", "av", "ave", "ae",
	  "aym", "ay", "aze", "az", "bak", "ba", "bam", "bm", "baq", "eu", "eus", "eu", "bel", "be",
	  "ben", "bn", "bih", "bh", "bis", "bi", "bod", "bo", "tib", "bo", "bos", "bs", "bre", "br",
	  "bul", "bg", "bur", "my", "mya", "my", "cat", "ca", "ces", "cs", "cze", "cs", "cha", "ch",
	  "che", "ce", "chi", "zh", "zho", "zh", "chu", "cu", "chv", "cv", "cor", "kw", "cos", "co",
	  "cre", "cr", "cym", "cy", "wel", "cy", "cze", "cs", "ces", "cs", "dan", "da", "deu", "de",
	  "ger", "de", "div", "dv", "dut", "nl", "nld", "nl", "dzo", "dz", "ell", "el", "gre", "el",
	  "eng", "en", "epo", "eo", "est", "et", "eus", "eu", "baq", "eu", "ewe", "ee", "fao", "fo",
	  "fas", "fa", "per", "fa", "fij", "fj", "fin", "fi", "fra", "fr", "fre", "fr", "fre", "fr",
	  "fra", "fr", "fry", "fy", "ful", "ff", "geo", "ka", "kat", "ka", "ger", "de", "deu", "de",
	  "gla", "gd", "gle", "ga", "glg", "gl", "glv", "gv", "gre", "el", "ell", "el", "grn", "gn",
	  "guj", "gu", "hat", "ht", "hau", "ha", "heb", "he", "her", "hz", "hin", "hi", "hmo", "ho",
	  "hrv", "hr", "scr", "hr", "hun", "hu", "hye", "hy", "arm", "hy", "ibo", "ig", "ice", "is",
	  "isl", "is", "ido", "io", "iii", "ii", "iku", "iu", "ile", "ie", "ina", "ia", "ind", "id",
	  "ipk", "ik", "isl", "is", "ice", "is", "ita", "it", "jav", "jv", "jpn", "ja", "kal", "kl",
	  "kan", "kn", "kas", "ks", "kat", "ka", "geo", "ka", "kau", "kr", "kaz", "kk", "khm", "km",
	  "kik", "ki", "kin", "rw", "kir", "ky", "kom", "kv", "kon", "kg", "kor", "ko", "kua", "kj",
	  "kur", "ku", "lao", "lo", "lat", "la", "lav", "lv", "lim", "li", "lin", "ln", "lit", "lt",
	  "ltz", "lb", "lub", "lu", "lug", "lg", "mac", "mk", "mkd", "mk", "mah", "mh", "mal", "ml",
	  "mao", "mi", "mri", "mi", "mar", "mr", "may", "ms", "msa", "ms", "mkd", "mk", "mac", "mk",
	  "mlg", "mg", "mlt", "mt", "mol", "mo", "mon", "mn", "mri", "mi", "mao", "mi", "msa", "ms",
	  "may", "ms", "mya", "my", "bur", "my", "nau", "na", "nav", "nv", "nbl", "nr", "nde", "nd",
	  "ndo", "ng", "nep", "ne", "nld", "nl", "dut", "nl", "nno", "nn", "nob", "nb", "nor", "no",
	  "nya", "ny", "oci", "oc", "oji", "oj", "ori", "or", "orm", "om", "oss", "os", "pan", "pa",
	  "per", "fa", "fas", "fa", "pli", "pi", "pol", "pl", "por", "pt", "pus", "ps", "que", "qu",
	  "roh", "rm", "ron", "ro", "rum", "ro", "rum", "ro", "ron", "ro", "run", "rn", "rus", "ru",
	  "sag", "sg", "san", "sa", "scc", "sr", "srp", "sr", "scr", "hr", "hrv", "hr", "sin", "si",
	  "slk", "sk", "slo", "sk", "slo", "sk", "slk", "sk", "slv", "sl", "sme", "se", "smo", "sm",
	  "sna", "sn", "snd", "sd", "som", "so", "sot", "st", "spa", "es", "sqi", "sq", "alb", "sq",
	  "srd", "sc", "srp", "sr", "scc", "sr", "ssw", "ss", "sun", "su", "swa", "sw", "swe", "sv",
	  "tah", "ty", "tam", "ta", "tat", "tt", "tel", "te", "tgk", "tg", "tgl", "tl", "tha", "th",
	  "tib", "bo", "bod", "bo", "tir", "ti", "ton", "to", "tsn", "tn", "tso", "ts", "tuk", "tk",
	  "tur", "tr", "twi", "tw", "uig", "ug", "ukr", "uk", "urd", "ur", "uzb", "uz", "ven", "ve",
	  "vie", "vi", "vol", "vo", "wel", "cy", "cym", "cy", "wln", "wa", "wol", "wo", "xho", "xh",
	  "yid", "yi", "yor", "yo", "zha", "za", "zho", "zh", "chi", "zh", "zul", "zu",
	  0, 0 };

static inline XMP_StringPtr Lookup2LetterLang ( XMP_StringPtr lang3 )
{
	for ( size_t i = 0; kKnownLangs[i] != 0; i += 2 ) {
		if ( XMP_LitMatch ( lang3, kKnownLangs[i] ) ) return kKnownLangs[i+1];
	}
	return "";
}

static inline XMP_StringPtr Lookup3LetterLang ( XMP_StringPtr lang2 )
{
	for ( size_t i = 0; kKnownLangs[i] != 0; i += 2 ) {
		if ( XMP_LitMatch ( lang2, kKnownLangs[i+1] ) ) return kKnownLangs[i];
	}
	return "";
}

// =================================================================================================
// MPEG4_CheckFormat
// =================
//
// There are 3 variations of recognized file:
//	- Normal MPEG-4 - has an 'ftyp' box containing a known compatible brand but not 'qt  '.
//	- Modern QuickTime - has an 'ftyp' box containing 'qt  ' as a compatible brand.
//	- Classic QuickTime - has no 'ftyp' box, should have recognized top level boxes.
//
// An MPEG-4 or modern QuickTime file is an instance of an ISO Base Media file, ISO 14496-12 and -14.
// A classic QuickTime file has the same physical box structure, but somewhat different box types.
// The ISO files must begin with an 'ftyp' box containing 'mp41', 'mp42', 'f4v ', or 'qt  ' in the
// compatible brands.
//
// The general box structure is:
//
//   0  4  uns32  box size, 0 means "to EoF", 1 means 64-bit size follows
//   4  4  uns32  box type
//   8  8  uns64  box size, present only if uns32 size is 1
//   -  *  box content
//
// The 'ftyp' box content is:
//
//   -  4  uns32  major brand
//   -  4  uns32  minor version
//   -  *  uns32  sequence of compatible brands, to the end of the box

// ! With the addition of QuickTime support there is some change in behavior in OpenFile when the
// ! kXMPFiles_OpenStrictly option is used wth a specific file format. The kXMP_MPEG4File and
// ! kXMP_MOVFile formats are treated uniformly, you can't force "real MOV" or "real MPEG-4". You
// ! can check afterwards using GetFileInfo to see what the file happens to be.

bool MPEG4_CheckFormat ( XMP_FileFormat format,
						 XMP_StringPtr  filePath,
						 LFA_FileRef    fileRef,
						 XMPFiles*      parent )
{
	XMP_Uns8  buffer [4*1024];
	XMP_Uns32 ioCount, brandCount, brandOffset;
	XMP_Uns64 fileSize, nextOffset;
	ISOMedia::BoxInfo currBox;

	#define IsTolerableBoxChar(ch)	( ((0x20 <= (ch)) && ((ch) <= 0x7E)) || ((ch) == 0xA9) )

	XMP_AbortProc abortProc  = parent->abortProc;
	void *        abortArg   = parent->abortArg;
	const bool    checkAbort = (abortProc != 0);

	bool openStrictly = XMP_OptionIsSet ( parent->openFlags, kXMPFiles_OpenStrictly);

	// Get the first box's info, see if it is 'ftyp' or not.

	XMP_Assert ( (parent->tempPtr == 0) && (parent->tempUI32 == 0) );

	fileSize = LFA_Measure ( fileRef );
	nextOffset = ISOMedia::GetBoxInfo ( fileRef, 0, fileSize, &currBox );
	if ( currBox.headerSize < 8 ) return false;	// Can't be an ISO or QuickTime file.

	if ( currBox.boxType == ISOMedia::k_ftyp ) {

		// Have an 'ftyp' box, look through the compatible brands. If 'qt  ' is present then this is
		// a modern QuickTime file, regardless of what else is found. Otherwise this is plain ISO if
		// any of the other recognized brands are found.

		if ( currBox.contentSize < 12 ) return false;	// No compatible brands at all.
		if ( currBox.contentSize > 1024*1024 ) return false;	// Sanity check and make sure count fits in 32 bits.
		brandCount = ((XMP_Uns32)currBox.contentSize - 8) >> 2;

		LFA_Seek ( fileRef, 8, SEEK_CUR );	// Skip the major and minor brands.
		ioCount = brandOffset = 0;

		bool haveCompatibleBrand = false;

		for ( ; brandCount > 0; --brandCount, brandOffset += 4 ) {

			if ( brandOffset >= ioCount ) {
				if ( checkAbort && abortProc(abortArg) ) {
					XMP_Throw ( "MPEG4_CheckFormat - User abort", kXMPErr_UserAbort );
				}
				ioCount = 4 * brandCount;
				if ( ioCount > sizeof(buffer) ) ioCount = sizeof(buffer);
				ioCount = LFA_Read ( fileRef, buffer, ioCount, kLFA_RequireAll );
				brandOffset = 0;
			}

			XMP_Uns32 brand = GetUns32BE ( &buffer[brandOffset] );
			if ( brand == ISOMedia::k_qt ) {	// Don't need to look further.
				if ( openStrictly && (format != kXMP_MOVFile) ) return false;
				parent->format = kXMP_MOVFile;
				parent->tempUI32 = MOOV_Manager::kFileIsModernQT;
				return true;
			} else if ( (brand == ISOMedia::k_mp41) || (brand == ISOMedia::k_mp42) || (brand == ISOMedia::k_f4v) ) {
				haveCompatibleBrand = true;	// Need to keep looking in case 'qt  ' follows.
			}

		}

		if ( ! haveCompatibleBrand ) return false;
		if ( openStrictly && (format != kXMP_MPEG4File) ) return false;
		parent->format = kXMP_MPEG4File;
		parent->tempUI32 = MOOV_Manager::kFileIsNormalISO;
		return true;

	} else {

		// No 'ftyp', look for classic QuickTime: 'moov', 'mdat', 'pnot', 'free', 'skip', and 'wide'.
		// As an expedient, quit when a 'moov' box is found. Tolerate other boxes, they are in the
		// wild for ill-formed files, e.g. seen when 'moov'/'udta' children get left at top level.

		while ( currBox.boxType != ISOMedia::k_moov ) {

			if ( ! IsClassicQuickTimeBox ( currBox.boxType ) ) {
				// Make sure the box type is 4 ASCII characters or 0xA9 (MacRoman copyright).
				XMP_Uns8 b1 = (XMP_Uns8) (currBox.boxType >> 24);
				XMP_Uns8 b2 = (XMP_Uns8) ((currBox.boxType >> 16) & 0xFF);
				XMP_Uns8 b3 = (XMP_Uns8) ((currBox.boxType >> 8) & 0xFF);
				XMP_Uns8 b4 = (XMP_Uns8) (currBox.boxType & 0xFF);
				bool ok = IsTolerableBoxChar(b1) && IsTolerableBoxChar(b2) &&
						  IsTolerableBoxChar(b3) && IsTolerableBoxChar(b4);
				if ( ! ok ) return false;
			}
			if ( nextOffset >= fileSize ) return false;
			if ( checkAbort && abortProc(abortArg) ) {
				XMP_Throw ( "MPEG4_CheckFormat - User abort", kXMPErr_UserAbort );
			}
			nextOffset = ISOMedia::GetBoxInfo ( fileRef, nextOffset, fileSize, &currBox );

		}

		if ( openStrictly && (format != kXMP_MOVFile) ) return false;
		parent->format = kXMP_MOVFile;
		parent->tempUI32 = MOOV_Manager::kFileIsTraditionalQT;
		return true;

	}

	return false;

}	// MPEG4_CheckFormat

// =================================================================================================
// MPEG4_MetaHandlerCTor
// =====================

XMPFileHandler * MPEG4_MetaHandlerCTor ( XMPFiles * parent )
{

	return new MPEG4_MetaHandler ( parent );

}	// MPEG4_MetaHandlerCTor

// =================================================================================================
// MPEG4_MetaHandler::MPEG4_MetaHandler
// ====================================

MPEG4_MetaHandler::MPEG4_MetaHandler ( XMPFiles * _parent )
	: fileMode(0), havePreferredXMP(false), xmpBoxPos(0), moovBoxPos(0), xmpBoxSize(0), moovBoxSize(0)
{

	this->parent = _parent;	// Inherited, can't set in the prefix.
	this->handlerFlags = kMPEG4_HandlerFlags;
	this->stdCharForm  = kXMP_Char8Bit;
	this->fileMode = (XMP_Uns8)_parent->tempUI32;
	_parent->tempUI32 = 0;

}	// MPEG4_MetaHandler::MPEG4_MetaHandler

// =================================================================================================
// MPEG4_MetaHandler::~MPEG4_MetaHandler
// =====================================

MPEG4_MetaHandler::~MPEG4_MetaHandler()
{

	// Nothing to do.

}	// MPEG4_MetaHandler::~MPEG4_MetaHandler

// =================================================================================================
// SecondsToXMPDate
// ================

// *** ASF has similar code with different origin, should make a shared utility.

static void SecondsToXMPDate ( XMP_Uns64 isoSeconds, XMP_DateTime * xmpDate )
{
	memset ( xmpDate, 0, sizeof(XMP_DateTime) );	// AUDIT: Using sizeof(XMP_DateTime) is safe.

	XMP_Int32 days = (XMP_Int32) (isoSeconds / 86400);
	isoSeconds -= ((XMP_Uns64)days * 86400);

	XMP_Int32 hour = (XMP_Int32) (isoSeconds / 3600);
	isoSeconds -= ((XMP_Uns64)hour * 3600);

	XMP_Int32 minute = (XMP_Int32) (isoSeconds / 60);
	isoSeconds -= ((XMP_Uns64)minute * 60);

	XMP_Int32 second = (XMP_Int32)isoSeconds;

	xmpDate->year  = 1904;	// Start with the ISO origin.
	xmpDate->month = 1;
	xmpDate->day   = 1;

	xmpDate->day += days;	// Add in the delta.
	xmpDate->hour = hour;
	xmpDate->minute = minute;
	xmpDate->second = second;

	xmpDate->hasTimeZone = true;	// ! Needed for ConvertToUTCTime to do anything.
	SXMPUtils::ConvertToUTCTime ( xmpDate );	// Normalize the date/time.

}	// SecondsToXMPDate

// =================================================================================================
// XMPDateToSeconds
// ================

// *** ASF has similar code with different origin, should make a shared utility.

static bool IsLeapYear ( XMP_Int32 year )
{
	if ( year < 0 ) year = -year + 1;		// Fold the negative years, assuming there is a year 0.
	if ( (year % 4) != 0 ) return false;	// Not a multiple of 4.
	if ( (year % 100) != 0 ) return true;	// A multiple of 4 but not a multiple of 100.
	if ( (year % 400) == 0 ) return true;	// A multiple of 400.
	return false;							// A multiple of 100 but not a multiple of 400.
}

// -------------------------------------------------------------------------------------------------

static XMP_Int32 DaysInMonth ( XMP_Int32 year, XMP_Int32 month )
{
	static XMP_Int32 daysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
										 // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
	XMP_Int32 days = daysInMonth[month];
	if ( (month == 2) && IsLeapYear(year) ) days += 1;
	return days;
}

// -------------------------------------------------------------------------------------------------

static void XMPDateToSeconds ( const XMP_DateTime & _xmpDate, XMP_Uns64 * isoSeconds )
{
	XMP_DateTime xmpDate = _xmpDate;
	SXMPUtils::ConvertToUTCTime ( &xmpDate );

	XMP_Uns64 tempSeconds = (XMP_Uns64)xmpDate.second;
	tempSeconds += (XMP_Uns64)xmpDate.minute * 60;
	tempSeconds += (XMP_Uns64)xmpDate.hour * 3600;

	XMP_Int32 days = (xmpDate.day - 1);

	--xmpDate.month;
	while ( xmpDate.month >= 1 ) {
		days += DaysInMonth ( xmpDate.year, xmpDate.month );
		--xmpDate.month;
	}

	--xmpDate.year;
	while ( xmpDate.year >= 1904 ) {
		days += (IsLeapYear ( xmpDate.year) ? 366 : 365 );
		--xmpDate.year;
	}

	tempSeconds += (XMP_Uns64)days * 86400;
	*isoSeconds = tempSeconds;

}	// XMPDateToSeconds

// =================================================================================================
// ImportMVHDItems
// ===============

static bool ImportMVHDItems ( MOOV_Manager::BoxInfo mvhdInfo, SXMPMeta * xmp )
{
	XMP_Assert ( mvhdInfo.boxType == ISOMedia::k_mvhd );
	if ( mvhdInfo.contentSize < 4 ) return false;	// Just enough to check the version/flags at first.
	
	XMP_Uns8 mvhdVersion = *mvhdInfo.content;
	if ( mvhdVersion > 1 ) return false;
	
	XMP_Uns64 creationTime, modificationTime, duration;
	XMP_Uns32 timescale;
	
	if ( mvhdVersion == 0 ) {
	
		if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_0 ) ) return false;
		MOOV_Manager::Content_mvhd_0 * mvhdRaw_0 = (MOOV_Manager::Content_mvhd_0*) mvhdInfo.content;

		creationTime = (XMP_Uns64) GetUns32BE ( &mvhdRaw_0->creationTime );
		modificationTime = (XMP_Uns64) GetUns32BE ( &mvhdRaw_0->modificationTime );
		timescale = GetUns32BE ( &mvhdRaw_0->timescale );
		duration = (XMP_Uns64) GetUns32BE ( &mvhdRaw_0->duration );
	
	} else {

		XMP_Assert ( mvhdVersion == 1 );
		if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_1 ) ) return false;
		MOOV_Manager::Content_mvhd_1 * mvhdRaw_1 = (MOOV_Manager::Content_mvhd_1*) mvhdInfo.content;

		creationTime = GetUns64BE ( &mvhdRaw_1->creationTime );
		modificationTime = GetUns64BE ( &mvhdRaw_1->modificationTime );
		timescale = GetUns32BE ( &mvhdRaw_1->timescale );
		duration = GetUns64BE ( &mvhdRaw_1->duration );

	}

	bool haveImports = false;
	XMP_DateTime xmpDate;

	if ( (creationTime >> 32) < 0xFF ) {		// Sanity check for bogus date info.
		SecondsToXMPDate ( creationTime, &xmpDate );
		xmp->SetProperty_Date ( kXMP_NS_XMP, "CreateDate", xmpDate );
		haveImports = true;
	}

	if ( (modificationTime >> 32) < 0xFF ) {	// Sanity check for bogus date info.
		SecondsToXMPDate ( modificationTime, &xmpDate );
		xmp->SetProperty_Date ( kXMP_NS_XMP, "ModifyDate", xmpDate );
		haveImports = true;
	}

	if ( timescale != 0 ) {	// Avoid 1/0 for the scale field.
		char buffer [32];	// A 64-bit number is at most 20 digits.
		xmp->DeleteProperty ( kXMP_NS_DM, "duration" );	// Delete the whole struct.
		snprintf ( buffer, sizeof(buffer), "%llu", duration );	// AUDIT: The buffer is big enough.
		xmp->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "value", &buffer[0] );
		snprintf ( buffer, sizeof(buffer), "1/%u", timescale );	// AUDIT: The buffer is big enough.
		xmp->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "scale", &buffer[0] );
		haveImports = true;
	}

	return haveImports;

}	// ImportMVHDItems

// =================================================================================================
// ExportMVHDItems
// ===============

static void ExportMVHDItems ( const SXMPMeta & xmp, MOOV_Manager * moovMgr )
{
	XMP_DateTime xmpDate;
	bool createFound, modifyFound;
	XMP_Uns64 createSeconds = 0, modifySeconds = 0;
	
	MOOV_Manager::BoxInfo mvhdInfo;
	MOOV_Manager::BoxRef  mvhdRef = moovMgr->GetBox ( "moov/mvhd", &mvhdInfo );
	if ( (mvhdRef == 0) || (mvhdInfo.contentSize < 4) ) return;
	
	XMP_Uns8 version = *mvhdInfo.content;
	if ( version > 1 ) return;

	createFound = xmp.GetProperty_Date ( kXMP_NS_XMP, "CreateDate", &xmpDate, 0 );
	if ( createFound ) XMPDateToSeconds ( xmpDate, &createSeconds );

	modifyFound = xmp.GetProperty_Date ( kXMP_NS_XMP, "ModifyDate", &xmpDate, 0 );
	if ( modifyFound ) XMPDateToSeconds ( xmpDate, &modifySeconds );
	
	if ( version == 1 ) {
	
		// Modify the v1 box in-place.

		if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_1 ) ) return;
		
		XMP_Uns64 oldCreate = GetUns64BE ( mvhdInfo.content + 4 );
		XMP_Uns64 oldModify = GetUns64BE ( mvhdInfo.content + 12 );
		
		if ( createFound ) {
			if ( createSeconds != oldCreate ) PutUns64BE ( createSeconds, ((XMP_Uns8*)mvhdInfo.content + 4) );
			moovMgr->NoteChange();
		}
		if ( modifyFound  ) {
			if ( modifySeconds != oldModify ) PutUns64BE ( modifySeconds, ((XMP_Uns8*)mvhdInfo.content + 12) );
			moovMgr->NoteChange();
		}
		
	} else if ( ((createSeconds >> 32) == 0) && ((modifySeconds >> 32) == 0) ) {
	
		// Modify the v0 box in-place.

		if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_0 ) ) return;
		
		XMP_Uns32 oldCreate = GetUns32BE ( mvhdInfo.content + 4 );
		XMP_Uns32 oldModify = GetUns32BE ( mvhdInfo.content + 8 );
		
		if ( createFound ) {
			if ( (XMP_Uns32)createSeconds != oldCreate ) PutUns32BE ( (XMP_Uns32)createSeconds, ((XMP_Uns8*)mvhdInfo.content + 4) );
			moovMgr->NoteChange();
		}
		if ( modifyFound ) {
			if ( (XMP_Uns32)modifySeconds != oldModify ) PutUns32BE ( (XMP_Uns32)modifySeconds, ((XMP_Uns8*)mvhdInfo.content + 8) );
			moovMgr->NoteChange();
		}
	
	} else {

		// Replace the v0 box with a v1 box.

		XMP_Assert ( createFound | modifyFound );	// One of them has high bits set.
		if ( mvhdInfo.contentSize != sizeof ( MOOV_Manager::Content_mvhd_0 ) ) return;
	
		MOOV_Manager::Content_mvhd_0 * mvhdV0 = (MOOV_Manager::Content_mvhd_0*) mvhdInfo.content;
		MOOV_Manager::Content_mvhd_1   mvhdV1;
		
		// Copy the unchanged fields directly.
		
		mvhdV1.timescale = mvhdV0->timescale;
		mvhdV1.rate = mvhdV0->rate;
		mvhdV1.volume = mvhdV0->volume;
		mvhdV1.pad_1 = mvhdV0->pad_1;
		mvhdV1.pad_2 = mvhdV0->pad_2;
		mvhdV1.pad_3 = mvhdV0->pad_3;
		for ( int i = 0; i < 9; ++i ) mvhdV1.matrix[i] = mvhdV0->matrix[i];
		for ( int i = 0; i < 6; ++i ) mvhdV1.preDef[i] = mvhdV0->preDef[i];
		mvhdV1.nextTrackID = mvhdV0->nextTrackID;
		
		// Set the fields that have changes.
		
		mvhdV1.vFlags = (1 << 24) | (mvhdV0->vFlags & 0xFFFFFF);
		mvhdV1.duration = MakeUns64BE ( (XMP_Uns64) GetUns32BE ( &mvhdV0->duration ) );
		
		XMP_Uns64 temp64;
		
		temp64 = (XMP_Uns64) GetUns32BE ( &mvhdV0->creationTime );
		if ( createFound ) temp64 = createSeconds;
		mvhdV1.creationTime = MakeUns64BE ( temp64 );
		
		temp64 = (XMP_Uns64) GetUns32BE ( &mvhdV0->modificationTime );
		if ( modifyFound ) temp64 = modifySeconds;
		mvhdV1.modificationTime = MakeUns64BE ( temp64 );
		
		moovMgr->SetBox ( mvhdRef, &mvhdV1, sizeof ( MOOV_Manager::Content_mvhd_1 ) );
	
	}

}	// ExportMVHDItems

// =================================================================================================
// ImportISOCopyrights
// ===================
//
// The cached 'moov'/'udta'/'cprt' boxes are full boxes. The "real" content is a UInt16 packed 3
// character language code and a UTF-8 or UTF-16 string.

static bool ImportISOCopyrights ( const std::vector<MOOV_Manager::BoxInfo> & cprtBoxes, SXMPMeta * xmp )
{
	bool haveImports = false;

	std::string tempStr;
	char lang3 [4];	// The unpacked ISO-639-2/T language code with final null.
	lang3[3] = 0;

	for ( size_t i = 0, limit = cprtBoxes.size(); i < limit; ++i ) {
	
		const MOOV_Manager::BoxInfo & currBox = cprtBoxes[i];
		if ( currBox.contentSize < 4+2+1 ) continue;	// Want enough for a non-empty value.
		if ( *currBox.content != 0 ) continue;	// Only proceed for version 0, ignore the flags.

		XMP_Uns16 packedLang = GetUns16BE ( currBox.content + 4 );
		lang3[0] = (packedLang >> 10) + 0x60;
		lang3[1] = ((packedLang >> 5) & 0x1F) + 0x60;
		lang3[2] = (packedLang & 0x1F) + 0x60;
		XMP_StringPtr xmpLang  = Lookup2LetterLang ( lang3 );
		if ( *xmpLang == 0 ) continue;

		XMP_StringPtr textPtr = (XMP_StringPtr) (currBox.content + 6);
		XMP_StringLen textLen = currBox.contentSize - 6;

		if ( (textLen >= 2) && (GetUns16BE(textPtr) == 0xFEFF) ) {
			FromUTF16 ( (UTF16Unit*)textPtr, textLen/2, &tempStr, true /* big endian */ );
			textPtr = tempStr.c_str();
		}

		xmp->SetLocalizedText ( kXMP_NS_DC, "rights", xmpLang, xmpLang, textPtr );
		haveImports = true;

	}

	return haveImports;

}	// ImportISOCopyrights

// =================================================================================================
// ExportISOCopyrights
// ===================

static void ExportISOCopyrights ( const SXMPMeta & xmp, MOOV_Manager * moovMgr )
{
	bool haveMappings = false;	// True if any ISO-XMP language mappings are found.

	// Go through the ISO 'cprt' items and look for a corresponding XMP item. Ignore the ISO item if
	// there is no language mapping to XMP. Update the ISO item if the mappable XMP exists, delete
	// the ISO item if the mappable XMP does not exist. Since the import side would have made sure
	// the mappable XMP items existed, if they don't now they must have been deleted.

	MOOV_Manager::BoxInfo udtaInfo;
	MOOV_Manager::BoxRef  udtaRef = moovMgr->GetBox ( "moov/udta", &udtaInfo );
	if ( udtaRef == 0 ) return;

	std::string xmpPath, xmpValue, xmpLang, tempStr;
	char lang3 [4];	// An unpacked ISO-639-2/T language code.
	lang3[3] = 0;

	for ( XMP_Uns32 ordinal = udtaInfo.childCount; ordinal > 0; --ordinal ) {	// ! Go backwards because of deletions.

		MOOV_Manager::BoxInfo cprtInfo;
		MOOV_Manager::BoxRef  cprtRef = moovMgr->GetNthChild ( udtaRef, ordinal-1, &cprtInfo );
		if ( (cprtRef == 0) ) break;	// Sanity check, should not happen.
		if ( (cprtInfo.boxType != ISOMedia::k_cprt) || (cprtInfo.contentSize < 6) ) continue;
		if ( *cprtInfo.content != 0 ) continue;	// Only accept version 0, ignore the flags.

		XMP_Uns16 packedLang = GetUns16BE ( cprtInfo.content + 4 );
		lang3[0] = (packedLang >> 10) + 0x60;
		lang3[1] = ((packedLang >> 5) & 0x1F) + 0x60;
		lang3[2] = (packedLang & 0x1F) + 0x60;

		XMP_StringPtr lang2  = Lookup2LetterLang ( lang3 );
		if ( *lang2 == 0 ) continue;	// No language mapping to XMP.
		haveMappings = true;

		bool xmpFound = xmp.GetLocalizedText ( kXMP_NS_DC, "rights", lang2, lang2, &xmpLang, &xmpValue, 0 );
		if ( xmpFound ) {
			if ( (xmpLang.size() < 2) ||
				 ( (xmpLang.size() == 2) && (xmpLang != lang2) ) ||
				 ( (xmpLang.size() > 2) && ( (xmpLang[2] != '-') || (! XMP_LitNMatch ( xmpLang.c_str(), lang2, 2)) ) ) ) {
				xmpFound = false;	// The language does not match, the corresponding XMP does not exist.
			}
		}

		if ( ! xmpFound ) {

			// No XMP, delete the ISO item.
			moovMgr->DeleteNthChild ( udtaRef, ordinal-1 );

		} else {

			// Update the ISO item if necessary.
			XMP_StringPtr isoStr = (XMP_StringPtr)cprtInfo.content + 6;
			size_t rawLen = cprtInfo.contentSize - 6;
			if ( (rawLen >= 8) && (GetUns16BE(isoStr) == 0xFEFF) ) {
				FromUTF16 ( (UTF16Unit*)(isoStr+2), (rawLen-2)/2, &tempStr, true /* big endian */ );
				isoStr = tempStr.c_str();
			}
			if ( xmpValue != isoStr ) {
				std::string newContent = "vfffll";
				newContent += xmpValue;
				memcpy ( (char*)newContent.c_str(), cprtInfo.content, 6 ); // Keep old version, flags, and language.
				moovMgr->SetBox ( cprtRef, newContent.c_str(), (XMP_Uns32)(newContent.size() + 1) );
			}

		}

	}

	// Go through the XMP items and look for a corresponding ISO item. Skip if found (did it above),
	// otherwise add a new ISO item.

	bool haveXDefault = false;
	XMP_Index xmpCount = xmp.CountArrayItems ( kXMP_NS_DC, "rights" );

	for ( XMP_Index xmpIndex = 1; xmpIndex <= xmpCount; ++xmpIndex ) {	// ! The first XMP array index is 1.

		SXMPUtils::ComposeArrayItemPath ( kXMP_NS_DC, "rights", xmpIndex, &xmpPath );
		xmp.GetArrayItem ( kXMP_NS_DC, "rights", xmpIndex, &xmpValue, 0 );
		bool hasLang = xmp.GetQualifier ( kXMP_NS_DC, xmpPath.c_str(), kXMP_NS_XML, "lang", &xmpLang, 0 );
		if ( ! hasLang ) continue;	// Sanity check.
		if ( xmpLang == "x-default" ) {
			haveXDefault = true;	// See later special case.
			continue;
		}

		XMP_StringPtr isoLang = "";
		size_t rootLen = xmpLang.find ( '-' );
		if ( rootLen == std::string::npos ) rootLen = xmpLang.size();
		if ( rootLen == 2 ) {
			xmpLang[2] = 0;
			isoLang = Lookup3LetterLang ( xmpLang.c_str() );
			if ( *isoLang == 0 ) continue;
		} else if ( rootLen == 3 ) {
			xmpLang[3] = 0;
			isoLang = xmpLang.c_str();
		} else {
			continue;
		}
		haveMappings = true;

		bool isoFound = false;
		XMP_Uns16 packedLang = ((isoLang[0] - 0x60) << 10) | ((isoLang[1] - 0x60) << 5) | (isoLang[2] - 0x60);

		for ( XMP_Uns32 isoIndex = 0; (isoIndex < udtaInfo.childCount) && (! isoFound); ++isoIndex ) {

			MOOV_Manager::BoxInfo cprtInfo;
			MOOV_Manager::BoxRef  cprtRef = moovMgr->GetNthChild ( udtaRef, isoIndex, &cprtInfo );
			if ( (cprtRef == 0) ) break;	// Sanity check, should not happen.
			if ( (cprtInfo.boxType != ISOMedia::k_cprt) || (cprtInfo.contentSize < 6) ) continue;
			if ( *cprtInfo.content != 0 ) continue;	// Only accept version 0, ignore the flags.
			if ( packedLang != GetUns16BE ( cprtInfo.content + 4 ) ) continue;	// Look for matching language.

			isoFound = true;	// Found the language entry, whether or not we update it.

		}

		if ( ! isoFound ) {

			std::string newContent = "vfffll";
			newContent += xmpValue;
			*((XMP_Uns32*)newContent.c_str()) = 0;	// Set the version and flags to zero.
			PutUns16BE ( packedLang, (char*)newContent.c_str() + 4 );
			moovMgr->AddChildBox ( udtaRef, ISOMedia::k_cprt, newContent.c_str(), (XMP_Uns32)(newContent.size() + 1) );

		}

	}

	// If there were no mappings in the loops, export the XMP "x-default" value to the first ISO item.

	if ( ! haveMappings ) {

		MOOV_Manager::BoxInfo cprtInfo;
		MOOV_Manager::BoxRef  cprtRef = moovMgr->GetTypeChild ( udtaRef, ISOMedia::k_cprt, &cprtInfo );
		
		if ( (cprtRef != 0) && (cprtInfo.contentSize >= 6) && (*cprtInfo.content == 0) ) {

			bool xmpFound = xmp.GetLocalizedText ( kXMP_NS_DC, "rights", "", "x-default", &xmpLang, &xmpValue, 0 );
	
			if ( xmpFound && (xmpLang == "x-default") ) {
	
				// Update the ISO item if necessary.
				XMP_StringPtr isoStr = (XMP_StringPtr)cprtInfo.content + 6;
				size_t rawLen = cprtInfo.contentSize - 6;
				if ( (rawLen >= 8) && (GetUns16BE(isoStr) == 0xFEFF) ) {
					FromUTF16 ( (UTF16Unit*)(isoStr+2), (rawLen-2)/2, &tempStr, true /* big endian */ );
					isoStr = tempStr.c_str();
				}
				if ( xmpValue != isoStr ) {
					std::string newContent = "vfffll";
					newContent += xmpValue;
					memcpy ( (char*)newContent.c_str(), cprtInfo.content, 6 ); // Keep old version, flags, and language.
					moovMgr->SetBox ( cprtRef, newContent.c_str(), (XMP_Uns32)(newContent.size() + 1) );
				}
	
			}
		
		}

	}

}	// ExportISOCopyrights

// =================================================================================================
// ExportQuickTimeItems
// ====================

static void ExportQuickTimeItems ( const SXMPMeta & xmp, TradQT_Manager * qtMgr, MOOV_Manager * moovMgr )
{
	
	// The QuickTime 'udta' timecode items are done here for simplicity.

	#define createWithZeroLang true
	
	qtMgr->ExportSimpleXMP ( kQTilst_Reel, xmp, kXMP_NS_DM, "tapeName" );
	qtMgr->ExportSimpleXMP ( kQTilst_Timecode, xmp, kXMP_NS_DM, "startTimecode/xmpDM:timeValue", createWithZeroLang );
	qtMgr->ExportSimpleXMP ( kQTilst_TimeScale, xmp, kXMP_NS_DM, "startTimeScale", createWithZeroLang );
	qtMgr->ExportSimpleXMP ( kQTilst_TimeSize, xmp, kXMP_NS_DM, "startTimeSampleSize", createWithZeroLang );

	qtMgr->UpdateChangedBoxes ( moovMgr );

}	// ExportQuickTimeItems

// =================================================================================================
// SelectTimeFormat
// ================

static const char * SelectTimeFormat ( const MPEG4_MetaHandler::TimecodeTrackInfo & tmcdInfo )
{
	const char * timeFormat = 0;

	float fltFPS = (float)tmcdInfo.timeScale / (float)tmcdInfo.frameDuration;
	int   intFPS = (int) (fltFPS + 0.5);

	switch ( intFPS ) {

		case 30:
			if ( fltFPS >= 29.985 ) {
				timeFormat = "30Timecode";
			} else if ( tmcdInfo.isDropFrame ) {
				timeFormat = "2997DropTimecode";
			} else {
				timeFormat = "2997NonDropTimecode";
			}
			break;

		case 24:
			if ( fltFPS >= 23.988 ) {
				timeFormat = "24Timecode";
			} else {
				timeFormat = "23976Timecode";
			}
			break;

		case 25:
			timeFormat = "25Timecode";
			break;

		case 50:
			timeFormat = "50Timecode";
			break;

		case 60:
			if ( fltFPS >= 59.97 ) {
				timeFormat = "60Timecode";
			} else if ( tmcdInfo.isDropFrame ) {
				timeFormat = "5994DropTimecode";
			} else {
				timeFormat = "5994NonDropTimecode";
			}
			break;

	}

	return timeFormat;

}	// SelectTimeFormat

// =================================================================================================
// SelectTimeFormat
// ================

static const char * SelectTimeFormat ( const SXMPMeta & xmp )
{
	bool ok;
	MPEG4_MetaHandler::TimecodeTrackInfo tmcdInfo;

	XMP_Int64 timeScale;
	ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeScale", &timeScale, 0 );
	if ( ! ok ) return 0;
	tmcdInfo.timeScale = (XMP_Uns32)timeScale;

	XMP_Int64 frameDuration;
	ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeSampleSize", &frameDuration, 0 );
	if ( ! ok ) return 0;
	tmcdInfo.frameDuration = (XMP_Uns32)frameDuration;

	std::string timecode;
	ok = xmp.GetProperty ( kXMP_NS_DM, "startTimecode/xmpDM:timeValue", &timecode, 0 );
	if ( ! ok ) return 0;
	if ( (timecode.size() == 11) && (timecode[8] == ';') ) tmcdInfo.isDropFrame = true;

	return SelectTimeFormat ( tmcdInfo );

}	// SelectTimeFormat

// =================================================================================================
// ComposeTimecode
// ===============

static const char * kDecDigits = "0123456789";
#define TwoDigits(val,str)	(str)[0] = kDecDigits[(val)/10]; (str)[1] = kDecDigits[(val)%10]

static bool ComposeTimecode ( const MPEG4_MetaHandler::TimecodeTrackInfo & tmcdInfo, std::string * strValue )
{
	float fltFPS = (float)tmcdInfo.timeScale / (float)tmcdInfo.frameDuration;
	int   intFPS = (int) (fltFPS + 0.5);
	if ( (intFPS != 30) && (intFPS != 24) && (intFPS != 25) && (intFPS != 50) && (intFPS != 60) ) return false;

	XMP_Uns32 framesPerDay = intFPS * 24*60*60;
	XMP_Uns32 dropLimit = 2;	// Used in the drop-frame correction.

	if ( tmcdInfo.isDropFrame ) {
		if ( intFPS == 30 ) {
			framesPerDay = 2589408;	// = 29.97 * 24*60*60
		} else if ( intFPS == 60 ) {
			framesPerDay = 5178816;	// = 59.94 * 24*60*60
			dropLimit = 4;
		} else {
			strValue->erase();
			return false;	// Dropframe can only apply to 29.97 and 59.94.
		}
	}

	XMP_Uns32 framesPerHour = framesPerDay / 24;
	XMP_Uns32 framesPerTenMinutes = framesPerHour / 6;
	XMP_Uns32 framesPerMinute = framesPerTenMinutes / 10;

	XMP_Uns32 frameCount = tmcdInfo.timecodeSample;
	while (frameCount >= framesPerDay ) frameCount -= framesPerDay;	// Normalize to be within 24 hours.

	XMP_Uns32 hours, minHigh, minLow, seconds;

	hours = frameCount / framesPerHour;
	frameCount -= (hours * framesPerHour);

	minHigh = frameCount / framesPerTenMinutes;
	frameCount -= (minHigh * framesPerTenMinutes);

	minLow = frameCount / framesPerMinute;
	frameCount -= (minLow * framesPerMinute);

	// Do some drop-frame corrections at this point: If this is drop-frame and the units of minutes
	// is non-zero, and the seconds are zero, and the frames are zero or one, the time is illegal.
	// Perform correction by subtracting 1 from the units of minutes and adding 1798 to the frames.Ê
	// For example, 1:00:00 becomes 59:28, and 1:00:01 becomes 59:29. A special case can occur for
	// when the frameCount just before the minHigh calculation is less than framesPerTenMinutes but
	// more than 10*framesPerMinute. This happens because of roundoff, and will result in a minHigh
	// of 0 and a minLow of 10.ÊThe drop frame correction mustÊalso be performed for this case.

	if ( tmcdInfo.isDropFrame ) {
		if ( (minLow == 10) || ((minLow != 0) && (frameCount < dropLimit)) ) {
			minLow -= 1;
			frameCount += framesPerMinute;
		}
	}

	seconds = frameCount / intFPS;
	frameCount -= (seconds * intFPS);

	if ( tmcdInfo.isDropFrame ) {
		*strValue = "hh;mm;ss;ff";
	} else {
		*strValue = "hh:mm:ss:ff";
	}

	char * str = (char*)strValue->c_str();
	TwoDigits ( hours, str );
	str[3] = kDecDigits[minHigh]; str[4] = kDecDigits[minLow];
	TwoDigits ( seconds, str+6 );
	TwoDigits ( frameCount, str+9 );

	return true;

}	// ComposeTimecode

// =================================================================================================
// DecomposeTimecode
// =================

static bool DecomposeTimecode ( const char * strValue, MPEG4_MetaHandler::TimecodeTrackInfo * tmcdInfo )
{
	float fltFPS = (float)tmcdInfo->timeScale / (float)tmcdInfo->frameDuration;
	int   intFPS = (int) (fltFPS + 0.5);
	if ( (intFPS != 30) && (intFPS != 24) && (intFPS != 25) && (intFPS != 50) && (intFPS != 60) ) return false;

	XMP_Uns32 framesPerDay = intFPS * 24*60*60;

	int items, hours, minutes, seconds, frames;

	if ( ! tmcdInfo->isDropFrame ) {
		items = sscanf ( strValue, "%d:%d:%d:%d", &hours, &minutes, &seconds, &frames );
	} else {
		items = sscanf ( strValue, "%d;%d;%d;%d", &hours, &minutes, &seconds, &frames );
		if ( intFPS == 30 ) {
			framesPerDay = 2589408;	// = 29.97 * 24*60*60
		} else if ( intFPS == 60 ) {
			framesPerDay = 5178816;	// = 59.94 * 24*60*60
		} else {
			return false;	// Dropframe can only apply to 29.97 and 59.94.
		}
	}
	
	if ( items != 4 ) return false;
	int minHigh = minutes / 10;
	int minLow = minutes % 10;

	XMP_Uns32 framesPerHour = framesPerDay / 24;
	XMP_Uns32 framesPerTenMinutes = framesPerHour / 6;
	XMP_Uns32 framesPerMinute = framesPerTenMinutes / 10;

	tmcdInfo->timecodeSample = (hours * framesPerHour) + (minHigh * framesPerTenMinutes) +
							   (minLow * framesPerMinute) + (seconds * intFPS) + frames;

	return true;

}	// DecomposeTimecode

// =================================================================================================
// FindTimecodeTrack
// =================
//
// Look for a well-formed timecode track, return the .../mdia/minf/stbl box ref. 

static MOOV_Manager::BoxRef FindTimecodeTrack ( const MOOV_Manager & moovMgr )
{

	// Find a 'trak' box with a handler type of 'tmcd'.
	
	MOOV_Manager::BoxInfo moovInfo;
	MOOV_Manager::BoxRef  moovRef = moovMgr.GetBox ( "moov", &moovInfo );
	XMP_Assert ( moovRef != 0 );
	
	MOOV_Manager::BoxInfo trakInfo;
	MOOV_Manager::BoxRef  trakRef;
	
	size_t i = 0;
	for ( ; i < moovInfo.childCount; ++i ) {

		trakRef = moovMgr.GetNthChild ( moovRef, i, &trakInfo );
		if ( trakRef == 0 ) return 0;	// Sanity check, should not happen.
		if ( trakInfo.boxType != ISOMedia::k_trak ) continue;
		
		MOOV_Manager::BoxRef  innerRef;
		MOOV_Manager::BoxInfo innerInfo;
		
		innerRef = moovMgr.GetTypeChild ( trakRef, ISOMedia::k_mdia, &innerInfo );
		if ( innerRef == 0 ) continue;
		
		innerRef = moovMgr.GetTypeChild ( innerRef, ISOMedia::k_hdlr, &innerInfo );
		if ( (innerRef == 0) || (innerInfo.contentSize < sizeof ( MOOV_Manager::Content_hdlr )) ) continue;
		
		const MOOV_Manager::Content_hdlr * hdlr = (MOOV_Manager::Content_hdlr*) innerInfo.content;
		if ( hdlr->versionFlags != 0 ) continue;
		if ( GetUns32BE ( &hdlr->handlerType ) == ISOMedia::k_tmcd ) break;

	}
	if ( i == moovInfo.childCount ) return 0;

	// Find the .../mdia/minf/stbl box.
	
	MOOV_Manager::BoxInfo tempInfo;
	MOOV_Manager::BoxRef  tempRef, stblRef;
	
	tempRef = moovMgr.GetTypeChild ( trakRef, ISOMedia::k_mdia, &tempInfo );
	if ( tempRef == 0 ) return 0;
	
	tempRef = moovMgr.GetTypeChild ( tempRef, ISOMedia::k_minf, &tempInfo );
	if ( tempRef == 0 ) return 0;
	
	stblRef = moovMgr.GetTypeChild ( tempRef, ISOMedia::k_stbl, &tempInfo );
	return stblRef;

}	// FindTimecodeTrack

// =================================================================================================
// ImportTimecodeItems
// ===================

static bool ImportTimecodeItems ( const MPEG4_MetaHandler::TimecodeTrackInfo & tmcdInfo,
								  const TradQT_Manager & qtInfo, SXMPMeta * xmp )
{
	std::string xmpValue;
	bool haveItem;
	bool haveImports = false;

	// The QT user data item '©REL' goes into xmpDM:tapeName, and the 'name' box at the end of the
	// timecode sample description goes into xmpDM:altTapeName.
	haveImports |= qtInfo.ImportSimpleXMP ( kQTilst_Reel, xmp, kXMP_NS_DM, "tapeName" );
	if ( ! tmcdInfo.macName.empty() ) {
		haveItem = ConvertFromMacLang ( tmcdInfo.macName, tmcdInfo.macLang, &xmpValue );
		if ( haveItem ) {
			xmp->SetProperty ( kXMP_NS_DM, "altTapeName", xmpValue.c_str() );
			haveImports = true;
		}
	}

	// The QT user data item '©TSC' goes into xmpDM:startTimeScale. If that isn't present, then
	// the timecode sample description's timeScale is used.
	haveItem = qtInfo.ImportSimpleXMP ( kQTilst_TimeScale, xmp, kXMP_NS_DM, "startTimeScale" );
	if ( tmcdInfo.stsdBoxFound & (! haveItem) ) {
		xmp->SetProperty_Int64 ( kXMP_NS_DM, "startTimeScale", tmcdInfo.timeScale );
		haveItem = true;
	}
	haveImports |= haveItem;

	// The QT user data item '©TSZ' goes into xmpDM:startTimeSampleSize. If that isn't present, then
	// the timecode sample description's frameDuration is used.
	haveItem = qtInfo.ImportSimpleXMP ( kQTilst_TimeSize, xmp, kXMP_NS_DM, "startTimeSampleSize" );
	if ( tmcdInfo.stsdBoxFound & (! haveItem) ) {
		xmp->SetProperty_Int64 ( kXMP_NS_DM, "startTimeSampleSize", tmcdInfo.frameDuration );
		haveItem = true;
	}
	haveImports |= haveItem;

	const char * timeFormat;

	// The Timecode struct type is used for xmpDM:startTimecode and xmpDM:altTimecode. For both, only
	// the xmpDM:timeValue and xmpDM:timeFormat fields are set.

	// The QT user data item '©TIM' goes into xmpDM:startTimecode/xmpDM:timeValue. This is an already
	// formatted timecode string. The XMP values of xmpDM:startTimeScale, xmpDM:startTimeSampleSize,
	// and xmpDM:startTimecode/xmpDM:timeValue are used to select the timeFormat value.
	haveImports |= qtInfo.ImportSimpleXMP ( kQTilst_Timecode, xmp, kXMP_NS_DM, "startTimecode/xmpDM:timeValue" );
	timeFormat = SelectTimeFormat ( *xmp );
	if ( timeFormat != 0 ) {
		xmp->SetProperty ( kXMP_NS_DM, "startTimecode/xmpDM:timeFormat", timeFormat );
		haveImports = true;
	}

	if ( tmcdInfo.stsdBoxFound ) {

		haveItem = ComposeTimecode ( tmcdInfo, &xmpValue );
		if ( haveItem ) {
			xmp->SetProperty ( kXMP_NS_DM, "altTimecode/xmpDM:timeValue", xmpValue.c_str() );
			haveImports = true;
		}

		timeFormat = SelectTimeFormat ( tmcdInfo );
		if ( timeFormat != 0 ) {
			xmp->SetProperty ( kXMP_NS_DM, "altTimecode/xmpDM:timeFormat", timeFormat );
			haveImports = true;
		}

	}

	return haveImports;

}	// ImportTimecodeItems

// =================================================================================================
// ExportTimecodeItems
// ===================

static void ExportTimecodeItems ( const SXMPMeta & xmp, MPEG4_MetaHandler::TimecodeTrackInfo * tmcdInfo,
								  TradQT_Manager * qtMgr, MOOV_Manager * moovMgr )
{
	// Export the items that go into the timecode track:
	//  - the timescale and frame duration in the first 'stsd' table entry
	//  - the 'name' box appended to the first 'stsd' table entry
	//  - the first timecode sample
	// ! The QuickTime 'udta' timecode items are handled in ExportQuickTimeItems.

	if ( ! tmcdInfo->stsdBoxFound ) return;	// Don't make changes unless there is a well-formed timecode track.
	
	MOOV_Manager::BoxRef stblRef = FindTimecodeTrack ( *moovMgr );
	if ( stblRef == 0 ) return;
	
	MOOV_Manager::BoxInfo stsdInfo;
	MOOV_Manager::BoxRef  stsdRef;
	
	stsdRef = moovMgr->GetTypeChild ( stblRef, ISOMedia::k_stsd, &stsdInfo );
	if ( stsdRef == 0 ) return;
	if ( stsdInfo.contentSize < (8 + sizeof ( MOOV_Manager::Content_stsd_entry )) ) return;
	if ( GetUns32BE ( stsdInfo.content + 4 ) == 0 ) return;	// Make sure the entry count is non-zero.
	
	const MOOV_Manager::Content_stsd_entry * stsdRawEntry = (MOOV_Manager::Content_stsd_entry*) (stsdInfo.content + 8);

	XMP_Uns32 stsdEntrySize = GetUns32BE ( &stsdRawEntry->entrySize );
	if ( stsdEntrySize > (stsdInfo.contentSize - 4) ) stsdEntrySize = stsdInfo.contentSize - 4;
	if ( stsdEntrySize < sizeof ( MOOV_Manager::Content_stsd_entry ) ) return;
	
	bool ok;
	std::string xmpValue;
	XMP_Int64 int64;	// Used to allow UInt32 values, GetProperty_Int is SInt32.
	
	// The tmcdInfo timeScale field is set from xmpDM:startTimeScale.
	 ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeScale", &int64, 0 );
	 if ( ok && (int64 <= 0xFFFFFFFF) && ((XMP_Uns32)int64 != tmcdInfo->timeScale) ) {
	 	tmcdInfo->timeScale = (XMP_Uns32)int64;
	 	PutUns32BE ( tmcdInfo->timeScale, (void*)&stsdRawEntry->timeScale );
	 	moovMgr->NoteChange();
	 }
	
	// The tmcdInfo frameDuration field is set from xmpDM:startTimeSampleSize.
	 ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeSampleSize", &int64, 0 );
	 if ( ok && (int64 <= 0xFFFFFFFF) && ((XMP_Uns32)int64 != tmcdInfo->frameDuration) ) {
	 	tmcdInfo->frameDuration = (XMP_Uns32)int64;
	 	PutUns32BE ( tmcdInfo->frameDuration, (void*)&stsdRawEntry->frameDuration );
	 	moovMgr->NoteChange();
	 }
	
	// The tmcdInfo isDropFrame flag is set from xmpDM:altTimecode/xmpDM:timeValue. The timeScale
	// and frameDuration must be updated first, they are used by DecomposeTimecode. Compute the new
	// UInt32 timecode sample, but it gets written to the file later by UpdateFile.

	ok = xmp.GetProperty ( kXMP_NS_DM, "altTimecode/xmpDM:timeValue", &xmpValue, 0 );
	if ( ok && (xmpValue.size() == 11) ) {

		bool oldDropFrame = tmcdInfo->isDropFrame;
		tmcdInfo->isDropFrame = false;
		if ( xmpValue[8] == ';' ) tmcdInfo->isDropFrame = true;
		if ( oldDropFrame != tmcdInfo->isDropFrame ) {
			XMP_Uns32 flags = GetUns32BE ( &stsdRawEntry->flags );
			flags = (flags & 0xFFFFFFFE) | (XMP_Uns32)tmcdInfo->isDropFrame;
			PutUns32BE ( flags, (void*)&stsdRawEntry->flags );
		 	moovMgr->NoteChange();
		}

		XMP_Uns32 oldSample = tmcdInfo->timecodeSample;
		ok = DecomposeTimecode ( xmpValue.c_str(), tmcdInfo );
	 	if ( ok && (oldSample != tmcdInfo->timecodeSample) ) moovMgr->NoteChange();

	}

	// The 'name' box attached to the first 'stsd' table entry is set from xmpDM:altTapeName.

	bool replaceNameBox = false;
	
	ok = xmp.GetProperty ( kXMP_NS_DM, "altTapeName", &xmpValue, 0 );
	if ( (! ok) || xmpValue.empty() ) {
		if ( tmcdInfo->nameOffset != 0 ) replaceNameBox = true;	// No XMP, get rid of existing name.
	} else {
		std::string macValue;
		ok = ConvertToMacLang ( xmpValue, tmcdInfo->macLang, &macValue );
		if ( ok && (macValue != tmcdInfo->macName) ) {
			tmcdInfo->macName = macValue;
			replaceNameBox = true;	// Write changed name.
		}
	}

	if ( replaceNameBox ) {
	
		// To replace the 'name' box we have to create an entire new 'stsd' box, and attach the
		// new name to the first 'stsd' table entry. The 'name' box content is a UInt16 text length,
		// UInt16 language code, and Mac encoded text with no nul termination.
		
		if ( tmcdInfo->macName.size() > 0xFFFF ) tmcdInfo->macName.erase ( 0xFFFF );
		
		ISOMedia::BoxInfo oldNameInfo;
		XMP_Assert ( (oldNameInfo.headerSize == 0) && (oldNameInfo.contentSize == 0) );
		if ( tmcdInfo->nameOffset != 0 ) {
			const XMP_Uns8 * oldNamePtr = stsdInfo.content + tmcdInfo->nameOffset;
			const XMP_Uns8 * oldNameLimit = stsdInfo.content + stsdInfo.contentSize;
			(void) ISOMedia::GetBoxInfo ( oldNamePtr, oldNameLimit, &oldNameInfo );
		}
		
		XMP_Uns32 oldNameBoxSize = (XMP_Uns32)oldNameInfo.headerSize + (XMP_Uns32)oldNameInfo.contentSize;
		XMP_Uns32 newNameBoxSize = 0;
		if ( ! tmcdInfo->macName.empty() ) newNameBoxSize = 4+4 + 2+2 + (XMP_Uns32)tmcdInfo->macName.size();
		
		XMP_Uns32 stsdNewContentSize = stsdInfo.contentSize - oldNameBoxSize + newNameBoxSize;
		RawDataBlock stsdNewContent;
		stsdNewContent.assign ( stsdNewContentSize, 0 );	// Get the space allocated, direct fill below.
		
		XMP_Uns32 stsdPrefixSize = tmcdInfo->nameOffset;
		if ( tmcdInfo->nameOffset == 0 ) stsdPrefixSize = 4+4 + sizeof ( MOOV_Manager::Content_stsd_entry );

		XMP_Uns32 oldSuffixOffset = stsdPrefixSize + oldNameBoxSize;
		XMP_Uns32 newSuffixOffset = stsdPrefixSize + newNameBoxSize;
		XMP_Uns32 stsdSuffixSize  = stsdInfo.contentSize - oldSuffixOffset;
		
		memcpy ( &stsdNewContent[0], stsdInfo.content, stsdPrefixSize );
		if ( stsdSuffixSize != 0 ) memcpy ( &stsdNewContent[newSuffixOffset], (stsdInfo.content + oldSuffixOffset), stsdSuffixSize );
	
		XMP_Uns32 newEntrySize = stsdEntrySize - oldNameBoxSize + newNameBoxSize;
		MOOV_Manager::Content_stsd_entry * stsdNewEntry = (MOOV_Manager::Content_stsd_entry*) (&stsdNewContent[0] + 8);
		PutUns32BE ( newEntrySize, &stsdNewEntry->entrySize );
		
		if ( newNameBoxSize != 0 ) {
			PutUns32BE ( newNameBoxSize, &stsdNewContent[stsdPrefixSize] );
			PutUns32BE ( ISOMedia::k_name, &stsdNewContent[stsdPrefixSize+4] );
			PutUns16BE ( (XMP_Uns16)tmcdInfo->macName.size(), &stsdNewContent[stsdPrefixSize+8] );
			PutUns16BE ( tmcdInfo->macLang, &stsdNewContent[stsdPrefixSize+10] );
			memcpy ( &stsdNewContent[stsdPrefixSize+12], tmcdInfo->macName.c_str(), tmcdInfo->macName.size() );
		}
		
		moovMgr->SetBox ( stsdRef, &stsdNewContent[0], stsdNewContentSize );

	}

}	// ExportTimecodeItems

// =================================================================================================
// ImportCr8rItems
// ===============

#pragma pack ( push, 1 )

struct PrmLBoxContent {
	XMP_Uns32 magic;
	XMP_Uns32 size;
	XMP_Uns16 verAPI;
	XMP_Uns16 verCode;
	XMP_Uns32 exportType;
	XMP_Uns16 MacVRefNum;
	XMP_Uns32 MacParID;
	char filePath[260];
};

enum { kExportTypeMovie = 0, kExportTypeStill = 1, kExportTypeAudio = 2, kExportTypeCustom = 3 }; 

struct Cr8rBoxContent {
	XMP_Uns32 magic;
	XMP_Uns32 size;
	XMP_Uns16 majorVer;
	XMP_Uns16 minorVer;
	XMP_Uns32 creatorCode;
	XMP_Uns32 appleEvent;
	char fileExt[16];
	char appOptions[16];
	char appName[32];
};

#pragma pack ( pop )

// -------------------------------------------------------------------------------------------------

static bool ImportCr8rItems ( const MOOV_Manager & moovMgr, SXMPMeta * xmp )
{
	bool haveXMP = false;
	
	MOOV_Manager::BoxInfo infoPrmL, infoCr8r;
	MOOV_Manager::BoxRef  refPrmL = moovMgr.GetBox ( "moov/udta/PrmL", &infoPrmL );
	MOOV_Manager::BoxRef  refCr8r = moovMgr.GetBox ( "moov/udta/Cr8r", &infoCr8r );
	
	bool havePrmL = ( (refPrmL != 0) && (infoPrmL.contentSize == sizeof ( PrmLBoxContent )) );
	bool haveCr8r = ( (refCr8r != 0) && (infoCr8r.contentSize == sizeof ( Cr8rBoxContent )) );
	
	if ( havePrmL ) {
	
		PrmLBoxContent rawPrmL;
		XMP_Assert ( sizeof ( rawPrmL ) == 282 );
		XMP_Assert ( sizeof ( rawPrmL.filePath ) == 260 );
		memcpy ( &rawPrmL, infoPrmL.content, sizeof ( rawPrmL ) );
		if ( rawPrmL.magic != 0xBEEFCAFE ) {
			Flip4 ( &rawPrmL.exportType );	// The only numeric field that we care about.
		}

		rawPrmL.filePath[259] = 0;	// Ensure a terminating nul.
		if ( rawPrmL.filePath[0] != 0 ) {
			if ( rawPrmL.filePath[0] == '/' ) {
				haveXMP = true;
				xmp->SetStructField ( kXMP_NS_CreatorAtom, "macAtom",
									  kXMP_NS_CreatorAtom, "posixProjectPath", rawPrmL.filePath );
			} else if ( XMP_LitNMatch ( rawPrmL.filePath, "\\\\?\\", 4 ) ) {
				haveXMP = true;
				xmp->SetStructField ( kXMP_NS_CreatorAtom, "windowsAtom",
									  kXMP_NS_CreatorAtom, "uncProjectPath", rawPrmL.filePath );
			}
		}
	
		const char * exportStr = 0;
		switch ( rawPrmL.exportType ) {
			case kExportTypeMovie  : exportStr = "movie";  break;
			case kExportTypeStill  : exportStr = "still";  break;
			case kExportTypeAudio  : exportStr = "audio";  break;
			case kExportTypeCustom : exportStr = "custom"; break;
		}
		if ( exportStr != 0 ) {
			haveXMP = true;
			xmp->SetStructField ( kXMP_NS_DM, "projectRef", kXMP_NS_DM, "type", exportStr );
		}
	
	}
	
	if ( haveCr8r ) {
		
		Cr8rBoxContent rawCr8r;
		XMP_Assert ( sizeof ( rawCr8r ) == 84 );
		XMP_Assert ( sizeof ( rawCr8r.fileExt ) == 16 );
		XMP_Assert ( sizeof ( rawCr8r.appOptions ) == 16 );
		XMP_Assert ( sizeof ( rawCr8r.appName ) == 32 );
		memcpy ( &rawCr8r, infoCr8r.content, sizeof ( rawCr8r ) );
		if ( rawCr8r.magic != 0xBEEFCAFE ) {
			Flip4 ( &rawCr8r.creatorCode );	// The only numeric fields that we care about.
			Flip4 ( &rawCr8r.appleEvent );
		}
	
		std::string fieldPath;
		
		SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "applicationCode", &fieldPath );
		if ( rawCr8r.creatorCode != 0 ) {
			haveXMP = true;
			xmp->SetProperty_Int64 ( kXMP_NS_CreatorAtom, fieldPath.c_str(), (XMP_Int64)rawCr8r.creatorCode );	// ! Unsigned trickery.
		}
		
		SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "invocationAppleEvent", &fieldPath );
		if ( rawCr8r.appleEvent != 0 ) {
			haveXMP = true;
			xmp->SetProperty_Int64 ( kXMP_NS_CreatorAtom, fieldPath.c_str(), (XMP_Int64)rawCr8r.appleEvent );	// ! Unsigned trickery.
		}
	
		rawCr8r.fileExt[15] = 0;	// Ensure a terminating nul.
		if ( rawCr8r.fileExt[0] != 0 ) {
			haveXMP = true;
			xmp->SetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "extension", rawCr8r.fileExt );
		}
	
		rawCr8r.appOptions[15] = 0;	// Ensure a terminating nul.
		if ( rawCr8r.appOptions[0] != 0 ) {
			haveXMP = true;
			xmp->SetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "invocationFlags", rawCr8r.appOptions );
		}
	
		rawCr8r.appName[31] = 0;	// Ensure a terminating nul.
		if ( rawCr8r.appName[0] != 0 ) {
			haveXMP = true;
			xmp->SetProperty ( kXMP_NS_XMP, "CreatorTool", rawCr8r.appName );
		}

	}
	
	return haveXMP;
	
}	// ImportCr8rItems

// =================================================================================================
// ExportCr8rItems
// ===============

static inline void SetBufferedString ( char * dest, const std::string source, size_t limit )
{
	memset ( dest, 0, limit );
	size_t count = source.size();
	if ( count >= limit ) count = limit - 1;	// Ensure a terminating nul.
	memcpy ( dest, source.c_str(), count );
}

// -------------------------------------------------------------------------------------------------

static void ExportCr8rItems ( const SXMPMeta & xmp, MOOV_Manager * moovMgr )
{
	bool haveNewCr8r = false;
	std::string creatorCode, appleEvent, fileExt, appOptions, appName;
	
	haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "applicationCode", &creatorCode, 0 );
	haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "invocationAppleEvent", &appleEvent, 0 );
	haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "extension", &fileExt, 0 );
	haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "invocationFlags", &appOptions, 0 );
	haveNewCr8r |= xmp.GetProperty ( kXMP_NS_XMP, "CreatorTool", &appName, 0 );
	
	MOOV_Manager::BoxInfo infoCr8r;
	MOOV_Manager::BoxRef  refCr8r = moovMgr->GetBox ( "moov/udta/Cr8r", &infoCr8r );
	bool haveOldCr8r = ( (refCr8r != 0) && (infoCr8r.contentSize == sizeof ( Cr8rBoxContent )) );
	
	if ( ! haveNewCr8r ) {
		if ( haveOldCr8r ) {
			MOOV_Manager::BoxRef udtaRef = moovMgr->GetBox ( "moov/udta", 0 );
			moovMgr->DeleteTypeChild ( udtaRef, 0x43723872 /* 'Cr8r' */ );
		}
		return;
	}
	
	Cr8rBoxContent newCr8r;
	const Cr8rBoxContent * oldCr8r = (Cr8rBoxContent*) infoCr8r.content;

	if ( ! haveOldCr8r ) {

		memset ( &newCr8r, 0, sizeof(newCr8r) );
		newCr8r.magic = MakeUns32BE ( 0xBEEFCAFE );
		newCr8r.size = MakeUns32BE ( sizeof ( newCr8r ) );
		newCr8r.majorVer = MakeUns16BE ( 1 );

	} else {

		memcpy ( &newCr8r, oldCr8r, sizeof(newCr8r) );
		if ( GetUns32BE ( &newCr8r.magic ) != 0xBEEFCAFE ) {	// Make sure we write BE numbers.
			Flip4 ( &newCr8r.magic );
			Flip4 ( &newCr8r.size );
			Flip2 ( &newCr8r.majorVer );
			Flip2 ( &newCr8r.minorVer );
			Flip4 ( &newCr8r.creatorCode );
			Flip4 ( &newCr8r.appleEvent );
		}

	}
	
	if ( ! creatorCode.empty() ) {
		newCr8r.creatorCode = MakeUns32BE ( (XMP_Uns32) strtoul ( creatorCode.c_str(), 0, 0 ) );
	}
	
	if ( ! appleEvent.empty() ) {
		newCr8r.appleEvent = MakeUns32BE ( (XMP_Uns32) strtoul ( appleEvent.c_str(), 0, 0 ) );
	}
	
	if ( ! fileExt.empty() ) SetBufferedString ( newCr8r.fileExt, fileExt, sizeof ( newCr8r.fileExt ) );
	if ( ! appOptions.empty() ) SetBufferedString ( newCr8r.appOptions, appOptions, sizeof ( newCr8r.appOptions ) );
	if ( ! appName.empty() ) SetBufferedString ( newCr8r.appName, appName, sizeof ( newCr8r.appName ) );
	
	moovMgr->SetBox ( "moov/udta/Cr8r", &newCr8r, sizeof(newCr8r) );
	
}	// ExportCr8rItems

// =================================================================================================
// GetAtomInfo
// ===========

struct AtomInfo {
	XMP_Int64 atomSize;
	XMP_Uns32 atomType;
	bool hasLargeSize;
};

enum {	// ! Do not rearrange, code depends on this order.
	kBadQT_NoError		= 0,	// No errors.
	kBadQT_SmallInner	= 1,	// An extra 1..7 bytes at the end of an inner span.
	kBadQT_LargeInner	= 2,	// More serious inner garbage, found as invalid atom length.
	kBadQT_SmallOuter	= 3,	// An extra 1..7 bytes at the end of the file.
	kBadQT_LargeOuter	= 4		// More serious EOF garbage, found as invalid atom length.
};
typedef XMP_Uns8 QTErrorMode;

static QTErrorMode GetAtomInfo ( const LFA_FileRef qtFile, XMP_Int64 spanSize, int nesting, AtomInfo * info )
{
	QTErrorMode status = kBadQT_NoError;
	XMP_Uns8 buffer [8];

	info->hasLargeSize = false;

	LFA_Read ( qtFile, buffer, 8, kLFA_RequireAll );	// Will throw if 8 bytes aren't available.
	info->atomSize = GetUns32BE ( &buffer[0] );	// ! Yes, the initial size is big endian UInt32.
	info->atomType = GetUns32BE ( &buffer[4] );

	if ( info->atomSize == 0 ) {	// Does the atom extend to EOF?

		if ( nesting != 0 ) return kBadQT_LargeInner;
		info->atomSize = spanSize;	// This outer atom goes to EOF.

	} else if ( info->atomSize == 1 ) {	// Does the atom have a 64-bit size?

		if ( spanSize < 16 ) {	// Is there room in the span for the 16 byte header?
			status = kBadQT_LargeInner;
			if ( nesting == 0 ) status += 2;	// Convert to "outer".
			return status;
		}

		LFA_Read ( qtFile, buffer, 8, kLFA_RequireAll );
		info->atomSize = (XMP_Int64) GetUns64BE ( &buffer[0] );
		info->hasLargeSize = true;

	}

	XMP_Assert ( status == kBadQT_NoError );
	return status;

}	// GetAtomInfo

// =================================================================================================
// CheckAtomList
// =============
//
// Check that a sequence of atoms fills a given span. The I/O position must be at the start of the
// span, it is left just past the span on success. Recursive checks are done for top level 'moov'
// atoms, and second level 'udta' atoms ('udta' inside 'moov').
//
// Checking continues for "small inner" errors. They will be reported if no other kinds of errors
// are found, otherwise the other error is reported. Checking is immediately aborted for any "large"
// error. The rationale is that QuickTime can apparently handle small inner errors. They might be
// arise from updates that shorten an atom by less than 8 bytes. Larger shrinkage should introduce a
// 'free' atom.

static QTErrorMode CheckAtomList ( const LFA_FileRef qtFile, XMP_Int64 spanSize, int nesting )
{
	QTErrorMode status = kBadQT_NoError;
	AtomInfo    info;

	const static XMP_Uns32 moovAtomType = 0x6D6F6F76;	// ! Don't use MakeUns32BE, already big endian.
	const static XMP_Uns32 udtaAtomType = 0x75647461;

	for ( ; spanSize >= 8; spanSize -= info.atomSize ) {

		QTErrorMode atomStatus = GetAtomInfo ( qtFile, spanSize, nesting, &info );
		if ( atomStatus != kBadQT_NoError ) return atomStatus;

		XMP_Int64 headerSize = 8;
		if ( info.hasLargeSize ) headerSize = 16;

		if ( (info.atomSize < headerSize) || (info.atomSize > spanSize) ) {
			status = kBadQT_LargeInner;
			if ( nesting == 0 ) status += 2;	// Convert to "outer".
			return status;
		}

		bool doChildren = false;
		if ( (nesting == 0) && (info.atomType == moovAtomType) ) doChildren = true;
		if ( (nesting == 1) && (info.atomType == udtaAtomType) ) doChildren = true;

		XMP_Int64 dataSize = info.atomSize - headerSize;

		if ( ! doChildren ) {
			LFA_Seek ( qtFile, dataSize, SEEK_CUR );
		} else {
			QTErrorMode innerStatus = CheckAtomList ( qtFile, dataSize, nesting+1 );
			if ( innerStatus > kBadQT_SmallInner ) return innerStatus;	// Quit for serious errors.
			if ( status == kBadQT_NoError ) status = innerStatus;	// Remember small inner errors.
		}

	}

	XMP_Assert ( status <= kBadQT_SmallInner );	// Else already returned.
	// ! Make sure inner kBadQT_SmallInner is propagated if this span is OK.

	if ( spanSize != 0 ) {
		LFA_Seek ( qtFile, spanSize, SEEK_CUR );	// ! Skip the trailing garbage of this span.
		status = kBadQT_SmallInner;
		if ( spanSize >= 8 ) status = kBadQT_LargeInner;
		if ( nesting == 0 ) status += 2;	// Convert to "outer".
	}

	return status;

}	// CheckAtomList

// =================================================================================================
// AttemptFileRepair
// =================

static void AttemptFileRepair ( LFA_FileRef qtFile, XMP_Int64 fileSpace, QTErrorMode status )
{

	switch ( status ) {
		case kBadQT_NoError    : return;	// Sanity check.
		case kBadQT_SmallInner : return;	// Fixed in normal update code for the 'udta' box.
		case kBadQT_LargeInner : XMP_Throw ( "Can't repair QuickTime file", kXMPErr_BadFileFormat );
		case kBadQT_SmallOuter : break;		// Truncate file below.
		case kBadQT_LargeOuter : break;		// Truncate file below.
		default                : XMP_Throw ( "Invalid QuickTime error mode", kXMPErr_InternalFailure );
	}

	AtomInfo info;
	XMP_Int64 headerSize;

	// Process the top level atoms until an error is found.

	LFA_Seek ( qtFile, 0, SEEK_SET );

	for ( ; fileSpace >= 8; fileSpace -= info.atomSize ) {

		QTErrorMode atomStatus = GetAtomInfo ( qtFile, fileSpace, 0, &info );

		headerSize = 8;	// ! Set this before checking atomStatus, used after the loop.
		if ( info.hasLargeSize ) headerSize = 16;

		if ( atomStatus != kBadQT_NoError ) break;
		if ( (info.atomSize < headerSize) || (info.atomSize > fileSpace) ) break;

		XMP_Int64 dataSize = info.atomSize - headerSize;
		LFA_Seek ( qtFile, dataSize, SEEK_CUR );

	}

	// Truncate the file. If fileSpace >= 8 then the loop exited early due to a bad atom, seek back
	// to the atom's start. Otherwise, the loop exited because no more atoms are possible, no seek.

	if ( fileSpace >= 8 ) LFA_Seek ( qtFile, -headerSize, SEEK_CUR );
	XMP_Int64 currPos = LFA_Tell ( qtFile );
	LFA_Truncate ( qtFile, currPos );

}	// AttemptFileRepair

// =================================================================================================
// CheckQTFileStructure
// ====================

static void CheckQTFileStructure ( XMPFileHandler * thiz, bool doRepair )
{
	XMPFiles * parent = thiz->parent;
	LFA_FileRef fileRef  = parent->fileRef;
	XMP_Int64 fileSize = LFA_Measure ( fileRef );

	// Check the basic file structure and try to repair if asked.

	LFA_Seek ( fileRef, 0, SEEK_SET );
	QTErrorMode status = CheckAtomList ( fileRef, fileSize, 0 );

	if ( status != kBadQT_NoError ) {
		if ( doRepair || (status == kBadQT_SmallInner) || (status == kBadQT_SmallOuter) ) {
			AttemptFileRepair ( fileRef, fileSize, status );	// Will throw if the attempt fails.
		} else if ( status != kBadQT_SmallInner ) {
			XMP_Throw ( "Ill-formed QuickTime file", kXMPErr_BadFileFormat );
		} else {
			return;	// ! Ignore these, QT seems to be able to handle them.
			// *** Might want to throw for check-only, ignore when repairing.
		}
	}

}	// CheckQTFileStructure;

// =================================================================================================
// CheckFinalBox
// =============
//
// Before appending anything new, check if the final top level box has a "to EoF" length. If so, fix
// it to have an explicit length.

static void CheckFinalBox ( LFA_FileRef fileRef, XMP_AbortProc abortProc, void * abortArg )
{
	const bool checkAbort = (abortProc != 0);

	XMP_Uns64 fileSize = LFA_Measure ( fileRef );

	// Find the last 2 boxes in the file. Need the previous to last in case it is an Apple 'wide' box.

	XMP_Uns64 prevPos, lastPos, nextPos;
	ISOMedia::BoxInfo prevBox, lastBox;
	XMP_Uns8 buffer [16];	// Enough to create an extended header.

	memset ( &prevBox, 0, sizeof(prevBox) );	// AUDIT: Using sizeof(prevBox) is safe.
	memset ( &lastBox, 0, sizeof(lastBox) );	// AUDIT: Using sizeof(lastBox) is safe.
	prevPos = lastPos = nextPos = 0;
	while ( nextPos != fileSize ) {
		if ( checkAbort && abortProc(abortArg) ) {
			XMP_Throw ( "MPEG4_MetaHandler::CheckFinalBox - User abort", kXMPErr_UserAbort );
		}
		prevBox = lastBox;
		prevPos = lastPos;
		lastPos = nextPos;
		nextPos = ISOMedia::GetBoxInfo ( fileRef, lastPos, fileSize, &lastBox, true /* throw errors */ );
	}

	// See if the last box is valid and has a "to EoF" size.

	if ( lastBox.headerSize < 8 ) XMP_Throw ( "MPEG-4 final box is invalid", kXMPErr_EnforceFailure );
	LFA_Seek ( fileRef, lastPos, SEEK_SET );
	LFA_Read ( fileRef, buffer, 4 );
	XMP_Uns64 lastSize = GetUns32BE ( &buffer[0] );	// ! Yes, the file has a 32-bit value.
	if ( lastSize != 0 ) return;

	// Have a final "to EoF" box, try to write the explicit size.

	lastSize = lastBox.headerSize + lastBox.contentSize;
	if ( lastSize <= 0xFFFFFFFFUL ) {

		// Fill in the 32-bit exact size.
		PutUns32BE ( (XMP_Uns32)lastSize, &buffer[0] );
		LFA_Seek ( fileRef, lastPos, SEEK_SET );
		LFA_Write ( fileRef, buffer, 4 );

	} else {

		// Try to convert to using an extended header.

		if ( (prevBox.boxType != ISOMedia::k_wide) || (prevBox.headerSize != 8) || (prevBox.contentSize != 0) ) {
			XMP_Throw ( "Can't expand final box header", kXMPErr_EnforceFailure );
		}
		XMP_Assert ( prevPos == (lastPos - 8) );

		PutUns32BE ( 1, &buffer[0] );
		PutUns32BE ( lastBox.boxType, &buffer[4] );
		PutUns64BE ( lastSize, &buffer[8] );
		LFA_Seek ( fileRef, prevPos, SEEK_SET );
		LFA_Write ( fileRef, buffer, 16 );

	}

}	// CheckFinalBox

// =================================================================================================
// WriteBoxHeader
// ==============

static void WriteBoxHeader ( LFA_FileRef fileRef, XMP_Uns32 boxType, XMP_Uns64 boxSize )
{
	XMP_Uns32 u32;
	XMP_Uns64 u64;
	XMP_Enforce ( boxSize >= 8 );	// The size must be the full size, not just the content.
	
	if ( boxSize <= 0xFFFFFFFF ) {

		u32 = MakeUns32BE ( (XMP_Uns32)boxSize );
		LFA_Write ( fileRef, &u32, 4 );
		u32 = MakeUns32BE ( boxType );
		LFA_Write ( fileRef, &u32, 4 );

	} else {

		u32 = MakeUns32BE ( 1 );
		LFA_Write ( fileRef, &u32, 4 );
		u32 = MakeUns32BE ( boxType );
		LFA_Write ( fileRef, &u32, 4 );
		u64 = MakeUns64BE ( boxSize );
		LFA_Write ( fileRef, &u64, 8 );

	}

}	// WriteBoxHeader

// =================================================================================================
// WipeBoxFree
// ===========
//
// Change the box's type to 'free' (or create a 'free' box) and zero the content.

static XMP_Uns8 kZeroes [64*1024];	// C semantics guarantee zero initialization.

static void WipeBoxFree ( LFA_FileRef fileRef, XMP_Uns64 boxOffset, XMP_Uns32 boxSize )
{
	if ( boxSize == 0 ) return;
	XMP_Enforce ( boxSize >= 8 );

	LFA_Seek ( fileRef, boxOffset, SEEK_SET );
	XMP_Uns32 u32;
	u32 = MakeUns32BE ( boxSize );	// ! The actual size should not change, but might have had a long header.
	LFA_Write ( fileRef, &u32, 4 );
	u32 = MakeUns32BE ( ISOMedia::k_free );
	LFA_Write ( fileRef, &u32, 4 );
	
	XMP_Uns32 ioCount = sizeof ( kZeroes );
	for ( boxSize -= 8; boxSize > 0; boxSize -= ioCount ) {
		if ( ioCount > boxSize ) ioCount = boxSize;
		LFA_Write ( fileRef, &kZeroes[0], ioCount );
	}
	
}	// WipeBoxFree

// =================================================================================================
// CreateFreeSpaceList
// ===================

struct SpaceInfo {
	XMP_Uns64 offset, size;
	SpaceInfo() : offset(0), size(0) {};
	SpaceInfo ( XMP_Uns64 _offset, XMP_Uns64 _size ) : offset(_offset), size(_size) {};
};

typedef std::vector<SpaceInfo> FreeSpaceList;

static void CreateFreeSpaceList ( LFA_FileRef fileRef, XMP_Uns64 fileSize,
								  XMP_Uns64 oldOffset, XMP_Uns32 oldSize, FreeSpaceList * spaceList )
{
	XMP_Uns64 boxPos, boxNext, adjacentFree;
	ISOMedia::BoxInfo currBox;

	LFA_Seek ( fileRef, 0, SEEK_SET );
	spaceList->clear();

	for ( boxPos = 0; boxPos < fileSize; boxPos = boxNext ) {

		boxNext = ISOMedia::GetBoxInfo ( fileRef, boxPos, fileSize, &currBox, true /* throw errors */ );
		XMP_Uns64 currSize = currBox.headerSize + currBox.contentSize;
		
		if ( (currBox.boxType == ISOMedia::k_free) ||
			 (currBox.boxType == ISOMedia::k_skip) ||
			 ((boxPos == oldOffset) && (currSize == oldSize)) ) {

			if ( spaceList->empty() || (boxPos != adjacentFree) ) {
				spaceList->push_back ( SpaceInfo ( boxPos, currSize ) );
				adjacentFree = boxPos + currSize;
			} else {
				SpaceInfo * lastSpace = &spaceList->back();
				lastSpace->size += currSize;
			}

		}
		
	}
	
}	// CreateFreeSpaceList

// =================================================================================================
// MPEG4_MetaHandler::CacheFileData
// ================================
//
// There are 3 file variants: normal ISO Base Media, modern QuickTime, and classic QuickTime. The
// XMP is placed differently between the ISO and two QuickTime forms, and there is different but not
// colliding native metadata. The entire 'moov' subtree is cached, along with the top level 'uuid'
// box of XMP if present.

void MPEG4_MetaHandler::CacheFileData()
{
	XMP_Assert ( ! this->containsXMP );

	XMPFiles * parent = this->parent;
	XMP_OptionBits openFlags = parent->openFlags;

	LFA_FileRef fileRef  = parent->fileRef;

	XMP_AbortProc abortProc  = parent->abortProc;
	void *        abortArg   = parent->abortArg;
	const bool    checkAbort = (abortProc != 0);

	// First do some special case repair to QuickTime files, based on bad files in the wild.

	const bool isUpdate = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenForUpdate );
	const bool doRepair = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenRepairFile );

	if ( isUpdate && (parent->format == kXMP_MOVFile) ) {
		CheckQTFileStructure ( this, doRepair );	// Will throw for failure.
	}

	// Cache the top level 'moov' and 'uuid'/XMP boxes.

	XMP_Uns64 fileSize = LFA_Measure ( fileRef );

	XMP_Uns64 boxPos, boxNext;
	ISOMedia::BoxInfo currBox;

	bool xmpOnly = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenOnlyXMP );
	bool haveISOFile = (this->fileMode == MOOV_Manager::kFileIsNormalISO);

	bool uuidFound = (! haveISOFile);			// Ignore the XMP 'uuid' box for QuickTime files.
	bool moovIgnored = (xmpOnly & haveISOFile);	// Ignore the 'moov' box for XMP-only ISO files.
	bool moovFound = moovIgnored;

	for ( boxPos = 0; boxPos < fileSize; boxPos = boxNext ) {

		if ( checkAbort && abortProc(abortArg) ) {
			XMP_Throw ( "MPEG4_MetaHandler::CacheFileData - User abort", kXMPErr_UserAbort );
		}


		boxNext = ISOMedia::GetBoxInfo ( fileRef, boxPos, fileSize, &currBox );

		if ( (! moovFound) && (currBox.boxType == ISOMedia::k_moov) ) {
			
			XMP_Uns64 fullMoovSize = currBox.headerSize + currBox.contentSize;
			if ( fullMoovSize > moovBoxSizeLimit ) {	// From here on we know 32-bit offsets are safe.
				XMP_Throw ( "Oversize 'moov' box", kXMPErr_EnforceFailure );
			}
			
			this->moovMgr.fullSubtree.assign ( (XMP_Uns32)fullMoovSize, 0 );
			LFA_Seek ( fileRef, boxPos, SEEK_SET );
			LFA_Read ( fileRef, &this->moovMgr.fullSubtree[0], (XMP_Uns32)fullMoovSize );

			this->moovBoxPos = boxPos;
			this->moovBoxSize = (XMP_Uns32)fullMoovSize;
			moovFound = true;
			if ( uuidFound ) break;	// Exit the loop when both are found.

		} else if ( (! uuidFound) && (currBox.boxType == ISOMedia::k_uuid) ) {

			if ( currBox.contentSize < 16 ) continue;
			
			XMP_Uns8 uuid [16];
			LFA_Read ( fileRef, uuid, 16, kLFA_RequireAll );
			if ( memcmp ( uuid, ISOMedia::k_xmpUUID, 16 ) != 0 ) continue;	// Check for the XMP GUID.
			
			XMP_Uns64 fullUuidSize = currBox.headerSize + currBox.contentSize;
			if ( fullUuidSize > moovBoxSizeLimit ) {	// From here on we know 32-bit offsets are safe.
				XMP_Throw ( "Oversize XMP 'uuid' box", kXMPErr_EnforceFailure );
			}

			this->packetInfo.offset = boxPos + currBox.headerSize + 16;	// The 16 is for the UUID.
			this->packetInfo.length = (XMP_Int32) (currBox.contentSize - 16);

			this->xmpPacket.assign ( this->packetInfo.length, ' ' );
			LFA_Read ( fileRef, (void*)this->xmpPacket.data(), this->packetInfo.length, kLFA_RequireAll );

			this->xmpBoxPos = boxPos;
			this->xmpBoxSize = (XMP_Uns32)fullUuidSize;
			uuidFound = true;
			if ( moovFound ) break;	// Exit the loop when both are found.

		}

	}
	
	if ( (! moovFound) && (! moovIgnored) ) XMP_Throw ( "No 'moov' box", kXMPErr_BadFileFormat );

}	// MPEG4_MetaHandler::CacheFileData

// =================================================================================================
// MPEG4_MetaHandler::ProcessXMP
// =============================

void MPEG4_MetaHandler::ProcessXMP()
{
	if ( this->processedXMP ) return;
	this->processedXMP = true;	// Make sure only called once.

	XMPFiles * parent = this->parent;
	XMP_OptionBits openFlags = parent->openFlags;

	bool xmpOnly = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenOnlyXMP );
	bool haveISOFile = (this->fileMode == MOOV_Manager::kFileIsNormalISO);
	
	// Process the cached XMP (from the 'uuid' box) if that is all we want and this is an ISO file.
	
	if ( xmpOnly & haveISOFile ) {

		this->containsXMP = this->havePreferredXMP = (this->packetInfo.length != 0);

		if ( this->containsXMP ) {
			FillPacketInfo ( this->xmpPacket, &this->packetInfo );
			this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() );
			this->xmpObj.DeleteProperty ( kXMP_NS_XMP, "NativeDigests" );	// No longer used.
		}

		return;

	}
	
	// Parse the cached 'moov' subtree, parse the preferred XMP.
	
	if ( this->moovMgr.fullSubtree.empty() ) XMP_Throw ( "No 'moov' box", kXMPErr_BadFileFormat );
	this->moovMgr.ParseMemoryTree ( this->fileMode );
	
	if ( (this->xmpBoxPos == 0) || (! haveISOFile) ) {

		// Look for the QuickTime moov/uuid/XMP_ box.
	
		MOOV_Manager::BoxInfo xmpInfo;
		MOOV_Manager::BoxRef  xmpRef = this->moovMgr.GetBox ( "moov/udta/XMP_", &xmpInfo );
		
		if ( (xmpRef != 0) && (xmpInfo.contentSize != 0) ) {
		
			this->xmpBoxPos = this->moovBoxPos + this->moovMgr.GetParsedOffset ( xmpRef );
			this->packetInfo.offset = this->xmpBoxPos + this->moovMgr.GetHeaderSize ( xmpRef );
			this->packetInfo.length = xmpInfo.contentSize;
			
			this->xmpPacket.assign ( (char*)xmpInfo.content, this->packetInfo.length );
			this->havePreferredXMP = (! haveISOFile);
		
		}

	}
	
	if ( this->xmpBoxPos != 0 ) {
		this->containsXMP = true;
		FillPacketInfo ( this->xmpPacket, &this->packetInfo );
		this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() );
		this->xmpObj.DeleteProperty ( kXMP_NS_XMP, "NativeDigests" );	// No longer used.
	}
	
	// Import the non-XMP items. Do the imports in reverse priority order, last import wins!
	
	MOOV_Manager::BoxInfo mvhdInfo;
	MOOV_Manager::BoxRef  mvhdRef = this->moovMgr.GetBox ( "moov/mvhd", &mvhdInfo );
	bool mvhdFound = ((mvhdRef != 0) && (mvhdInfo.contentSize != 0));
	
	MOOV_Manager::BoxInfo udtaInfo;
	MOOV_Manager::BoxRef  udtaRef = this->moovMgr.GetBox ( "moov/udta", &udtaInfo );
	std::vector<MOOV_Manager::BoxInfo> cprtBoxes;

	if ( udtaRef != 0 ) {
		for ( XMP_Uns32 i = 0; i < udtaInfo.childCount; ++i ) {
			MOOV_Manager::BoxInfo currInfo;
			MOOV_Manager::BoxRef  currRef = this->moovMgr.GetNthChild ( udtaRef, i, &currInfo );
			if ( currRef == 0 ) break;	// Sanity check, should not happen.
			if ( currInfo.boxType != ISOMedia::k_cprt ) continue;
			cprtBoxes.push_back ( currInfo );
		}
	}
	bool cprtFound = (! cprtBoxes.empty());

	bool tradQTFound = this->tradQTMgr.ParseCachedBoxes ( this->moovMgr );
	bool tmcdFound = this->ParseTimecodeTrack();

	if ( this->fileMode == MOOV_Manager::kFileIsNormalISO ) {

		if ( mvhdFound )   this->containsXMP |= ImportMVHDItems ( mvhdInfo, &this->xmpObj );
		if ( cprtFound )   this->containsXMP |= ImportISOCopyrights ( cprtBoxes, &this->xmpObj );
	} else {	// This is a QuickTime file, either traditional or modern.

		if ( mvhdFound )   this->containsXMP |= ImportMVHDItems ( mvhdInfo, &this->xmpObj );
		if ( cprtFound )   this->containsXMP |= ImportISOCopyrights ( cprtBoxes, &this->xmpObj );
		if ( tmcdFound | tradQTFound ) {
			// Some of the timecode items are in the .../udta/©... set but handled by ImportTimecodeItems.
			this->containsXMP |= ImportTimecodeItems ( this->tmcdInfo, this->tradQTMgr, &this->xmpObj );
		}

		this->containsXMP |= ImportCr8rItems ( this->moovMgr, &this->xmpObj );

	}

}	// MPEG4_MetaHandler::ProcessXMP

// =================================================================================================
// MPEG4_MetaHandler::ParseTimecodeTrack
// =====================================

bool MPEG4_MetaHandler::ParseTimecodeTrack()
{
	MOOV_Manager::BoxRef stblRef = FindTimecodeTrack ( this->moovMgr );
	if ( stblRef == 0 ) return false;
	
	// Find the .../stbl/stsd box and process the first table entry.
	
	MOOV_Manager::BoxInfo stsdInfo;
	MOOV_Manager::BoxRef  stsdRef;
	
	stsdRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_stsd, &stsdInfo );
	if ( stsdRef == 0 ) return false;
	if ( stsdInfo.contentSize < (8 + sizeof ( MOOV_Manager::Content_stsd_entry )) ) return false;
	if ( GetUns32BE ( stsdInfo.content + 4 ) == 0 ) return false;	// Make sure the entry count is non-zero.
	
	const MOOV_Manager::Content_stsd_entry * stsdRawEntry = (MOOV_Manager::Content_stsd_entry*) (stsdInfo.content + 8);

	XMP_Uns32 stsdEntrySize = GetUns32BE ( &stsdRawEntry->entrySize );
	if ( stsdEntrySize > (stsdInfo.contentSize - 4) ) stsdEntrySize = stsdInfo.contentSize - 4;
	if ( stsdEntrySize < sizeof ( MOOV_Manager::Content_stsd_entry ) ) return false;

	XMP_Uns32 stsdEntryFormat = GetUns32BE ( &stsdRawEntry->format );
	if ( stsdEntryFormat != ISOMedia::k_tmcd ) return false;
	
	this->tmcdInfo.timeScale = GetUns32BE ( &stsdRawEntry->timeScale );
	this->tmcdInfo.frameDuration = GetUns32BE ( &stsdRawEntry->frameDuration );
	
	XMP_Uns32 flags = GetUns32BE ( &stsdRawEntry->flags );
	this->tmcdInfo.isDropFrame = flags & 0x1;
	
	// Look for a trailing 'name' box on the first stsd table entry.
	
	XMP_Uns32 stsdTrailerSize = stsdEntrySize - sizeof ( MOOV_Manager::Content_stsd_entry );
	if ( stsdTrailerSize > 8 ) {	// Room for a non-empty 'name' box?

		const XMP_Uns8 * trailerStart = stsdInfo.content + 8 + sizeof ( MOOV_Manager::Content_stsd_entry );
		const XMP_Uns8 * trailerLimit = trailerStart + stsdTrailerSize;
		const XMP_Uns8 * trailerPos;
		const XMP_Uns8 * trailerNext;
		ISOMedia::BoxInfo trailerInfo;
		
		for ( trailerPos = trailerStart; trailerPos < trailerLimit; trailerPos = trailerNext ) {

			trailerNext = ISOMedia::GetBoxInfo ( trailerPos, trailerLimit, &trailerInfo );

			if ( trailerInfo.boxType == ISOMedia::k_name ) {
			
				this->tmcdInfo.nameOffset = (XMP_Uns32) (trailerPos - stsdInfo.content);

				if ( trailerInfo.contentSize > 4 ) {

					XMP_Uns16 textLen = GetUns16BE ( trailerPos + trailerInfo.headerSize );
					this->tmcdInfo.macLang = GetUns16BE ( trailerPos + trailerInfo.headerSize + 2 );

					if ( trailerInfo.contentSize >= (XMP_Uns64)(textLen + 4) ) {
						const char * textPtr = (char*) (trailerPos + trailerInfo.headerSize + 4);
						this->tmcdInfo.macName.assign ( textPtr, textLen );
					}

				}

				break;	// Done after finding the first 'name' box.

			}

		}

	}
	
	// Find the timecode sample.

	XMP_Uns64 sampleOffset = 0;
	MOOV_Manager::BoxInfo tempInfo;
	MOOV_Manager::BoxRef  tempRef;
	
	tempRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_stsc, &tempInfo );
	if ( tempRef == 0 ) return false;
	if ( tempInfo.contentSize < (8 + sizeof ( MOOV_Manager::Content_stsc_entry )) ) return false;
	if ( GetUns32BE ( tempInfo.content + 4 ) == 0 ) return false;	// Make sure the entry count is non-zero.
	
	XMP_Uns32 firstChunkNumber = GetUns32BE ( tempInfo.content + 8 );	// Want first field of first entry.
	
	tempRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_stco, &tempInfo );
	
	if ( tempRef != 0 ) {
	
		if ( tempInfo.contentSize < (8 + 4) ) return false;
		XMP_Uns32 stcoCount = GetUns32BE ( tempInfo.content + 4 );
		if ( stcoCount < firstChunkNumber ) return false;
		XMP_Uns32 * stcoPtr = (XMP_Uns32*) (tempInfo.content + 8);
		sampleOffset = GetUns32BE ( &stcoPtr[firstChunkNumber-1] );	// ! Chunk number is 1-based.
	
	} else {

		tempRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_co64, &tempInfo );
		if ( (tempRef == 0) || (tempInfo.contentSize < (8 + 8)) ) return false;
		XMP_Uns32 co64Count = GetUns32BE ( tempInfo.content + 4 );
		if ( co64Count < firstChunkNumber ) return false;
		XMP_Uns64 * co64Ptr = (XMP_Uns64*) (tempInfo.content + 8);
		sampleOffset = GetUns64BE ( &co64Ptr[firstChunkNumber-1] );	// ! Chunk number is 1-based.

	}
	
	if ( sampleOffset != 0 ) {
	
		// Read the timecode sample. Need to reopen the file if the XMPFile was open for read-only,
		// normally all I/O is done within CacheFileData.

		bool openForRead = XMP_OptionIsSet ( this->parent->openFlags, kXMPFiles_OpenForRead);
		
		LFA_FileRef fileRef = this->parent->fileRef;
		if ( openForRead ) {
			XMP_Assert ( fileRef == 0 );
			fileRef = LFA_Open ( this->parent->filePath.c_str(), 'r' );
		}

		if ( fileRef != 0 ) {	// The reopen might have failed.
			LFA_Seek ( fileRef, sampleOffset, SEEK_SET );
			LFA_Read ( fileRef, &this->tmcdInfo.timecodeSample, 4, kLFA_RequireAll );
			this->tmcdInfo.timecodeSample = MakeUns32BE ( this->tmcdInfo.timecodeSample );
			if ( openForRead ) LFA_Close ( fileRef );
		}

	}

	// Finally update this->tmcdInfo to remember (for update) that there is an OK timecode track.

	this->tmcdInfo.stsdBoxFound = true;
	this->tmcdInfo.sampleOffset = sampleOffset;
	return true;

}	// MPEG4_MetaHandler::ParseTimecodeTrack

// =================================================================================================
// MPEG4_MetaHandler::UpdateTopLevelBox
// ====================================

void MPEG4_MetaHandler::UpdateTopLevelBox ( XMP_Uns64 oldOffset, XMP_Uns32 oldSize,
											const XMP_Uns8 * newBox, XMP_Uns32 newSize )
{
	if ( (oldSize == 0) && (newSize == 0) ) return;	// Sanity check, should not happen.

	LFA_FileRef fileRef = this->parent->fileRef;
	XMP_Uns64 oldFileSize = LFA_Measure ( fileRef );

	XMP_AbortProc abortProc = this->parent->abortProc;
	void *        abortArg  = this->parent->abortArg;
	
	if ( newSize == oldSize ) {
	
		// Trivial case, update the existing box in-place.
		LFA_Seek ( fileRef, oldOffset, SEEK_SET );
		LFA_Write ( fileRef, newBox, oldSize );
		
	} else if ( (oldOffset + oldSize) == oldFileSize ) {
	
		// The old box was at the end, write the new and truncate the file if necessary.
		LFA_Seek ( fileRef, oldOffset, SEEK_SET );
		LFA_Write ( fileRef, newBox, newSize );
		LFA_Truncate ( fileRef, (oldOffset + newSize) );	// Does nothing if new size is bigger.
	
	} else if ( (newSize < oldSize) && ((oldSize - newSize) >= 8) ) {
	
		// The new size is smaller and there is enough room to create a free box.
		LFA_Seek ( fileRef, oldOffset, SEEK_SET );
		LFA_Write ( fileRef, newBox, newSize );
		WipeBoxFree ( fileRef, (oldOffset + newSize), (oldSize - newSize) );
	
	} else {
	
		// Look for a trailing free box with enough space. If not found, consider any free space.
		// If still not found, append the new box and make the old one free.
	
		ISOMedia::BoxInfo nextBoxInfo;
		(void) ISOMedia::GetBoxInfo ( fileRef, (oldOffset + oldSize), oldFileSize, &nextBoxInfo, true /* throw errors */ );
		
		XMP_Uns64 totalRoom = oldSize + nextBoxInfo.headerSize + nextBoxInfo.contentSize;
		
		bool nextIsFree = (nextBoxInfo.boxType == ISOMedia::k_free) || (nextBoxInfo.boxType == ISOMedia::k_skip);
		bool haveEnoughRoom = (newSize == totalRoom) ||
							  ( (newSize < totalRoom) && ((totalRoom - newSize) >= 8) );

		if ( nextIsFree & haveEnoughRoom ) {

			LFA_Seek ( fileRef, oldOffset, SEEK_SET );
			LFA_Write ( fileRef, newBox, newSize );

			if ( newSize < totalRoom ) {
				// Don't wipe, at most 7 old bytes left, it will be covered by the free header.
				WriteBoxHeader ( fileRef, ISOMedia::k_free, (totalRoom - newSize) );
			}

		} else {

			// Create a list of all top level free space, including the old space as free. Use the
			// earliest space that fits. If none, append.
			
			FreeSpaceList spaceList;
			CreateFreeSpaceList ( fileRef, oldFileSize, oldOffset, oldSize, &spaceList );
			
			size_t freeSlot, limit;
			for ( freeSlot = 0, limit = spaceList.size(); freeSlot < limit; ++freeSlot ) {
				XMP_Uns64 freeSize = spaceList[freeSlot].size;
				if ( (newSize == freeSize) || ( (newSize < freeSize) && ((freeSize - newSize) >= 8) ) ) break;
			}
			
			if ( freeSlot == spaceList.size() ) {
			
				// No available free space, append the new box.
				CheckFinalBox ( fileRef, abortProc, abortArg );
				LFA_Seek ( fileRef, 0, SEEK_END );
				LFA_Write ( fileRef, newBox, newSize );
				WipeBoxFree ( fileRef, oldOffset, oldSize );
			
			} else {
			
				// Use the available free space. Wipe non-overlapping parts of the old box. The old
				// box is either included in the new space, or is fully disjoint.
				
				SpaceInfo & newSpace = spaceList[freeSlot];
				
				bool oldIsDisjoint = ((oldOffset + oldSize) <= newSpace.offset) ||		// Old is in front.
									 ((newSpace.offset + newSpace.size) <= oldOffset);	// Old is behind.
				
				XMP_Assert ( (newSize == newSpace.size) ||
							 ( (newSize < newSpace.size) && ((newSpace.size - newSize) >= 8) ) );
				
				XMP_Assert ( oldIsDisjoint ||
							 ( (newSpace.offset <= oldOffset) &&
							   ((oldOffset + oldSize) <= (newSpace.offset + newSpace.size)) ) /* old is included */ );

				XMP_Uns64 newFreeOffset = newSpace.offset + newSize;
				XMP_Uns64 newFreeSize   = newSpace.size - newSize;

				LFA_Seek ( fileRef, newSpace.offset, SEEK_SET );
				LFA_Write ( fileRef, newBox, newSize );
				
				if ( newFreeSize > 0 ) WriteBoxHeader ( fileRef, ISOMedia::k_free, newFreeSize );
				
				if ( oldIsDisjoint ) {

					WipeBoxFree ( fileRef, oldOffset, oldSize );

				} else {

					// Clear the exposed portion of the old box.
					
					XMP_Uns64 zeroStart = newFreeOffset + 8;
					if ( newFreeSize > 0xFFFFFFFF ) zeroStart += 8;
					if ( oldOffset > zeroStart ) zeroStart = oldOffset;
					XMP_Uns64 zeroEnd = newFreeOffset + newFreeSize;
					if ( (oldOffset + oldSize) < zeroEnd ) zeroEnd = oldOffset + oldSize;
					
					if ( zeroStart < zeroEnd ) {	// The new box might cover the old.
						XMP_Assert ( (zeroEnd - zeroStart) <= (XMP_Uns64)oldSize );
						XMP_Uns32 zeroSize = (XMP_Uns32) (zeroEnd - zeroStart);
						LFA_Seek ( fileRef, zeroStart, SEEK_SET );
						for ( XMP_Uns32 ioCount = sizeof ( kZeroes ); zeroSize > 0; zeroSize -= ioCount ) {
							if ( ioCount > zeroSize ) ioCount = zeroSize;
							LFA_Write ( fileRef, &kZeroes[0], ioCount );
						}
					}

				}
			
			}

		}
		
	}

}	// MPEG4_MetaHandler::UpdateTopLevelBox

// =================================================================================================
// MPEG4_MetaHandler::UpdateFile
// =============================
//
// Revamp notes:
// The 'moov' subtree and possibly the XMP 'uuid' box get updated. Compose the new copy of each and
// see if it fits in existing space, incorporating adjacent 'free' boxes if necessary. If that won't
// work, look for a sufficient 'free' box anywhere in the file. As a last resort, append the new copy.
// Assume no location sensitive data within 'moov', i.e. no offsets into it. This lets it be moved
// and its children freely rearranged.

void MPEG4_MetaHandler::UpdateFile ( bool doSafeUpdate )
{
	if ( ! this->needsUpdate ) {	// If needsUpdate is set then at least the XMP changed.
		return;
	}

	this->needsUpdate = false;	// Make sure only called once.
	XMP_Assert ( ! doSafeUpdate );	// This should only be called for "unsafe" updates.

	XMP_AbortProc abortProc  = this->parent->abortProc;
	void *        abortArg   = this->parent->abortArg;
	const bool    checkAbort = (abortProc != 0);

	LFA_FileRef fileRef  = this->parent->fileRef;
	XMP_Uns64   fileSize = LFA_Measure ( fileRef );

	bool haveISOFile = (this->fileMode == MOOV_Manager::kFileIsNormalISO);

	// Update the 'moov' subtree with exports from the XMP, but not the XMP itself (for QT files).
	
	ExportMVHDItems ( this->xmpObj, &this->moovMgr );
	ExportISOCopyrights ( this->xmpObj, &this->moovMgr );
	ExportQuickTimeItems ( this->xmpObj, &this->tradQTMgr, &this->moovMgr );
	ExportTimecodeItems ( this->xmpObj, &this->tmcdInfo, &this->tradQTMgr, &this->moovMgr );
	
	if ( ! haveISOFile ) ExportCr8rItems ( this->xmpObj, &this->moovMgr );
	
	// Try to update the XMP in-place if that is all that changed, or if it is in a preferred 'uuid' box.
	// The XMP has already been serialized by common code to the appropriate length. Otherwise, update
	// the 'moov'/'udta'/'XMP_' box in the MOOV_Manager, or the 'uuid' XMP box in the file.

	bool useUuidXMP = (this->fileMode == MOOV_Manager::kFileIsNormalISO);
	
	if ( (this->xmpPacket.size() == (size_t)this->packetInfo.length) &&
		  ( (useUuidXMP & this->havePreferredXMP) || (! this->moovMgr.IsChanged()) ) ) {

		// Update the existing XMP in-place.
		LFA_Seek ( fileRef, this->packetInfo.offset, SEEK_SET );
		LFA_Write ( fileRef, this->xmpPacket.c_str(), (XMP_Int32)this->xmpPacket.size() );

	} else if ( ! useUuidXMP ) {
	
		// Don't leave an old uuid XMP around (if we know about it).
		if ( (! havePreferredXMP) && (this->xmpBoxSize != 0) ) {
			WipeBoxFree ( fileRef, this->xmpBoxPos, this->xmpBoxSize );
		}
		
		// The udta form of XMP has just the XMP packet.
		this->moovMgr.SetBox ( "moov/udta/XMP_", this->xmpPacket.c_str(), (XMP_Uns32)this->xmpPacket.size() );
	
	} else {

		// Don't leave an old 'moov'/'udta'/'XMP_' box around.
		MOOV_Manager::BoxRef udtaRef = this->moovMgr.GetBox ( "moov/udta", 0 );
		if ( udtaRef != 0 ) this->moovMgr.DeleteTypeChild ( udtaRef, ISOMedia::k_XMP_ );
		
		// The uuid form of XMP has the 16-byte UUID in front of the XMP packet. Form the complete
		// box (including size/type header) for UpdateTopLevelBox.
		RawDataBlock uuidBox;
		XMP_Uns32 uuidSize = 4+4 + 16 + (XMP_Uns32)this->xmpPacket.size();
		uuidBox.assign ( uuidSize, 0 );
		PutUns32BE ( uuidSize, &uuidBox[0] );
		PutUns32BE ( ISOMedia::k_uuid, &uuidBox[4] );
		memcpy ( &uuidBox[8], ISOMedia::k_xmpUUID, 16 );
		memcpy ( &uuidBox[24], this->xmpPacket.c_str(), this->xmpPacket.size() );
		this->UpdateTopLevelBox ( this->xmpBoxPos, this->xmpBoxSize, &uuidBox[0], uuidSize );

	}
	
	// Update the 'moov' subtree if necessary, and finally update the timecode sample.
	
	if ( this->moovMgr.IsChanged() ) {
		this->moovMgr.UpdateMemoryTree();
		this->UpdateTopLevelBox ( moovBoxPos, moovBoxSize, &this->moovMgr.fullSubtree[0], (XMP_Uns32)this->moovMgr.fullSubtree.size() );
	}
	
	if ( this->tmcdInfo.sampleOffset != 0 ) {
		LFA_Seek ( fileRef, this->tmcdInfo.sampleOffset, SEEK_SET );
		XMP_Uns32 sample = MakeUns32BE ( this->tmcdInfo.timecodeSample );
		LFA_Write ( fileRef, &sample, 4 );
	}

}	// MPEG4_MetaHandler::UpdateFile

// =================================================================================================
// MPEG4_MetaHandler::WriteFile
// ============================
//
// Since the XMP and legacy is probably a miniscule part of the entire file, and since we can't
// change the offset of most of the boxes, just copy the entire source file to the dest file, then
// do an in-place update to the destination file.

void MPEG4_MetaHandler::WriteFile ( LFA_FileRef sourceRef, const std::string & sourcePath )
{
	XMP_Assert ( this->needsUpdate );

	LFA_FileRef destRef = this->parent->fileRef;

	LFA_Seek ( sourceRef, 0, SEEK_SET );
	LFA_Seek ( destRef, 0, SEEK_SET );
	LFA_Copy ( sourceRef, destRef, LFA_Measure ( sourceRef ),
			   this->parent->abortProc, this->parent->abortArg );

	this->UpdateFile ( false );

}	// MPEG4_MetaHandler::WriteFile

// =================================================================================================
