/*===========================================================================
  Copyright (C) 2008-2011 by the Okapi Framework contributors
-----------------------------------------------------------------------------
  This library is free software; you can redistribute it and/or modify it 
  under the terms of the GNU Lesser General Public License as published by 
  the Free Software Foundation; either version 2.1 of the License, or (at 
  your option) any later version.

  This library is distributed in the hope that it will be useful, but 
  WITHOUT ANY WARRANTY; without even the implied warranty of 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License 
  along with this library; if not, write to the Free Software Foundation, 
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

  See also the full LGPL text here: http://www.gnu.org/copyleft/lesser.html
===========================================================================*/

package net.sf.okapi.common.skeleton;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Stack;
import java.util.logging.Logger;

import net.sf.okapi.common.Event;
import net.sf.okapi.common.IParameters;
import net.sf.okapi.common.IResource;
import net.sf.okapi.common.ISkeleton;
import net.sf.okapi.common.annotation.AltTranslation;
import net.sf.okapi.common.annotation.AltTranslationsAnnotation;
import net.sf.okapi.common.encoder.EncoderManager;
import net.sf.okapi.common.filterwriter.ILayerProvider;
import net.sf.okapi.common.LocaleId;
import net.sf.okapi.common.resource.Code;
import net.sf.okapi.common.resource.DocumentPart;
import net.sf.okapi.common.resource.Ending;
import net.sf.okapi.common.resource.INameable;
import net.sf.okapi.common.resource.IReferenceable;
import net.sf.okapi.common.resource.Property;
import net.sf.okapi.common.resource.Segment;
import net.sf.okapi.common.resource.StartDocument;
import net.sf.okapi.common.resource.StartGroup;
import net.sf.okapi.common.resource.StartSubDocument;
import net.sf.okapi.common.resource.TextContainer;
import net.sf.okapi.common.resource.TextFragment;
import net.sf.okapi.common.resource.TextPart;
import net.sf.okapi.common.resource.ITextUnit;

/**
 * Implements ISkeletonWriter for the GenericSkeleton skeleton. 
 */
public class GenericSkeletonWriter implements ISkeletonWriter {

	public static final String ALLOWEMPTYOUTPUTTARGET = "allowEmptyOutputTarget";
	
	protected LocaleId inputLoc;
	protected LocaleId outputLoc;
	protected ILayerProvider layer;
	protected EncoderManager encoderManager;
	protected Stack<StorageList> storageStack;
	protected boolean isMultilingual;
	// A few formats may require to allow empty target in output.
	// They must be multilingual and not used with layer with this option
	protected boolean allowEmptyOutputTarget = false;
	
	private final Logger logger = Logger.getLogger(getClass().getName());

	private LinkedHashMap<String, Referent> referents;
	protected String outputEncoding;
	private int referentCopies = 1; // Number of copies to have for the referents (min=1)

//	private boolean segmentReferents = false;
	
	protected IReferenceable getReference (String id) {
		if ( referents == null ) return null;
		Referent ref = referents.get(id);
		if ( ref == null ) return null;
		// Remove the object found from the list
		if ( (--ref.count) == 0 ) {
			referents.remove(id);
		}
		return ref.ref;
	}

	@Override
	public void close () {
		if ( referents != null ) {
			referents.clear();
			referents = null;
		}
		if ( storageStack != null ) {
			storageStack.clear();
			storageStack = null;
		}
	}
	
	/**
	 * Sets the number of copies to keep for a referent. By default one copy is kept
	 * and discarded after it is referenced. Some layout may need to refer to the referent
	 * more than once, for example when they output both source and target.
	 * @param referentCopies the number of copies to hold (must be at least 1).
	 */
	public void setReferentCopies (int referentCopies) {
		if ( referentCopies < 1 ) this.referentCopies = 1; 
		else this.referentCopies = referentCopies;
	}

	@Override
	public String processStartDocument (LocaleId outputLocale,
		String outputEncoding,
		ILayerProvider layer,
		EncoderManager encoderManager,
		StartDocument resource)
	{
		referents = new LinkedHashMap<String, Referent>();
		storageStack = new Stack<StorageList>();

		this.inputLoc = resource.getLocale();
		this.outputLoc = outputLocale;
		this.encoderManager = encoderManager;
		this.outputEncoding = outputEncoding;
		this.layer = layer;
		isMultilingual = resource.isMultilingual();
		IParameters prm = resource.getFilterParameters();
		if ( this.encoderManager != null ) {
			this.encoderManager.setDefaultOptions(prm, outputEncoding,
				resource.getLineBreak());
			this.encoderManager.updateEncoder(resource.getMimeType());
		}
		// By default do not allow empty target in output
		allowEmptyOutputTarget = false;
		if ( prm != null ) {
			allowEmptyOutputTarget = prm.getBoolean(ALLOWEMPTYOUTPUTTARGET);
		}
		
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}

	@Override
	public String processEndDocument (Ending resource) {
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}

	@Override
	public String processStartSubDocument (StartSubDocument resource) {
		if ( storageStack.size() > 0 ) {
			storageStack.peek().add(resource);
			return "";
		}
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}

	@Override
	public String processEndSubDocument (Ending resource) {
		if ( storageStack.size() > 0 ) {
			storageStack.peek().add(resource);
			return "";
		}
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}
	
	@Override
	public String processStartGroup (StartGroup resource) {
		if ( resource.isReferent() ) {
			StorageList sl = new StorageList(resource);
			referents.put(sl.getId(), new Referent(sl, referentCopies));
			storageStack.push(sl);
			return "";
		}
		if ( storageStack.size() > 0 ) {
			StorageList sl = new StorageList(resource);
			storageStack.peek().add(sl);
			storageStack.push(sl);
			return "";
		}
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}
	
	@Override
	public String processEndGroup (Ending resource) {
		if ( storageStack.size() > 0 ) {
			storageStack.peek().add(resource);
			storageStack.pop();
			return "";
		}
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}
	
	@Override
	public String processTextUnit (ITextUnit resource) {
		if ( resource.isReferent() ) {
			referents.put(resource.getId(), new Referent(resource, referentCopies));
			return "";
		}
		if ( storageStack.size() > 0 ) {
			storageStack.peek().add(resource);
			return "";
		}
		return getString(resource, outputLoc, 1);
	}

	@Override
	public String processDocumentPart (DocumentPart resource) {
		if ( resource.isReferent() ) {
			referents.put(resource.getId(), new Referent(resource, referentCopies));
			return "";
		}
		if ( storageStack.size() > 0 ) {
			storageStack.peek().add(resource);
			return "";
		}
		return getString((GenericSkeleton)resource.getSkeleton(), 1);
	}
	
	protected String getString (ISkeleton skeleton,
		int context)
	{
		if ( skeleton == null ) return "";
		StringBuilder tmp = new StringBuilder();
		for ( GenericSkeletonPart part : ((GenericSkeleton)skeleton).getParts() ) {
			tmp.append(getString(part, context));
		}
		return tmp.toString();
	}
	
	protected String getString (GenericSkeletonPart part,
		int context)
	{
		// If it is not a reference marker, just use the data
		if ( !part.data.toString().startsWith(TextFragment.REFMARKER_START) ) {
			if ( layer == null ) {
				return part.data.toString();
			}
			else {
				return layer.encode(part.data.toString(), context);
			}
		}
		// Get the reference info
		Object[] marker = TextFragment.getRefMarker(part.data);
		// Check for problem
		if ( marker == null ) {
			return "-ERR:INVALID-REF-MARKER-";
		}
		String propName = (String)marker[3];

		// If we have a property name: It's a reference to a property of 
		// the resource holding this skeleton
		if ( propName != null ) { // Reference to the content of the referent
			if (Segment.REF_MARKER.equals(propName)) {
				String segId = (String) marker[0];
				ITextUnit tu = (ITextUnit)part.getParent();
				LocaleId locId = part.getLocale();
				TextContainer tc = null;
				
				if ( locId == null ) { // Source
					tc = tu.getSource();
				}
				else { // Target
					tc = tu.getTarget(locId);
				}
				Segment seg = null;
				if ( tc != null ) {
					seg = tc.getSegments().get(segId);
				}
				if (seg == null) {
					logger.warning(String.format("Segment reference '%s' not found.", (String)marker[0]));
					return "-ERR:INVALID-SEGMENT-REF-";
				}
				
				return getContent(seg.getContent(), locId, context);
			}
			else
				return getString((INameable)part.parent, propName, part.locId, context);
		}

		// Set the locToUse and the contextToUse parameters
		// If locToUse==null: it's source, so use output locale for monolingual
		LocaleId locToUse = (part.locId==null) ? outputLoc : part.locId;
		int contextToUse = context;
		if ( isMultilingual ) {
			locToUse = part.locId;
			// If locToUse==null: it's source, so not text in multilingual
			contextToUse = (locToUse==null) ? 0 : context;
		}
		
		// If a parent if set, it's a reference to the content of the resource
		// holding this skeleton. And it's always a TextUnit
		if ( part.parent != null ) {
			if ( part.parent instanceof ITextUnit ) {
				return getContent((ITextUnit)part.parent, locToUse, contextToUse);
			}
			else {
				throw new RuntimeException("The self-reference to this skeleton part must be a text-unit.");
			}
		}
		
		// Else this is a true reference to a referent
		IReferenceable ref = getReference((String)marker[0]);
		if ( ref == null ) {
			logger.warning(String.format("Reference '%s' not found.", (String)marker[0]));
			return "-ERR:REF-NOT-FOUND-";
		}
		if ( ref instanceof ITextUnit ) {
			return getString((ITextUnit)ref, locToUse, contextToUse); //TODO: Test locToUse
		}
		if ( ref instanceof GenericSkeletonPart ) {
			return getString((GenericSkeletonPart)ref, contextToUse);
		}
		if ( ref instanceof StorageList ) { // == StartGroup
			return getString((StorageList)ref, locToUse, contextToUse); //TODO: Test locToUse
		}
		// Else: DocumentPart, StartDocument, StartSubDocument 
		return getString((GenericSkeleton)((IResource)ref).getSkeleton(), context);
	}

	protected String getString (INameable ref,
		String propName,
		LocaleId locToUse,
		int context)
	{
		if ( ref == null ) {
			logger.warning(String.format("Null reference for '%s'.", propName));
			return "-ERR:NULL-REF-";
		}
		if ( propName != null ) {
			return getPropertyValue((INameable)ref, propName, locToUse, context);
		}
		if ( ref instanceof ITextUnit ) {
			return getString((ITextUnit)ref, locToUse, context);
		}
		if ( ref instanceof DocumentPart ) {
			return getString((GenericSkeleton)((IResource)ref).getSkeleton(), context);
		}
		if ( ref instanceof StorageList ) {
			return getString((StorageList)ref, locToUse, context);
		}
		logger.warning(String.format("Invalid reference type for '%s'.", propName));
		return "-ERR:INVALID-REFTYPE-";
	}

	/**
	 * Gets the skeleton and the original content of a given text unit.
	 * @param tu The text unit to process.
	 * @param locToUse locale to output. Use null for the source, or a LocaleId
	 * object for the target locales.
	 * @param context Context flag: 0=text, 1=skeleton, 2=in-line.
	 * @return The string representation of the text unit. 
	 */
	protected String getString (ITextUnit tu,
		LocaleId locToUse,
		int context)
	{
		GenericSkeleton skel = (GenericSkeleton)tu.getSkeleton();
		if ( skel == null ) { // No skeleton
			return getContent(tu, locToUse, context);
		}
		// Else: process the skeleton parts, one of them should
		// refer to the text-unit content itself
		StringBuilder tmp = new StringBuilder();
		for ( GenericSkeletonPart part : skel.getParts() ) {
			tmp.append(getString(part, context));
		}
		return tmp.toString();
	}

	/**
	 * Gets the original content of a given text unit.
	 * @param tu The text unit to process.
	 * @param locToUse locale to output. Use null for the source, or the locale
	 * for the target locales.
	 * @param context Context flag: 0=text, 1=skeleton, 2=inline.
	 * @return The string representation of the text unit content.
	 */
	protected String getContent (ITextUnit tu,
		LocaleId locToUse,
		int context)
	{
		// Update the encoder from the TU's MIME type
		if ( encoderManager != null ) {
			encoderManager.updateEncoder(tu.getMimeType());
		}
		
		// Get the right text container
		TextContainer srcCont = tu.getSource();
		TextContainer trgCont = null;
		if ( locToUse != null ) { // Expects a target output
			trgCont = tu.getTarget(locToUse);
			// If we do not have target
			// or if the target is empty (regardless the source)
			if (( trgCont == null ) || trgCont.isEmpty() ) {
				// If there is no target available
				if ( allowEmptyOutputTarget ) {
					// If empty targets are allowed and we don't have one: create a temporary one
					if ( trgCont == null ) {
						trgCont = tu.createTarget(locToUse, false, IResource.CREATE_EMPTY);
					}
				}
				else { // Fall back to the source
					trgCont = srcCont;
				}
			}
		}
		else { // Use the source
			// Set trgCont to it because that's the one driving the output
			trgCont = srcCont;
		}
		// Now trgCont is either the available target or the source (fall-back case)

		if ( !tu.isTranslatable() ) {
			context = 0; // Keep skeleton context
		}
		
		if ( srcCont.hasBeenSegmented() || !srcCont.contentIsOneSegment()
			|| ( trgCont.getAnnotation(AltTranslationsAnnotation.class) != null ))
		{
			return getSegmentedText(srcCont, trgCont, locToUse, context, tu.isReferent());
		}
		
		// Else: We have only one segment
		// Use trgCont, even if locToUse == null because then it's the source
		TextFragment tf = trgCont.getFirstContent();

		// Apply the layer if there is one
		if ( layer == null ) {
			return getContent(tf, locToUse, context);
		}
		else {
			switch ( context ) {
			case 1:
				return layer.endCode()
					+ getContent(tf, locToUse, 0)
					+ layer.startCode();
			case 2:
				return layer.endInline()
					+ getContent(tf, locToUse, 0)
					+ layer.startInline();
			default:
				return getContent(tf, locToUse, context);
			}
		}
	}


	private String getSegmentedText (TextContainer srcCont,
		TextContainer trgCont,
		LocaleId locToUse,
		int context,
		boolean isReferent)
	{
		StringBuilder tmp = new StringBuilder();

		// Get the alternate-translations if available
		AltTranslationsAnnotation atAnn = null;
//		atAnn = trgCont.getAnnotation(AltTranslationsAnnotation.class);
		
		// The output is driven by the target, not the source, so the interstices parts
		// are the ones of the target, no the one of the source
		for ( TextPart part : trgCont ) {
			if ( part.isSegment() ) {
				Segment trgSeg = (Segment)part;
				TextFragment trgFrag = trgSeg.text;

				// Compute the leverage score
				int lev = 0;
				AltTranslation at = null;
				atAnn = trgSeg.getAnnotation(AltTranslationsAnnotation.class);
				if ( atAnn != null ) {
					at = atAnn.getFirst();
					if ( at != null ) {
						lev = at.getScore();
					}
				}
				
				// Fall-back on the source if needed
				Segment srcSeg = srcCont.getSegments().get(trgSeg.id);
				if ( srcSeg == null ) {
					// A target segment without a corresponding source: give warning
					logger.warning(String.format("No source segment found for target segment id='%s':\n\"%s\".",
						trgSeg.id, trgFrag.toText()));
				}
				else {
					if ( trgFrag.isEmpty() && !srcSeg.text.isEmpty() ) {
						trgFrag = srcSeg.text;
						lev = 0; // Nothing leverage (target was not copied apparently)
					}
				}

				// Write the segment (note: srcSeg can be null)
				if ( layer == null ) {
					// If no layer: just write the target
					tmp.append(getContent(trgFrag, locToUse, context));
				}
				else { // If layer: write the bilingual entry
					switch ( context ) {
					case 1:
						tmp.append(layer.endCode()
							+ layer.startSegment()
							+ ((srcSeg==null) ? "" : getContent(srcSeg.text, locToUse, 0))
							+ layer.midSegment(lev)
							+ getContent(trgFrag, locToUse, 0)
							+ layer.endSegment()
							+ layer.startCode());
						break;
					case 2:
						tmp.append(layer.endInline()
							+ layer.startSegment()
							+ ((srcSeg==null) ? "" : getContent(srcSeg.text, locToUse, 0))
							+ layer.midSegment(lev)
							+ getContent(trgFrag, locToUse, 0)
							+ layer.endSegment()
							+ layer.startInline());
						break;
					default:
						tmp.append(layer.startSegment()
							+ ((srcSeg==null) ? "" : getContent(srcSeg.text, locToUse, 0))
							+ layer.midSegment(lev)
							+ getContent(trgFrag, locToUse, 0)
							+ layer.endSegment());
						break;
					}
				}
			}
			else { // Normal text fragment
				// Target fragment is used
				tmp.append(getContent(part.text, locToUse, context));
			}
		}

		return tmp.toString();
	}

	// This method assumes bi-lingual pairs are 1-1 and in the same order
//	private String getSegmentedText_OLD (TextContainer srcCont,
//		TextContainer trgCont,
//		LocaleId locToUse,
//		int context,
//		boolean isReferent)
//	{
//		StringBuilder tmp = new StringBuilder();
//
//		// Get the scores if they are available
//		ScoresAnnotation scores = null;
//		if ( trgCont != null ) {
//			scores = trgCont.getAnnotation(ScoresAnnotation.class);
//		}
//		
//		// The output is driven by the target, not the source, so the interstices parts
//		// are the ones of the target, no the one of the source
//		int scoreIndex = -1;
//		for ( TextPart part : trgCont ) {
//			if ( part.isSegment() ) {
//				scoreIndex++;
//				int lev = (( scores != null ) ? scores.getScore(scoreIndex) : 0 );
//				Segment trgSeg = (Segment)part;
//				Segment srcSeg = srcCont.getSegments().get(trgSeg.id);
//				if ( srcSeg == null ) {
//					// A target segment without a corresponding source: give warning
//					logger.warning(String.format("No source segment found for target segment id='%s':\n\"%s\".",
//						trgSeg.id, trgSeg.text.toText()));
//				}
//
//				// Write the segment (note: srcSeg can be null)
//				if ( layer == null ) {
//					// If no layer: just write the target
//					tmp.append(getContent(trgSeg.text, locToUse, context));
//				}
//				else { // If layer: write the bilingual entry
//					switch ( context ) {
//					case 1:
//						tmp.append(layer.endCode()
//							+ layer.startSegment()
//							+ ((srcSeg==null) ? "" : getContent(srcSeg.text, locToUse, 0))
//							+ layer.midSegment(lev)
//							+ getContent(trgSeg.text, locToUse, 0)
//							+ layer.endSegment()
//							+ layer.startCode());
//						break;
//					case 2:
//						tmp.append(layer.endInline()
//							+ layer.startSegment()
//							+ ((srcSeg==null) ? "" : getContent(srcSeg.text, locToUse, 0))
//							+ layer.midSegment(lev)
//							+ getContent(trgSeg.text, locToUse, 0)
//							+ layer.endSegment()
//							+ layer.startInline());
//						break;
//					default:
//						tmp.append(layer.startSegment()
//							+ ((srcSeg==null) ? "" : getContent(srcSeg.text, locToUse, 0))
//							+ layer.midSegment(lev)
//							+ getContent(trgSeg.text, locToUse, 0)
//							+ layer.endSegment());
//						break;
//					}
//				}
//			}
//			else { // Normal text fragment
//				// Target fragment is used
//				tmp.append(getContent(part.text, locToUse, context));
//			}
//		}
//
//		return tmp.toString();
//	}

//	/**
//	 * Gets the original content of a given text unit.
//	 * @param tu The text unit to process.
//	 * @param locToUse locale to output. Use null for the source, or the locale
//	 * for the target locales.
//	 * @param context Context flag: 0=text, 1=skeleton, 2=inline.
//	 * @return The string representation of the text unit content.
//	 */
//	protected String getContent (TextUnit tu,
//		LocaleId locToUse,
//		int context) // protected for OpenXML
//	{
//		// Update the encoder from the TU's MIME type
//		if ( encoderManager != null ) {
//			encoderManager.updateEncoder(tu.getMimeType());
//		}
//		
//		// Get the right text container
//		TextContainer srcCont = tu.getSource();
//		TextContainer trgCont = null;
//		if ( locToUse != null ) {
//			if ( (trgCont = tu.getTarget(locToUse)) == null ) {
//				if ( !srcCont.isSegmented() ) {
//					// Fall back to source, except when the source is segmented
//					trgCont = tu.getSource();
//				}
//			}
//		}
//		// Now trgCont is null only if we have segments and no target is available
//		// Otherwise trgCont is either the available target or the source (fall-back case)
//
//		if ( !tu.isTranslatable() ) {
//			context = 0; // Keep skeleton context
//		}
//		// Check for segmentation
//		if ( srcCont.isSegmented() ) {
//			// Special case of segmented entry: source + target
//			return getSegmentedText(tu.getSource(), trgCont, locToUse, context, tu.isReferent());
//		}
//		else { // Normal case: use the calculated target
//			TextContainer cont;
//			if ( locToUse == null ) cont = srcCont;
//			else cont = trgCont;
//			
//			// Apply the layer if there is one
//			if ( layer == null ) {
//				return getContent(cont, locToUse, context);
//			}
//			else {
//				switch ( context ) {
//				case 1:
//					return layer.endCode()
//						+ getContent(cont, locToUse, 0)
//						+ layer.startCode();
//				case 2:
//					return layer.endInline()
//						+ getContent(cont, locToUse, 0)
//						+ layer.startInline();
//				default:
//					return getContent(cont, locToUse, context);
//				}
//			}
//		}
//	}
	
//	private String getSegmentedText (TextContainer srcCont,
//		TextContainer trgCont,
//		LocaleId locToUse,
//		int context,
//		boolean isReferent)
//	{
//		StringBuilder tmp = new StringBuilder();
//		List<Segment> srcSegs = srcCont.getSegments();
//		List<Segment> trgSegs = null;
//		ScoresAnnotation scores = null;
//		if ( trgCont != null ) {
//			trgSegs = trgCont.getSegments();
//			scores = trgCont.getAnnotation(ScoresAnnotation.class);
//		}
//		
//		boolean markSegments = ( segmentReferents || !isReferent );
//		
//		String text = srcCont.getCodedText();
//		Code code;
//		char ch;
//		for ( int i=0; i<text.length(); i++ ) {
//			ch = text.charAt(i);
//			switch ( ch ) {
//			case TextFragment.MARKER_OPENING:
//			case TextFragment.MARKER_CLOSING:
//			case TextFragment.MARKER_ISOLATED:
//				//TODO: Handle codes outside the segments!!!
//				code = srcCont.getCode(text.charAt(++i));
//				tmp.append(expandCodeContent(code, locToUse, context));
//				break;
//			case TextFragment.MARKER_SEGMENT:
//				code = srcCont.getCode(text.charAt(++i));
//				int n = Integer.valueOf(code.getData());
//				// Check segment source/target
//				TextFragment trgFrag = null;
//				int lev = 0;
//				if (( trgSegs != null ) && ( n < trgSegs.size() )) {
//					trgFrag = trgSegs.get(n).text;
//					if ( scores != null ) lev = scores.getScore(n);
//				}
//				if ( trgFrag == null ) { // No target available: use the source
//					trgFrag = srcSegs.get(n).text;
//				}
//				// Write it
//				if ( layer == null ) {
////TODO: deal with not-in segment leading text
////TODO: deal with not-in-segment codes
//					// Get the inter-segment characters at the end of the segment
//					// So derived writers can treat all chars in getContent()
//					// i currently points to the index of the segment marker
//					int j; // Move forward until we found a marker or the end of the text
//					for ( j=1; i+j<text.length(); j++ ) {
//						if ( TextFragment.isMarker(text.charAt(i+j)) ) {
//							break;
//						}
//					} // Now j-1 should be the number of characters to add
//					if ( j > 1 ) {
//						trgFrag = trgFrag.clone(); // Make sure we don't change the original
//						trgFrag.append(text.substring(i+1, i+j));
//						i += (j-1); // Move the pointer at the last char we put in the segment
//					}
//					// Now get the content for the segment
//					tmp.append(getContent(trgFrag, locToUse, context));
//				}
//				else {
//					switch ( context ) {
//					case 1:
//						tmp.append(layer.endCode()
//							+ layer.startSegment()
//							+ getContent(srcSegs.get(n).text, null, 0)
//							+ layer.midSegment(lev)
//							+ ((trgFrag==null) ? "" : getContent(trgFrag, locToUse, 0))
//							+ layer.endSegment()
//							+ layer.startCode());
//						break;
//					case 2:
//						tmp.append(layer.endInline()
//							+ layer.startSegment()
//							+ getContent(srcSegs.get(n).text, null, 0)
//							+ layer.midSegment(lev)
//							+ ((trgFrag==null) ? "" : getContent(trgFrag, locToUse, 0))
//							+ layer.endSegment()
//							+ layer.startInline());
//						break;
//					default:
//						tmp.append(layer.startSegment()
//							+ getContent(srcSegs.get(n).text, null, context)
//							+ layer.midSegment(lev)
//							+ ((trgFrag==null) ? "" : getContent(trgFrag, locToUse, context))
//							+ layer.endSegment());
//						break;
//					}
//				}
//				break;
//			default:
//				if ( Character.isHighSurrogate(ch) ) {
//					int cp = text.codePointAt(i);
//					i++; // Skip low-surrogate
//					if ( encoderManager == null ) {
//						if ( layer == null ) {
//							tmp.append(new String(Character.toChars(cp)));
//						}
//						else {
//							tmp.append(layer.encode(cp, context));
//						}
//					}
//					else {
//						if ( layer == null ) {
//							tmp.append(encoderManager.encode(cp, context));
//						}
//						else {
//							tmp.append(layer.encode(
//								encoderManager.encode(cp, context),
//								context));
//						}
//					}
//				}
//				else { // Non-supplemental case
//					if ( encoderManager == null ) {
//						if ( layer == null ) {
//							tmp.append(ch);
//						}
//						else {
//							tmp.append(layer.encode(ch, context));
//						}
//					}
//					else {
//						if ( layer == null ) {
//							tmp.append(encoderManager.encode(ch, context));
//						}
//						else {
//							tmp.append(layer.encode(
//								encoderManager.encode(ch, context),
//								context));
//						}
//					}
//				}
//				break;
//			}
//		}
//		return tmp.toString();
//	}

	/**
	 * Gets the original content of a TextFragment.
	 * @param tf the TextFragment to process.
	 * @param locToUse locale to output. Use null for the source, or the locale
	 * for the target locales. This is used for referenced content in inline codes.
	 * @param context Context flag: 0=text, 1=skeleton, 2=inline.
	 * @return The string representation of the text unit content.
	 */
	public String getContent (TextFragment tf,
		LocaleId locToUse,
		int context)
	{
		// Output simple text
		if ( !tf.hasCode() ) {
			if ( encoderManager == null ) {
				if ( layer == null ) {
					return tf.toText();
				}
				else {
					return layer.encode(tf.toText(), context);
				}
			}
			else {
				if ( layer == null ) {
					return encoderManager.encode(tf.toText(), context);
				}
				else {
					return layer.encode(
						encoderManager.encode(tf.toText(), context), context);
				}
			}
		}

		// Output text with in-line codes
		List<Code> codes = tf.getCodes();
		StringBuilder tmp = new StringBuilder();
		String text = tf.getCodedText();
		Code code;
		char ch;
		for ( int i=0; i<text.length(); i++ ) {
			ch = text.charAt(i);
			switch ( ch ) {
			case TextFragment.MARKER_OPENING:
				code = codes.get(TextFragment.toIndex(text.charAt(++i)));
				tmp.append(expandCodeContent(code, locToUse, context));
				break;
			case TextFragment.MARKER_CLOSING:
				code = codes.get(TextFragment.toIndex(text.charAt(++i)));
				tmp.append(expandCodeContent(code, locToUse, context));
				break;
			case TextFragment.MARKER_ISOLATED:
				code = codes.get(TextFragment.toIndex(text.charAt(++i)));
				tmp.append(expandCodeContent(code, locToUse, context));
				break;
			default:
				if ( Character.isHighSurrogate(ch) ) {
					int cp = text.codePointAt(i);
					i++; // Skip low-surrogate
					if ( encoderManager == null ) {
						if ( layer == null ) {
							tmp.append(new String(Character.toChars(cp)));
						}
						else {
							tmp.append(layer.encode(cp, context));
						}
					}
					else {
						if ( layer == null ) {
							tmp.append(encoderManager.encode(cp, context));
						}
						else {
							tmp.append(layer.encode(
								encoderManager.encode(cp, context),
								context));
						}
					}
				}
				else { // Non-supplemental case
					if ( encoderManager == null ) {
						if ( layer == null ) {
							tmp.append(ch);
						}
						else {
							tmp.append(layer.encode(ch, context));
						}
					}
					else {
						if ( layer == null ) {
							tmp.append(encoderManager.encode(ch, context));
						}
						else {
							tmp.append(layer.encode(
								encoderManager.encode(ch, context),
								context));
						}
					}
				}
				break;
			}
		}
		return tmp.toString();
	}
	
	protected String expandCodeContent (Code code,
		LocaleId locToUse,
		int context)
	{ // this needs to be protected, not private, for OpenXML
		String codeTmp = code.getOuterData();
		if ( layer != null ) {
			codeTmp = layer.startInline() 
				+ layer.encode(codeTmp, 2)
				+ layer.endInline();
		}
		if ( !code.hasReference() ) {
			return codeTmp;
		}
		// Else: look for place-holders
		StringBuilder tmp = new StringBuilder(codeTmp);
		Object[] marker = null;
		while ( (marker = TextFragment.getRefMarker(tmp)) != null ) {
			int start = (Integer)marker[1];
			int end = (Integer)marker[2];
			String propName = (String)marker[3];
			IReferenceable ref = getReference((String)marker[0]);
			if ( ref == null ) {
				logger.warning(String.format("Reference '%s' not found.", (String)marker[0]));				
				tmp.replace(start, end, "-ERR:REF-NOT-FOUND-");
			}
			else if ( propName != null ) {
				tmp.replace(start, end,
					getPropertyValue((INameable)ref, propName, locToUse, 2));
			}
			else if ( ref instanceof ITextUnit ) {
				tmp.replace(start, end, getString((ITextUnit)ref, locToUse, 2));
			}
			else if ( ref instanceof GenericSkeletonPart ) {
				tmp.replace(start, end, getString((GenericSkeletonPart)ref, 2));
			}
			else if ( ref instanceof StorageList ) { // == StartGroup
				tmp.replace(start, end, getString((StorageList)ref, locToUse, 2));
			}
			else { // DocumentPart, StartDocument, StartSubDocument 
				tmp.replace(start, end, getString((GenericSkeleton)((IResource)ref).getSkeleton(), 2));
			}
		}
		return tmp.toString();
	}
	
	protected String getString (StorageList list,
		LocaleId locToUse,
		int context)
	{
		StringBuilder tmp = new StringBuilder();
		// Treat the skeleton of this list
		tmp.append(getString((GenericSkeleton)list.getSkeleton(), context));		
		// Then treat the list itself
		for ( IResource res : list ) {
			if ( res instanceof ITextUnit ) {
				tmp.append(getString((ITextUnit)res, locToUse, context));
			}
			else if ( res instanceof StorageList ) {
				tmp.append(getString((StorageList)res, locToUse, context));
			}
			else if ( res instanceof DocumentPart ) {
				tmp.append(getString((GenericSkeleton)res.getSkeleton(), context));
			}
			else if ( res instanceof Ending ) {
				tmp.append(getString((GenericSkeleton)res.getSkeleton(), context));
			}
		}
		return tmp.toString();
	}
	
	protected String getPropertyValue (INameable resource,
		String name,
		LocaleId locToUse,
		int context)
	{
		// Update the encoder from the TU's MIME type
		if ( encoderManager != null ) {
			encoderManager.updateEncoder(resource.getMimeType());
		}

		// Get the value based on the output locale
		Property prop;
		if ( locToUse == null ) { // Use the source
			prop = resource.getSourceProperty(name);
		}
		else if ( locToUse.equals(LocaleId.EMPTY) ) { // Use the resource-level properties
			prop = resource.getProperty(name);
		}
		else { // Use the given target locale if possible
			if ( resource.hasTargetProperty(locToUse, name) ) {
				prop = resource.getTargetProperty(locToUse, name);
			}
			else { // Fall back to source if there is no target
				prop = resource.getSourceProperty(name);				
			}
		}
		// Check the property we got
		if ( prop == null ) {
			logger.warning(String.format("Property '%s' not found.", name));
			return "-ERR:PROP-NOT-FOUND-";
		}
		// Else process the value
		String value = prop.getValue();
		if ( value == null ) {
			logger.warning(String.format("Property value for '%s' is null.", name));
			return "-ERR:PROP-VALUE-NULL-";
		}
		
		// Else: We got the property value
		// Check if it needs to be auto-modified
		if ( Property.LANGUAGE.equals(name) ) {
			// If it is the input locale, we change it with the output locale
			//TODO: Do we need an option to be region-insensitive? (en==en-gb)
			LocaleId locId = LocaleId.fromString(value);
			if ( locId.equals(inputLoc) ) {
				value = outputLoc.toString();
			}
		}
		else if ( Property.ENCODING.equals(name) ) {
			value = outputEncoding;
		}
		// Return the native value if possible
		if ( encoderManager == null ) {
			if ( layer == null ) return value;
			else return layer.encode(value, context); //TODO: context correct??
		}
		else {
			if ( layer == null ) return encoderManager.toNative(name, value);
			else return layer.encode(encoderManager.toNative(name, value), context);
		}
	}
	
	public void addToReferents (Event event) { // for OpenXML, so referents can stay private
		IResource resource;
		if ( event != null ) {
			if ( referents == null ) {
				referents = new LinkedHashMap<String, Referent>();
				storageStack = new Stack<StorageList>();
			}
			resource = event.getResource();
			if ( resource != null ) {
				switch( event.getEventType() ) {
				case TEXT_UNIT:
					if ( ((ITextUnit)resource).isReferent() ) {
						referents.put(resource.getId(), new Referent((ITextUnit)resource, referentCopies));
					}
					break;
				case DOCUMENT_PART:
					if ( ((DocumentPart)resource).isReferent() ) {
						referents.put(resource.getId(), new Referent((DocumentPart)resource, referentCopies));
					}
					break;
				case START_GROUP:
					if ( ((StartGroup)resource).isReferent() ) {
						StorageList sl = new StorageList((StartGroup)resource);
						referents.put(sl.getId(), new Referent(sl, referentCopies));
					}
					break;
				default:
					break;
				}
			}
		}
	}
}
