/************************************************************************
 *
 *  I18n.java
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License version 2.1, as published by the Free Software Foundation.
 *
 *  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
 *
 *  Copyright: 2002-2007 by Henrik Just
 *
 *  All Rights Reserved.
 * 
 *  Version 0.5 (2007-07-24) 
 * 
 */

package writer2latex.latex.i18n;

/* These classes takes care of i18n in Writer2LaTeX.
   In LaTeX, i18n is a mixture of inputencodings, fontencodings and babel languages.
   I18n thus manages these, and in particular implements a Unicode->LaTeX
   translation that can handle different inputencodings and fontencodings.
   The translation is table driven, using symbols.xml (embedded in the jar)
   Various sections of symbols.xml handles different cases:
     * common symbols in various font encodings such as T1, T2A, LGR etc.
     * input encodings such as ISO-8859-1 (latin-1), ISO-8859-7 (latin/greek) etc.
     * additional symbol fonts such as wasysym, dingbats etc.
     * font-specific symbols, eg. for 8-bit fonts/private use area
*/

import java.io.*;
import java.util.Hashtable;
import java.util.Stack;
import java.util.Vector;
import java.util.Enumeration;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;

import writer2latex.util.CSVList;
import writer2latex.util.Config;
import writer2latex.office.*;
import writer2latex.latex.LaTeXDocumentPortion;
import writer2latex.latex.ConverterPalette;
import writer2latex.latex.util.BeforeAfter;

public class I18n {
    // **** Inputencodings ****
    public static final int ASCII = 0;
    public static final int LATIN1 = 1; // ISO Latin 1 (ISO-8859-1)
    public static final int LATIN2 = 2; // ISO Latin 1 (ISO-8859-1)
    public static final int ISO_8859_7 = 3; // ISO latin/greek
    public static final int CP1250 = 4; // Microsoft Windows Eastern European
    public static final int CP1251 = 5; // Microsoft Windows Cyrillic
    public static final int KOI8_R = 6; // Latin/russian
    public static final int UTF8 = 7; // UTF-8

    // Read an inputencoding from a string
    public static final int readInputenc(String sInputenc) {
        if ("ascii".equals(sInputenc)) return ASCII;
        else if ("latin1".equals(sInputenc)) return LATIN1;
        else if ("latin2".equals(sInputenc)) return LATIN2;
        else if ("iso-8859-7".equals(sInputenc)) return ISO_8859_7;
        else if ("cp1250".equals(sInputenc)) return CP1250;
        else if ("cp1251".equals(sInputenc)) return CP1251;
        else if ("koi8-r".equals(sInputenc)) return KOI8_R;
        else if ("utf8".equals(sInputenc)) return UTF8;
        else return ASCII; // unknown = ascii
    }

    // Return the LaTeX name of an inputencoding
    public static final String writeInputenc(int nInputenc) {
        switch (nInputenc) {
            case ASCII : return "ascii";
            case LATIN1 : return "latin1";
            case LATIN2 : return "latin2";
            case ISO_8859_7 : return "iso-8859-7";
            case CP1250 : return "cp1250";
            case CP1251 : return "cp1251";
            case KOI8_R : return "koi8-r";
            case UTF8 : return "utf8";
            default : return "???";
        }
    }
	
    // Return the java i18n name of an inputencoding
    public static final String writeJavaEncoding(int nInputenc) {
        switch (nInputenc) {
            case ASCII : return "ASCII";
            case LATIN1 : return "ISO8859_1";
            case LATIN2 : return "ISO8859_2";
            case ISO_8859_7 : return "ISO8859_7";
            case CP1250 : return "Cp1250";
            case CP1251 : return "Cp1251";
            case KOI8_R : return "KOI8_R";
            case UTF8 : return "UTF-8";
            default : return "???";
        }
    }
	
    // **** Fontencodings ****
    public static final int OT1_ENC = 1;
    public static final int T1_ENC = 2;
    public static final int T2A_ENC = 4;
    public static final int T3_ENC = 8;
    public static final int LGR_ENC = 16;
    public static final int ANY_ENC = 31;

    // read set of font encodings from a string
    public static final int readFontencs(String sFontencs) {
        sFontencs = sFontencs.toUpperCase();
        if ("ANY".equals(sFontencs)) return ANY_ENC;
        int nFontencs = 0;
        int nOT1 = sFontencs.indexOf("OT1");
        int nT1 = sFontencs.indexOf("T1");
        int nT2A = sFontencs.indexOf("T2A");
        int nT3 = sFontencs.indexOf("T3");
        int nLGR = sFontencs.indexOf("LGR");
        if (nOT1>=0) nFontencs+=OT1_ENC;
        if (nT1==0 || (nT1>0 && nT1!=nOT1+1)) nFontencs+=T1_ENC;
        if (nT2A>=0) nFontencs+=T2A_ENC;
        if (nT3>=0) nFontencs+=T3_ENC;
        if (nLGR>=0) nFontencs+=LGR_ENC;
        return nFontencs;
    }
	
    // return string representation of a single font encoding
    public static final String writeFontenc(int nFontenc) {
        switch (nFontenc) {
            case OT1_ENC: return "OT1";
            case T1_ENC: return "T1";
            case T2A_ENC: return "T2A";
            case T3_ENC: return "T3";
            case LGR_ENC: return "LGR";
        }
        return null;
    }
	
    // check that a given set of font encodings contains a specific font encoding
    public static final boolean supportsFontenc(int nFontencs, int nFontenc) {
        return (nFontencs & nFontenc) != 0;
    }

    // get one fontencoding from a set of fontencodings
    public static final int getFontenc(int nFontencs) {
        if (supportsFontenc(nFontencs,T1_ENC)) return T1_ENC;
        if (supportsFontenc(nFontencs,T2A_ENC)) return T2A_ENC;
        if (supportsFontenc(nFontencs,T3_ENC)) return T3_ENC;
        if (supportsFontenc(nFontencs,LGR_ENC)) return LGR_ENC;
        return 0;
    }
	
    // get the font encoding for a specific iso language
    public static final int getFontenc(String sLang) {
        // Greek uses "local greek" encoding
        if ("el".equals(sLang)) return LGR_ENC;
        // Russian, ukrainian, bulgarian and serbian uses T2A encoding
        else if ("ru".equals(sLang)) return T2A_ENC;
        else if ("uk".equals(sLang)) return T2A_ENC;
        else if ("bg".equals(sLang)) return T2A_ENC;
        else if ("sr".equals(sLang)) return T2A_ENC;
        // Other languages uses T1 encoding
        else return T1_ENC;
    }

    // return cs for a fontencoding
    public static final String getFontencCs(int nFontenc) {
        switch (nFontenc) {
            case T1_ENC: return "\\textlatin"; // requires babel
            case T2A_ENC: return "\\textcyrillic"; // requires babel with russian, bulgarian or ukrainian option
            case T3_ENC: return "\\textipa"; // requires tipa.sty
            case LGR_ENC: return "\\textgreek"; // requires babel with greek option
            default: return null;
        }
    }
	
    // **** Languages ****
	
    // Convert iso language to babel language
    // todo: include iso country
    // todo: support automatic choice of inputenc (see comments)
    public static final String getBabelLanguage(String sLanguage) {
        if ("en".equals(sLanguage)) return "english"; // latin1
        else if ("bg".equals(sLanguage)) return "bulgarian"; // cp1251?
        else if ("cs".equals(sLanguage)) return "czech"; // latin2
        else if ("da".equals(sLanguage)) return "danish"; // latin1
        else if ("de".equals(sLanguage)) return "ngerman"; // latin1
        else if ("el".equals(sLanguage)) return "greek"; // iso-8859-7
        else if ("es".equals(sLanguage)) return "spanish"; // latin1
        else if ("fi".equals(sLanguage)) return "finnish"; // latin1 (latin9?)
        else if ("fr".equals(sLanguage)) return "french"; // latin1 (latin9?)
        else if ("ga".equals(sLanguage)) return "irish" ; // latin1
        else if ("hr".equals(sLanguage)) return "croatian"; // latin2
        else if ("hu".equals(sLanguage)) return "magyar"; // latin2
        else if ("is".equals(sLanguage)) return "icelandic"; // latin1
        else if ("it".equals(sLanguage)) return "italian"; // latin1
        else if ("nl".equals(sLanguage)) return "dutch"; // latin1
        else if ("no".equals(sLanguage)) return "norsk"; // latin1
        else if ("pl".equals(sLanguage)) return "polish"; // latin2
        else if ("pt".equals(sLanguage)) return "portuges"; // latin1
        else if ("ro".equals(sLanguage)) return "romanian"; // latin2
        else if ("ru".equals(sLanguage)) return "russian"; // cp1251?
        else if ("sk".equals(sLanguage)) return "slovak"; // latin2
        else if ("sl".equals(sLanguage)) return "slovene"; // latin2
        else if ("sr".equals(sLanguage)) return "serbian"; // cp1251?
        else if ("sv".equals(sLanguage)) return "swedish"; // latin1
        else if ("tr".equals(sLanguage)) return "turkish";
        else if ("uk".equals(sLanguage)) return "ukrainian"; // cp1251?
        else return null; // unknown or unsupported language
    }
	
    // End of static part of I18n!
	 
    // Global variables:
    private Hashtable<String,UnicodeTable> tableSet; // all tables
    private UnicodeTable table; // currently active table (top of stack)
    private Stack<UnicodeTable> tableStack; // bruno; stack of active tables
    private UnicodeStringParser ucparser; // Unicode string parser
    //bruno private String sDefaultLang = null; // Default language to use
    private int nDefaultFontenc = 0; // Fontenc for the default language
    private boolean bAlwaysUseDefaultLang = false; // Ignore sLang parameter to convert()
    private boolean bT2A = false; // do we use cyrillic letters?
    private boolean bGreek = false; // do we use greek letters?
    private boolean bPolytonicGreek = false; // do we use polytonic greek letters?
    private boolean bGreekMath;
    private String sMajorityLanguage = null; // cache result from method
    private ReplacementTrie stringReplace;
    private Config config;
    private OfficeReader ofr;

    // Constructor; loads symbol table based on the configuration
    public I18n(OfficeReader ofr, Config config, ConverterPalette palette) {
        this.ofr = ofr;
        this.config = config;
        stringReplace = config.getStringReplace();
		
        
		
        String sSymbols="ascii"; // always load common symbols
        if (config.getInputencoding()!=ASCII) {
            sSymbols+="|"+writeInputenc(config.getInputencoding());
        }

        if (config.useWasysym()) sSymbols+="|wasysym";
        if (config.useBbding()) sSymbols+="|bbding";
        if (config.useIfsym()) sSymbols+="|ifsym";
        if (config.usePifont()) sSymbols+="|dingbats";
        if (config.useEurosym()) sSymbols+="|eurosym";
        if (config.useTipa()) sSymbols+="tipa";

        readSymbols(sSymbols);
		
        bGreekMath = config.greekMath();
		
        bAlwaysUseDefaultLang = !config.multilingual();

		
        ucparser = new UnicodeStringParser();
    }

    // Constructor; loads specific symbols
    public I18n(String sSymbols) {
        readSymbols(sSymbols);
    }
	
    public void appendDeclarations(LaTeXDocumentPortion pack, LaTeXDocumentPortion decl) {
        // inputenc TODO: Remove dirty, autogenerated
        if (config.getInputencoding()==I18n.UTF8){
            pack.append("\\usepackage[dirty,autogenerated]{ucs}").nl();
        }
        pack.append("\\usepackage[")
                .append(writeInputenc(config.getInputencoding()))
                .append("]{inputenc}").nl();

        // fontenc
        CSVList fontencs = new CSVList(',');
        if (bT2A) { fontencs.addValue("T2A"); }
        if (bGreek) { fontencs.addValue("LGR"); }
        if (config.useTipa()) { fontencs.addValue("T3"); }
        fontencs.addValue("T1");
        pack.append("\\usepackage[").append(fontencs.toString())
            .append("]{fontenc}").nl();

        // babel
        convertLanguageUsage(pack);
			
        if (config.useTipa()) {
            pack.append("\\usepackage[noenc]{tipa}").nl()
                .append("\\usepackage{tipx}").nl();
        }

        // Bbding: (Has to avoid some nameclashes.)
        if (config.useBbding()) {
            pack.append("\\usepackage{bbding}").nl()
                .append("\\let\\bbCross\\Cross\\let\\Cross\\undefined").nl()
                .append("\\let\\bbSquare\\Square\\let\\Square\\undefined").nl()
                .append("\\let\\bbTrianbleUp\\TriangleUp\\let\\TriangleUp\\undefined").nl()
                .append("\\let\\bbTrianlgeDown\\TriangleDown\\let\\TriangleDown\\undefined").nl();
        }
		
        if (config.useIfsym()) { pack.append("\\usepackage[geometry,weather,misc,clock]{ifsym}").nl(); }

        if (config.usePifont()) { pack.append("\\usepackage{pifont}").nl(); }

        if (config.useEurosym()) { pack.append("\\usepackage{eurosym}").nl(); }

        // wasysym must be loaded between amsmath and amsfonts!
        if (config.useWasysym()) { 
            pack.append("\\usepackage{amsmath,wasysym,amssymb,amsfonts,textcomp}").nl();
        }
        else {
            pack.append("\\usepackage{amsmath,amssymb,amsfonts,textcomp}").nl();
        }
		
        // \textcyrillic command - deleted as babel defines it!
        //if (bT2A) {
        //    pack.append("\\DeclareRobustCommand{\\textcyrillic}[1]{{\\fontencoding{T2A}\\selectfont\\def\\encodingdefault{T2A}#1}}").nl();
        //}
    }
	
    public void setDefaultLanguage(String sDefaultLang) {
        //bruno this.sDefaultLang = sDefaultLang;
        nDefaultFontenc = getFontenc(sDefaultLang);
    }
	
    /** <p>Apply language.</p>
     *  @param <code>style</code> the OOo style to read attributesfrom
     *  @param <code>bDecl</code> true if declaration form is required
     *  @param <code>bInherit</code> true if inherited properties should be used
     *  @param <code>ba</code> the <code>BeforeAfter</code> to add LaTeX code to.
     */
    public void applyLanguage(StyleWithProperties style, boolean bDecl, boolean bInherit, BeforeAfter ba) {
        if (bAlwaysUseDefaultLang) { return; }
        if (style==null) { return; }
        String sLang = getBabelLanguage(style.getProperty(XMLString.FO_LANGUAGE,bInherit));
        if (sLang==null) { return; }
        if (bDecl) {
            ba.add("\\selectlanguage{"+sLang+"}","");
            //ba.add("\\begin{otherlanguage}{"+sLang+"}","\\end{otherlanguage}");
        }
        else {
            ba.add("\\foreignlanguage{"+sLang+"}{","}");
        }
    }
	
    // Return the iso language used in most paragaph styles (in a well-structured
    // document this will be the default language)
    // TODO: Base on content rather than style (move to converter!)
    public String getMajorityLanguage() {
        Hashtable<String, Integer> langs = new Hashtable<String, Integer>();//bruno

        // Read the default language from the default paragraph style
        String sDefaultLang = null;
        StyleWithProperties style = ofr.getDefaultParStyle();
        if (style!=null) { 
            sDefaultLang = style.getProperty(XMLString.FO_LANGUAGE);
        }

        // Collect languages from paragraph styles
        Enumeration enumeration = ofr.getParStyles().getStylesEnumeration();
        while (enumeration.hasMoreElements()) {
            style = (StyleWithProperties) enumeration.nextElement();
            String sLang = style.getProperty(XMLString.FO_LANGUAGE);
            if (sLang==null) { sLang = sDefaultLang; }
            if (sLang!=null) {
                int nCount = 1;
                if (langs.containsKey(sLang)) {
                    nCount = ((Integer) langs.get(sLang)).intValue()+1;
                }
                langs.put(sLang,new Integer(nCount));
            }
        }
		
        // Find the most common language
        int nMaxCount = 0;
        String sMajorityLanguage = null;
        enumeration = langs.keys();
        while (enumeration.hasMoreElements()) {
            String sLang = (String) enumeration.nextElement();
            int nCount = ((Integer) langs.get(sLang)).intValue();
            if (nCount>nMaxCount) {
                nMaxCount = nCount;
                sMajorityLanguage = sLang;
            }
        }
        this.sMajorityLanguage = sMajorityLanguage;
        return sMajorityLanguage;        
    }

    private void convertLanguageUsage(LaTeXDocumentPortion ldp) {
        Vector<String> languages = new Vector<String>();//bruno
        String sDefaultLanguage = null;
        if (config.multilingual()) {
            // Collect languages from text styles
            Enumeration enumeration = ofr.getTextStyles().getStylesEnumeration();
            while (enumeration.hasMoreElements()) {
                StyleWithProperties style = (StyleWithProperties) enumeration.nextElement();
                String sLang = I18n.getBabelLanguage(style.getProperty(XMLString.FO_LANGUAGE));
                if (sLang!=null && !languages.contains(sLang)) { languages.add(sLang); }
            }
            // Collect languages from paragraph styles
            enumeration = ofr.getParStyles().getStylesEnumeration();
            while (enumeration.hasMoreElements()) {
                StyleWithProperties style = (StyleWithProperties) enumeration.nextElement();
                String sLang = I18n.getBabelLanguage(style.getProperty(XMLString.FO_LANGUAGE));
                if (sLang!=null && !languages.contains(sLang)) { languages.add(sLang); }
            }
            // Read the default language from the default paragraph style
            StyleWithProperties style = ofr.getDefaultParStyle();
            if (style!=null) { 
                sDefaultLanguage = I18n.getBabelLanguage(style.getProperty(XMLString.FO_LANGUAGE));
            }
        }
        else { // the most common language is the only language
            sDefaultLanguage = I18n.getBabelLanguage(sMajorityLanguage==null ?
                               getMajorityLanguage() : sMajorityLanguage); 
        }
        // If the document contains "anonymous" greek letters we need greek in any case:
        if (this.greek() && !languages.contains("greek")) languages.add("greek");
        // If the document contains "anonymous cyrillic letters we need one of the
        // languages russian, ukrainian or bulgarian
        if (this.cyrillic() && !(languages.contains("ukrainian") ||
            languages.contains("ukrainian") || languages.contains("ukrainian"))) {
            languages.add("russian");
        } 
        // Load babel with the used languages
        CSVList babelopt = new CSVList(",");		
        Enumeration langenum = languages.elements();
        while (langenum.hasMoreElements()) {
            String sLang = (String) langenum.nextElement();
            if (!sLang.equals(sDefaultLanguage)) {
                if ("greek".equals(sLang) && this.polytonicGreek()) {
                    sLang = "polutonikogreek";
                }
                babelopt.addValue(sLang);
            }
        }
        // The default language must be the last one
        if (sDefaultLanguage!=null) {
            if ("greek".equals(sDefaultLanguage) && this.polytonicGreek()) {
                babelopt.addValue("polutonikogreek");
            }
            else {
                babelopt.addValue(sDefaultLanguage);
            }
        }
        if (!babelopt.isEmpty()) {
            ldp.append("\\usepackage[")
               .append(babelopt.toString())
               .append("]{babel}").nl();
        }
    }
	
    // The parameter sSymbolsets should contain a sequence of all symbols sets to
    // include eg. "ascii|latin1|dingbats" to include the default symbols,
    // use latin1 inputencoding and support dingbats (pifont.sty).
    private void readSymbols(String sSymbols) {
        tableSet = new Hashtable<String,UnicodeTable>();
        UnicodeTableHandler handler=new UnicodeTableHandler(tableSet, sSymbols);
        SAXParserFactory factory=SAXParserFactory.newInstance();
        InputStream is = this.getClass().getResourceAsStream("symbols.xml");
        try {
            SAXParser saxParser=factory.newSAXParser();
            saxParser.parse(is,handler);
        }
        catch (Throwable t){
		    System.err.println("Oops - Unable to read symbols.xml");
            t.printStackTrace();
        }
        // put root table at top of stack
        tableStack = new Stack<UnicodeTable>();//bruno
        tableStack.push((UnicodeTable) tableSet.get("root"));
        table = (UnicodeTable) tableSet.get("root");
    }

    // Did we use cyrillic?
    public boolean cyrillic() { return bT2A; }
	
    // Did we use greek?
    public boolean greek() { return bGreek; }
	
    // Did we use polytonic greek?
    public boolean polytonicGreek() { return bPolytonicGreek; }

    // Outside greek text, greek letters may be rendered in math mode,
    // if the user requires that in the configuration.
    private boolean greekMath(char c, int nFontenc) {
        return bGreekMath && nFontenc!=LGR_ENC && table.getFontencs(c)==LGR_ENC;
    }
	
    // Set cyrillic and greek flags
    private void setFlags(char c, int nFontenc) {
        if ((c>='\u1F00') && (c<='\u1FFF')) bPolytonicGreek = true;
        if (nFontenc==LGR_ENC) bGreek = true;
        if (nFontenc==T2A_ENC) bT2A = true;
    }

    // Missing symbol
    private String notFound(char c,int nFontenc) {
        //String sErrorMsg = "[Warning: Missing symbol " + Integer.toHexString(c).toUpperCase() +"]";
        String sErrorMsg = "["+Integer.toHexString(c).toUpperCase() +"?]";		
        if (nFontenc==T1_ENC) return sErrorMsg;
        else return "\\textlatin{"+sErrorMsg+"}"; 
    }

    // convert a single math character
    private String convertMathChar(char c, int nFontenc) {
        if (table.hasMathChar(c)) {
            return table.getMathChar(c);
        }
        else if (table.hasTextChar(c)) { // use text mode as a fallback
            int nFontencs = table.getFontencs(c);
            if (supportsFontenc(nFontencs,nFontenc)) {
                // The text character is valid in the current font encoding
                setFlags(c,nFontenc);
                if (table.getCharType(c)==UnicodeCharacter.COMBINING) {
                    return "\\text{" + table.getTextChar(c) +"{}}";
                }
                else {
                    return "\\text{" + table.getTextChar(c) +"}";
                }
            }
            else {
                // The text character is valid in another font encoding
                int nFontenc1 = getFontenc(nFontencs);
                setFlags(c,nFontenc1);
                if (table.getCharType(c)==UnicodeCharacter.COMBINING) {
                    return "\\text{" + getFontencCs(nFontenc1) + "{" + table.getTextChar(c) +"{}}}";
                }
                else {
                    return "\\text{" + getFontencCs(nFontenc1) + "{" + table.getTextChar(c) +"}}";
                }
            }
        }
        else {
            return "\\text{" + notFound(c,nFontenc) + "}";
        }
    }
	
    // Convert a single character	
    public String convert(char c, boolean bMathMode, String sLang){
        int nFontenc = bAlwaysUseDefaultLang ? nDefaultFontenc : getFontenc(sLang);
        if (bMathMode) {
            return convertMathChar(c,nFontenc);
        }
        else if (greekMath(c,nFontenc) || (table.hasMathChar(c) && !table.hasTextChar(c))) {
            return "$" + convertMathChar(c,nFontenc) + "$";
        }
        else if (table.hasTextChar(c)) {
            int nFontencs = table.getFontencs(c);
            if (supportsFontenc(nFontencs,nFontenc)) {
                // The text character is valid in the current font encoding
                setFlags(c,nFontenc);
                if (table.getCharType(c)==UnicodeCharacter.COMBINING) {
                    return table.getTextChar(c)+"{}";
                }
                else {
                    return table.getTextChar(c);
                }
            }
            else {
                // The text character is valid in another font encoding
                int nFontenc1 = getFontenc(nFontencs);
                setFlags(c,nFontenc1);
                if (table.getCharType(c)==UnicodeCharacter.COMBINING) {
                    return getFontencCs(nFontenc1) + "{" + table.getTextChar(c) +"{}}";
                }
                else {
                    return getFontencCs(nFontenc1) + "{" + table.getTextChar(c) +"}";
                }
            }
        }
        else {
            return notFound(c,nFontenc);
        }
    }

    // Convert a string of characters
    public String convert(String s, boolean bMathMode, String sLang){
        StringBuffer buf=new StringBuffer();
        int nFontenc = bAlwaysUseDefaultLang ? nDefaultFontenc : getFontenc(sLang);
        int nLen = s.length();
        int i = 0;
        int nStart = i;
        while (i<nLen) {
            ReplacementTrieNode node = stringReplace.get(s,i,nLen);
            if (node!=null) {
                if (i>nStart) {
                    convert(s,nStart,i,bMathMode,sLang,buf,nFontenc);
                }
                boolean bOtherFontenc = !supportsFontenc(node.getFontencs(),nFontenc);
                if (bOtherFontenc) {
                    buf.append(getFontencCs(getFontenc(node.getFontencs()))).append("{");
                }
                buf.append(node.getLaTeXCode());
                if (bOtherFontenc) {
                    buf.append("}");
                }
                i += node.getInputLength();
                nStart = i;
            }
            else {
                i++;
            }
        }
        if (nStart<nLen) {
            convert(s,nStart,nLen,bMathMode,sLang,buf,nFontenc);
        }
        return buf.toString();
    }	
	
    private void convert(String s, int nStart, int nEnd, boolean bMathMode, String sLang, StringBuffer buf, int nFontenc) {
        int nCurFontenc = nFontenc;
        ucparser.reset(table,s,nStart,nEnd);
        boolean bProtectDashes = true;
        while (ucparser.next()) {
            char c = ucparser.getChar();
            if (bMathMode) {
                buf.append(convertMathChar(c,nFontenc));
            }
            else if (greekMath(c,nFontenc) || (table.hasMathChar(c) && !table.hasTextChar(c))) {
                buf.append("$").append(convertMathChar(c,nFontenc)).append("$");
                bProtectDashes = false;
            }
            else if (table.hasTextChar(c)) {
                int nFontencs = table.getFontencs(c);
                if (supportsFontenc(nFontencs,nCurFontenc)) {
                    // The text character is valid in the current font encoding
                    // Note: Change of font encoding is greedy - change?

                    // Prevent unwanted --- ligatures
                    if (table.isDashes(c)) {
                        if (bProtectDashes) { buf.append("{}"); }
                        bProtectDashes = true;
                    }
                    else {
                        bProtectDashes = false;
                    }

                    setFlags(c,nCurFontenc);
                    if (ucparser.hasCombiningChar()) {
                        char cc = ucparser.getCombiningChar();
                        if (supportsFontenc(table.getFontencs(cc),nCurFontenc)) {
                            buf.append(table.getTextChar(cc)).append("{")
                               .append(table.getTextChar(c)).append("}");
                        }
                        else { // ignore combining char if not valid in this font encoding
                            buf.append(table.getTextChar(c));
                        }
                    }
                    else {
                        buf.append(table.getTextChar(c));
                    }
                }
                else {
                    // The text character is valid in another font encoding
					
                    bProtectDashes = table.isDashes(c);

                    int nFontenc1 = getFontenc(nFontencs);
                    setFlags(c,nFontenc1);
                    if (nCurFontenc!=nFontenc) { // end "other font encoding"
                        buf.append("}");
                    }
                    if (nFontenc1!=nFontenc) { // start "other font encoding"
                        buf.append(getFontencCs(nFontenc1)).append("{");
                    }

                    if (ucparser.hasCombiningChar()) {
                        char cc = ucparser.getCombiningChar();
                        if (supportsFontenc(table.getFontencs(cc),nCurFontenc)) {
                            buf.append(table.getTextChar(cc)).append("{")
                               .append(table.getTextChar(c)).append("}");
                        }
                        else { // ignore combining char if not valid in this font encoding
                            buf.append(table.getTextChar(c));
                        }
                    }
                    else {
                        buf.append(table.getTextChar(c));
                    }
                    nCurFontenc = nFontenc1;
                }
            }
            else {
                buf.append(notFound(c,nCurFontenc));
            }
        }

        if (nCurFontenc!=nFontenc) { // end unfinished "other font encoding"
            buf.append("}");
        }

    }
	
    public void pushSpecialTable(String sName) {
        // If no name is specified we should keep the current table
        // Otherwise try to find the table, and use root if it's not available
        if (sName!=null) {
            table = (UnicodeTable) tableSet.get(sName);
            if (table==null) { table = (UnicodeTable) tableSet.get("root"); }
        }
        tableStack.push(table);
    }
	
    public void popSpecialTable() {
        tableStack.pop();
        table = (UnicodeTable) tableStack.peek();
    }

    public int getCharCount() { return table.getCharCount(); }

}
