package org.xdef.sys;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
import java.util.Properties;
import java.util.Set;
import org.xdef.XDConstants;
import static org.xdef.XDConstants.XDPROPERTY_MESSAGES;
import static org.xdef.XDConstants.XDPROPERTY_MSGLANGUAGE;
import org.xdef.msg.SYS;
import org.xdef.sys.RegisterReportTables.ReportTable;
import org.xdef.xml.KXmlUtils;

/** Provides managing of properties, languages and report tables. It exists the singleton instance of SManager.
 * You can get this instance by the static method {@link org.xdef.sys.SManager#getInstance()}.
 * <p>Note the SManager is used as manager of reports. Therefore in such multi thread applications which requires
 * well defined environment for the language, properties and/or message tables you have to synchronize by</p>
 * <pre><code><b>
 * synchronize(SManager.getSManager()) {
 * ...
 * }
 * </b></code></pre>
 *
 * @author Vaclav Trojan
 */
public final class SManager implements XDConstants {
    /** The unique instance of SManager. */
    private static final SManager MANAGER = new SManager();
    /** Length of array of report tables. */
    private int _tableHighIndex = -1;
    /** Increment of table length. */
    private final static int TABLE_INC = 16;
    /** Array of package names. */
    private String[] _packages = new String[] {"org.xdef.msg"};
    /** Sorted array of report tables (key is registered table ID). */
    private ReportTable[] _tables = new ReportTable[TABLE_INC];
    /** Properties. */
    private Properties _properties;
    /** Language information (the actual and the primary language). */
    private SLanguage _language;

    private static final class SLanguage {
        private final String _language;
        private final int _languageID;
        private final String _defaultLanguage;
        private final int _defaultLanguageID;

        SLanguage(final String language, final String primaryLanguage) {
            _language = language;
            _defaultLanguage = primaryLanguage;
            _languageID = ReportTable.getLanguageID(_language);
            _defaultLanguageID = ReportTable.getLanguageID(_defaultLanguage);
        }
        private String getLanguage() {return _language;}
        private int getID() {return _languageID;}
        private String getDefaultLanguage() {return _defaultLanguage;}
        private int getDefaultID() {return _defaultLanguageID;}
    }

    /** This constructor can be invoked only from this class .*/
    private SManager() {
        _properties = (Properties) System.getProperties().clone();
        String s = getProperty(_properties, XDPROPERTY_MSGLANGUAGE);
        s = (s != null)
            ? SUtils.getISO3Language(s) : SUtils.getISO3Language(System.getProperties().getProperty("user.language"));
        _language = new SLanguage(s, "eng");
    }

    /** Get value property or environment variable.
     * @param props Object with properties.
     * @param key name of property.
     * @return value property or environment variable.
     */
    public static final String getProperty(final Properties props, final String key) {
        String val = null;
        if (props != null) {
            String newKey = key.startsWith("xdef.") ? key.replace('.','_'): key;
            val = props.getProperty(key);
            if (key.equals(newKey)) {
                if (val == null) {
                    String oldKey = key.startsWith("xdef_") ? key.replace('_','.'): key;
                    val = props.getProperty(oldKey);
                    if (val != null) {
                        props.remove(key);
                        props.setProperty(newKey, val);
                    }
                }
            }
        }
        if (val == null || (val = val.trim()).isEmpty()) {
            val = System.getProperty(key);
        }
        return (val != null && (val=val.trim()).isEmpty()) ? null : val;
    }

    /** Get SManager property.
     * @param name property name.
     * @return string with property value ornull.
     */
    public static final String getProperty(final String name) {
        return getProperty(getInstance()._properties, name);
    }

    /** Set properties to the SManager .
     * @param properties the clone from this argument and set to SManager.
     */
    public static final void setProperties(Properties properties) {
        SManager sm = getInstance();
        synchronized (sm) {
            sm._properties = (Properties) properties.clone();
            String s = getProperty(sm._properties,XDPROPERTY_MSGLANGUAGE);
            if (s != null) {
                sm._language = new SLanguage(SUtils.getISO3Language(s), "eng");
            }
        }
    }

    /** Get SManager properties.
     * @return properties from the SManager.
     */
    public static final Properties getProperties() {return getInstance()._properties;}

    /** Set SManager property.
     * @param key property name.
     * @param value property value (may be null - then the property is removed).
     * @return string with original property value or null.
     */
    public static final String setProperty(final String key, final String value) {
        SManager sm = getInstance();
        String newKey = key.startsWith("xdef.") ? key.replace('.', '_') : key;
        synchronized (sm) {
            String result = getProperty(sm._properties, newKey);
            if (result == null) {
                result = getProperty(sm._properties, key);
            }
            sm._properties.remove(key);
            sm._properties.remove(newKey);
            if (value != null) {
                sm._properties.setProperty(newKey, value);
            }
            if (XDPROPERTY_MSGLANGUAGE.equals(newKey)) {
                String v = value;
                if (v == null) {
                    v = System.getProperties().getProperty("user.language");
                }
                sm._language = new SLanguage(SUtils.getISO3Language(v), sm._language.getDefaultLanguage());
            }
            return result;
        }
    }

    /** Set actual language and primary language for reports.
     * @param language language for reports.
     * @return the language object.
     */
    public static final Object setLanguage(final String language) {
        SManager sm = getInstance();
        synchronized (sm) {
            String lang = SUtils.getISO3Language(language);
            String plang = sm._language.getDefaultLanguage();
            if (!sm._language.getLanguage().equals(lang) || !sm._language.getDefaultLanguage().equals(plang)){
                sm._language = new SLanguage(lang, plang);
            }
            return sm._language;
        }
    }

    /** Get actual language for reports.
     * @return actual language for reports.
     */
    public static final String getLanguage() {return getInstance()._language.getLanguage();}

    /** Get default language.
     * @return default language.
     */
    public static final String getDefaultLanguage() {return getInstance()._language.getDefaultLanguage();}

    /** Add package with registered reports.
     * @param packageName new package to be added.
     */
    public static final void addReportPackage(final String packageName) {
        SManager sm = getInstance();
        synchronized (sm) {
            for (String s : sm._packages) {
                if (s.equals(packageName)) return; // already exists
            }
            String[] newPackages = new String[sm._packages.length + 1];
            System.arraycopy(sm._packages,0,newPackages,0,sm._packages.length);
            newPackages[sm._packages.length] = packageName;
            sm._packages = newPackages;
        }
    }

    /** Remove package with registered reports.
     * @param packageName package to be removed.
     */
    public static final void removeReportPackage(final String packageName) {
        SManager sm = getInstance();
        synchronized (sm) {
            String[] newPackages = new String[sm._packages.length - 1];
            int j = 0;
            for (String s : sm._packages) {
                if (j < sm._packages.length && !s.equals(packageName)) {
                    newPackages[j++] = s;
                }
            }
            if (j < sm._packages.length) {
                sm._packages = newPackages;
            }
        }
    }

    /** Add report table created from properties.
     * @param reportTable properties with reports.
     * @throws RuntimeException if an error occurs.
     */
    public static final void addReports(final Properties reportTable) {
        addReportTable(RegisterReportTables.genReportTable(reportTable));
    }

    /** Get ReportTable.
     * @param tableNameID name of report table.
     * @return report table.
     */
    final ReportTable getReportTable(final String tableNameID) {
        return findReportTable(RegisterReportTables.getTableID(tableNameID));
    }

    /** Get report table associated with given report. If the language is null, the primary langue table is searched.
     * @param reportID report ID.
     * @param language language code (ISO-639) or null.
     * @return report table or null.
     */
    final ReportTable getReportTable(final String reportID, final String language) {
        String langId = language == null ? getLanguage() : SUtils.getISO3Language(language);
        String prefix = ReportTable.getPrefixFromID(reportID);
        if (prefix == null) {
            return null;
        }
        return getReportTable(prefix + "_" + langId);
    }

    /** Remove all report tables from manager. */
    public static final void removeAllReportTables() {
        SManager sm = getInstance();
        synchronized (sm) {
            if (sm._tableHighIndex >= 0) {
                if (sm._tables.length > TABLE_INC) {
                    sm._tables = new ReportTable[TABLE_INC];
                } else {
                    for (int i = 0; i <= sm._tableHighIndex; i++) {
                        sm._tables[i] = null;
                    }
                }
                sm._tableHighIndex = -1;
            }
        }
    }

    /** Remove report tables with given prefix for all languages.
     * @param prefix Prefix of report tables.
     */
    public static final void removeReportTables(final String prefix) {
        SManager sm = getInstance();
        synchronized (sm) {
            String pfx = prefix + '_';
            for (int i = sm._tableHighIndex; i >= 0 ; i--) {
                String tableName = sm._tables[i].getTableName();
                if (tableName.startsWith(pfx)) {
                    sm.removeReportTable(tableName);
                }
            }
        }
    }

    /** Remove specified report table.
     * @param prefix prefix of messages in table.
     * @param language language of table.
     */
    public static final void removeReportTable(final String prefix, final String language) {
        SManager sm = getInstance();
        synchronized (sm) {
            sm.removeReportTable(prefix + "_" + language);
        }
    }

    /** Get array of langue identifiers of available languages for given report ID.
     * @param id the report ID
     * @return array of langue identifiers of available languages.
     */
    public static final String[] getAvailableLanguages(final String id) {
        Set<String> x = new TreeSet<>();
        String prefix = ReportTable.getPrefixFromID(id) + '_';
        SManager sm = getInstance();
        synchronized (sm) {
            for (int i = 0; i <= sm._tableHighIndex; i++) {
                String tableName = sm._tables[i].getTableName();
                if (tableName.startsWith(prefix)) {
                    x.add(tableName.substring(0,prefix.length() - 1));
                }
            }
        }
        String[] result = new String[x.size()];
        x.toArray(result);
        return result;
    }

    /** Get instance of SManager.
     * @return instance of SManager.
     */
    public static final SManager getInstance() {return MANAGER;}

    /** Modify given text with modification string (see description above {@link Report}).
     * @param text The original text.
     * @param modif The modification string.
     * @param language language id (ISO-639).
     * @return The modified text.
     */
    public static final String getModifiedText(final String text, final String modif, final String language) {
        int indx1, indx;
        if (text == null || (indx1 = text.indexOf("&{")) < 0) {
            return text;
        }
        indx = 0;
        String mod;
        //resolve report references in the modification string
        if ((mod = modif) != null && modif.length() > 0) {
            int i = mod.indexOf("&{#");
            while (i >= 0) {
                int j = mod.indexOf('}', i + 3);
                if (j < 0) {
                    break; //error
                }
                String id;
                String t = "";
                String m = null;
                int p = mod.indexOf('{', i + 3);
                if (p > 0 && p < j) {
                    id = mod.substring(i + 3, p).trim();
                    j = getParamaterEnd(mod, p);
                    if (j < 0) {
                        break; //error
                    }
                    t = mod.substring(p + 1, j++);
                    p = mod.indexOf('{', j);
                    j = mod.indexOf('}', j);
                    if (p > 0 && p < j) {
                        int q = p;
                        do {
                            if ((j = getParamaterEnd(mod, q)) < 0) {
                                break; //error
                            }
                            q = mod.indexOf('{', j);
                            j = mod.indexOf('}', j + 1);
                        } while (q > 0 && q < j);
                        if (j < 0) {
                            break; //error
                        }
                        m = mod.substring(p + 1, j - 1);
                    }
                } else {
                    id = mod.substring(i + 3, j).trim();
                }
                Report r = Report.text(id.trim(), t, m);
                mod = mod.substring(0, i) + r.toString(language)
                    + mod.substring(j + 1);
                i = mod.indexOf("&{#", i);
            }
        } else {
            mod = null;
        }
        StringBuilder sb = new StringBuilder();
        int len = text.length();
        while (true) {
            sb.append(text.substring(indx, indx1)); //part without parameter
            int keyEnd = text.indexOf('}', indx1 + 2);
            if (keyEnd < 0) {
                break; //error
            }
            String param = text.substring(indx1, keyEnd + 1); //whole param
            indx1 = keyEnd + 1;
            int paramPos;
            if (mod == null || (paramPos = mod.indexOf(param)) < 0) {
                //parameter was not found in modification string
                if (indx1 < len && text.charAt(indx1) == '{') {//follows '{'
                    indx1++; //yes, next char
                    int max = text.indexOf('}', indx1); //end of section
                    if (max < 0) {
                        break; //error
                    }
                    if (indx1 < max && text.charAt(indx1) == '&') {
                        //copy key
                        indx1++;
                        indx = text.indexOf('&', indx1); //next param
                        if (indx < 0) {
                            indx = max; //next param
                        }
                        if (indx > indx1 && indx <= max) {
                            sb.append(text.substring(indx1, indx));
                        }
                    }
                    indx1 = max;
                    if (max < len) {
                        indx1++;
                        if (indx1 < len && text.charAt(indx1) == '{') {
                            indx1++;
                            max = text.indexOf('}', indx1);
                            indx1 = max > 0 ? max + 1 : len;
                        }
                    }
                }
            } else { //parameter was found in modification string
                String postfix = null;
                if (indx1 < len && text.charAt(indx1) == '{') {
                    indx1++;
                    int max = text.indexOf('}', indx1);
                    if (max < 0) {
                        max = len;
                    }
                    if (indx1 < max && text.charAt(indx1) == '&') { //copy key
                        indx1++;
                        indx = text.indexOf('&', indx1);
                        indx1 = (indx >= indx1 && indx < max) ? indx + 1 : max;
                    }
                    sb.append(text.substring(indx1, max));
                    indx1 = max;
                    if (max < len) {
                        indx1++;
                        if (indx1 < len && text.charAt(indx1) == '{') {
                            indx1++;
                            max = text.indexOf('}', indx1);
                            if (max < 0) {
                                postfix = text.substring(indx1);
                                indx1 = len;
                            } else {
                                postfix = text.substring(indx1, max);
                                indx1 = max + 1;
                            }
                        }
                    }
                }
                paramPos += param.length();
                int paramPosEnd = mod.indexOf("&{", paramPos);
                if (paramPosEnd < 0) {
                    paramPosEnd = mod.length();
                }
                sb.append(mod.substring(paramPos, paramPosEnd));
                if (postfix != null) {
                    sb.append(postfix);
                }
            }
            indx = indx1;
            if ((indx1 = text.indexOf("&{", indx1)) < 0) {
                break;
            }
        }
        return sb.append(text.substring(indx)).toString();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Auxiliary methos called from this package.
    ////////////////////////////////////////////////////////////////////////////

    /** Get text of report from report table. All references to other reports are resolved if the argument
     * resolveReferences is true.
     * @param regID registered report ID.
     * @param language language code.
     * @param resolveReferences if true then all references to other reports are resolved.
     * @return text of report.
     */
    final String getReportText(final long regID, final String language, final boolean resolveReferences) {
        return getReportText(regID, language != null ? ReportTable.getLanguageID(language) : 0, resolveReferences);
    }

    /** Get text of report from report table. All references to other reports are resolved
     * if the argument resolveReferences is true.
     * @param id report ID.
     * @param language code of required language or null (then the actual setting language is used).
     * @param resolveReferences if true then all references to other reports are resolved.
     * @return text of report.
     */
    final String getReportText(final String id, final String language, final boolean resolveReferences) {
        if (id == null || id.trim().isEmpty()) {
            return null;
        }
        ReportTable table = getReportTable(id, language);
        if (table == null) {
            if ((table = getReportTable(id, getDefaultLanguage())) == null) {
                return null;
            }
        }
        String text = table.getReportText(id);
        if (text == null) {
             if (getDefaultLanguage().equals(table.getLanguage())) {
                 return null;
             }
            table = getReportTable(id, getDefaultLanguage());
            text = table == null ? null : table.getReportText(id);
        }
        String lang = language != null ? SUtils.getISO3Language(language) : getLanguage();
        return resolveReferences ? resolveReportReferences(text, lang) : text;
    }

    /** Find available report table for the the actual language or for the primary language. If the actual language
     * table is not available then the table for the primary language is searched. If none of those
     * was found then return null.
     * @param regID registered report ID.
     * @return report table or null.
     */
    final ReportTable getReportTable(final long regID) {
        return findReportTable(regID & ReportTable.REGTABIDMASK | _language.getID());
    }

    /** Get modified text of localized registered report (see description of this class above).
     * @param regID registered report ID.
     * @param modification The modification string.
     * @param language language id (ISO-639).
     * @return The text of localized report in given language or null.
     */
    final String getLocalizedText(final long regID, final String modification, final String language) {
        long tabid = regID & ReportTable.REGTABIDMASK | (language != null ? ReportTable.getLanguageID(language) : 0);
        ReportTable table = findReportTable(tabid);
        if (table == null) {
            table = findReportTable(regID & ReportTable.REGTABIDMASK | _language.getDefaultID());
            if (table == null) {
                return null;
            }
        }
        return getLocalizedText(table.getReportID(regID), null, modification, table.getLanguage());
    }

    /** Get modified text of localized report (see description of this class above) .
     * @param id report id.
     * @param text model text of report.
     * @param modification modification string.
     * @param language language id (ISO-639).
     * @return localized text of report in given language or null.
     */
    final String getLocalizedText(final String id, final String text, final String modification, final String language){
        String mod = modification;
        String txt;
        if (id != null && id.trim().length() > 0) {
            txt = getReportText(id, language, true);
            if (txt == null) {
                txt = text;
            }
        } else {
            txt = resolveReportReferences(text, language);
        }
        if (txt == null || !txt.contains("&{")) {
            return txt;
        }
        if (mod != null) {
            int i = mod.indexOf("&{&&");
             // process reports included in the modifier and
            while (i >= 0) {
                int j =  mod.indexOf("&}", i + 4);
                StringBuffer sb = (j < 0) ? new StringBuffer(mod.substring(i + 4))
                    : new StringBuffer(mod.substring(i + 4, j));
                // resolve entities for characters '<', '>', '"', ''', '&'
                SUtils.modifyStringBuffer(sb,"&lt;","<");
                SUtils.modifyStringBuffer(sb,"&gt;",">");
                SUtils.modifyStringBuffer(sb,"&quot;","\"");
                SUtils.modifyStringBuffer(sb,"&apos;",",");
                SUtils.modifyStringBuffer(sb,"&amp;","&");
                try {
                    //Read element with report item.
                    Report r = new Report(KXmlUtils.parseXml(sb.toString()).getDocumentElement());
                    sb = new StringBuffer(i > 0 ? mod.substring(0, i) : "");
                    sb.append(r.toString(language));
                } catch (Exception ex) {
                    sb.append(Report.error(SYS.SYS201).toString(language));//Can't read report source
                }
                i = sb.length();
                if (j >= 0) {
                    sb.append(mod.substring(j + 2));
                }
                mod = sb.toString();
                i = mod.indexOf("&{&", i);
            }
        }
        return getModifiedText(txt, mod, language);
    }

    /** Get sorted array of parameter names from report text.
     * @param reportID report ID.
     * @param id language id (ISO-639) or null.
     * @return The sorted array of parameter names or null.
     */
    final String[] getReportParamNames(final String id, final String language) {
        ReportTable table = getReportTable(id, language);
        return table == null ? null : table.getReportParamNames(id);
    }

    /** Get reportID from registered report ID.
     * @param regID registered report ID with.
     * @return reportID decoded from registered report ID.
     */
    final static String getReportID(final long regID) {
        return getTextFromRegisteredForm(regID >>> ReportTable.IDBITS)
            + getTextFromRegisteredForm(regID & ReportTable.IDMASK);
    }

    ////////////////////////////////////////////////////////////////////////////
    // Private methods
    ////////////////////////////////////////////////////////////////////////////

    /** Add report table.
     * @param reportTable report table.
     * @return report table
     */
    private static ReportTable addReportTable(final ReportTable reportTable) {
        SManager sm = getInstance();
        synchronized(sm) {
            if (sm._tableHighIndex < 0) {
                sm._tables[0] = reportTable;
                sm._tableHighIndex = 0;
                return reportTable;
            }
            int i = sm.indexOfTable(reportTable.getRegisteredTableID());
            if (i >= 0) {
                ReportTable result = sm._tables[i];
                sm._tables[i] = reportTable; //already exists, we replace it
                return result; //we return the original one;
            }
            //new report table
            int len = sm._tables.length;
            if (len <= ++sm._tableHighIndex) {
                ReportTable[] tables = sm._tables;
                sm._tables = new ReportTable[len + TABLE_INC];
                System.arraycopy(tables, 0, sm._tables, 0, sm._tableHighIndex);
            }
            sm._tables[sm._tableHighIndex] = reportTable;
            sm.sortTables(0, sm._tableHighIndex);
            return reportTable; //not exists yet, we return the table
        }
    }

    /** Remove specified report table.
     * @param tableID Report table name.
     * @return removed table or null.
     */
    private ReportTable removeReportTable(final String tableID) {
        long id  = RegisterReportTables.getTableID(tableID);
        synchronized(this) {
            int i = indexOfTable(id);
            if (i < 0) {
                return null;
            }
            ReportTable result;
            if (_tableHighIndex == 0) {
                result = _tables[0];
                _tableHighIndex = -1;
                _tables[0] = null;
                return result;
            }
            result = _tables[i];
            if (_tables.length - _tableHighIndex > TABLE_INC) {
                ReportTable[] oldTables = _tables;
                _tables = new ReportTable[_tableHighIndex + TABLE_INC];
                if (i > 0) {
                    System.arraycopy(oldTables, 0, _tables, 0, i);
                }
                if (i < _tableHighIndex) {
                    System.arraycopy(oldTables,
                    i + 1, _tables, i, _tableHighIndex - i);
                }
            } else {
                if (i < _tableHighIndex) {
                    System.arraycopy(_tables, i + 1, _tables, i, _tableHighIndex - i);
                }
                _tables[_tableHighIndex] = null;
            }
            _tableHighIndex--;
            return result;
        }
    }

    /** Get text of report from report table. All references to other reports are resolved if the argument
     * resolveReferences is true.
     * @param regID registered report ID.
     * @param languageID computed language ID or 0.
     * @param resolveReferences if true then all references to other reports are resolved.
     * @return text of report.
     */
    private String getReportText(final long regID, final int languageID, final boolean resolveReferences) {
        long tabid = regID & ReportTable.REGTABIDMASK | (languageID == 0 ? _language.getID() : languageID);
        ReportTable table = findReportTable(tabid);
        if (table == null) {
            table = findReportTable(regID & ReportTable.REGTABIDMASK |_language.getDefaultID());
            if (table == null) {
                return null;
            }
        }
        String text = table.getReportText(regID);
        if (text == null) {
             if (getDefaultLanguage().equals(table.getLanguage())) {
                 return null;
             }
            table = findReportTable(regID & ReportTable.REGTABIDMASK | _language.getDefaultID());
        }
        return getReportText(table.getReportID(regID), table.getLanguage(), resolveReferences);
    }

    /** Get the end of parameter. Parameter may contain inner parts.
     * @param mod modification string.
     * @param start starting position of the parameter.
     * @return end position of parameter
     */
    private static int getParamaterEnd(final String mod, final int start) {
        int j = start + 1;
        int end = mod.indexOf('}', j);
        while ((j = mod.indexOf('{', ++j)) >= 0 && j < end) {
            int k = mod.indexOf('}', ++end);
            if (k < 0) {
                return end;
            }
            end = k;
        }
        return end;
    }

    /** Resolve report references in the report text.
     * @param reportText default text of report.
     * @param language language id (ISO-639).
     * @return text of report with resolved references.
     */
    private String resolveReportReferences(final String reportText, final String language) {
        int i;
        if (reportText == null || (i = reportText.indexOf("&{#")) < 0) {
            return reportText;
        }
        String text = reportText;
        int level = 0; //recursion level
        int j;
        do {
            j = text.indexOf("}", i + 3);
            if (j < 0) {
                return text; //error in report: reference is not closed
            }
            String s;
            String myId = text.substring(i + 3, j++);
            if (myId.length() > 0 && ++level < 100) {
                ReportTable table = getReportTable(myId, language);
                if (table == null) {
                    table = getReportTable(myId, getDefaultLanguage());
                    if (table == null) {
                        return null;
                    }
                }
                s = table.getReportText(myId);
                if (s == null) {
                    table = getReportTable(myId, getDefaultLanguage());
                    s = table == null ? "" : table.getReportText(myId);
                }
            } else {
                s = "";
            }
            text = text.substring(0, i) + s + text.substring(j);
        } while ((i = text.indexOf("&{#", j)) >= 0);
        return text;
    }

    /** Add report table with the specified table name.
     * @param tableName table ID (prefix + '_' + language)
     * @return ReportTable or null if no table can be added.
     */
    private ReportTable addReportTable(final String tableName){
        synchronized(this) {
            int i = indexOfTable(RegisterReportTables.getTableID(tableName));
            if (i >= 0) {
                return _tables[i];
            }
        }
        String[] ids = null; // list of report names in the registred table.
        for (String packageName : _packages) {
            try { // try to read properties from the package
                String prefix = tableName.substring(0, tableName.length() - 4);
                // get class of registered table.
                Class<?> c = Class.forName(packageName + '.' + prefix);
                // get fields of this class.
                Field[] fields = c.getDeclaredFields();
                List<String> ar = new ArrayList<>(fields.length-1);
                for (Field field : fields) {
                    String name = field.getName();
                    if (name.startsWith(prefix) && !prefix.equals(name)) {
                        // only fields of message identifiers
                        ar.add(name.substring(prefix.length()));
                    }
                }
                // create sorted array of message identifiers
                ids = new String[ar.size()];
                ar.toArray(ids);
                Arrays.sort(ids);
                InputStream input = c.getResourceAsStream(tableName + ".properties");
                if (input != null) {
                    InputStreamReader in = new InputStreamReader(input, StandardCharsets.UTF_8);
                    Properties props = new Properties();
                    props.load(in);
                    ReportTable table = RegisterReportTables.genReportTable(props);
                    table._ids = ids;
                    return addReportTable(table);
                }
            } catch (IOException | ClassNotFoundException | SecurityException ex) {}
            // not found, so we try to read the external data
            String s = getProperty(_properties, XDPROPERTY_MESSAGES+tableName);
            if (s == null) {
                s = getProperty(_properties, XDPROPERTY_MESSAGES + ReportTable.getPrefixFromID(tableName));
                if (s != null) {
                    File[] files = SUtils.getFileGroup(s);
                    s = null;
                    if (files != null) {
                        for (File f: files) {
                            String name = f.getName();
                            if (name.equals(tableName+".properties")) {
                                try {
                                    Properties props = ReportTable.readProperties(new FileInputStream(f));
                                    ReportTable table = RegisterReportTables.genReportTable(props);
                                    table._ids = ids;
                                    return addReportTable(table);
                                } catch (IOException ex) {
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            if (s != null) {
                try {
                    ReportTable table = RegisterReportTables.genReportTable(ReportTable.readProperties(s));
                    table._ids = ids;
                    return addReportTable(table);
                } catch (Exception ex) {}
            }
        }
        return null;
    }

    /** Get string from registered format.
     * @param registered encoded registered string.
     * @return decoded string from registered format.
     */
    private static String getTextFromRegisteredForm(final long registered) {
        StringBuilder sb = new StringBuilder(10);
        long i = registered;
        while (i > 0) {
            sb.insert(0, (char) (i % 27 + '@'));
            i /= 27;
        }
        return sb.toString();
    }

    /** Get ReportTable for the actual language or for the primary language. If the actual language table is
     * not available then a table for the primary language is returned. If none of those was found then return null.
     * @param tableID registered table ID.
     * @return report table or null.
     */
    private ReportTable findReportTable(final long tableID) {
        synchronized(this) {
            int i = indexOfTable(tableID);
            if (i >= 0) {
                return _tables[i];
            }
        }
        String prefix = getTextFromRegisteredForm(tableID >>> ReportTable.IDBITS);
        //Get language name from registered format
        StringBuilder sb = new StringBuilder(3);
        int i = (int) tableID & ReportTable.IDMASK;
        int x = 'a' - 1;
        while (i > 0) {
            sb.insert(0, (char) (i % 27 + x));
            i /= 27;
        }
        String lang = sb.toString();
        ReportTable result = addReportTable(prefix + '_' + lang);
        if (result != null) {
            return result;
        }
        long primaryID = tableID & ReportTable.REGTABIDMASK | _language.getDefaultID();
        if (tableID == primaryID){
            return null;
        }
        synchronized(this) {
            i = indexOfTable(primaryID);
            if (i >= 0) {
                return _tables[i];
            }
        }
        return addReportTable(prefix + '_' + _language.getDefaultLanguage());
    }

    /** Find index of table in sorted array of tables.
     * @param id table ID.
     * @return index of table if table was found otherwise return null.
     */
    private int indexOfTable(final long id) {
        int last;
        if ((last = _tableHighIndex) < 0) {
            return -1;
        }
        int first = 0;
        while (first < last) {
            int mid;
            long x = _tables[mid = (last + first) / 2].getRegisteredTableID();
            if (id > x) {
                first = mid + 1;  // repeat search in top half.
            } else if (id < x) {
                last = mid - 1; // repeat search in bottom half.
            } else {
                return mid;  // found it. return position
            }
        }
        return id == _tables[first].getRegisteredTableID() ? first : -1;
    }

    /** Sort array of tables in index sharp interval.
     * @param low low index.
     * @param high high index.
     */
    private void sortTables(final int low, final int high) {
        int lo = low, hi = high;
        long pivot = _tables[(lo + hi) / 2].getRegisteredTableID();
        while (lo <= hi) {
            while (_tables[lo].getRegisteredTableID() < pivot) {
                lo++;
            }
            while (_tables[hi].getRegisteredTableID() > pivot) {
                hi--;
            }
            if (lo <= hi) {
                if (lo < hi) {
                    ReportTable tmp = _tables[lo];
                    _tables[lo] = _tables[hi];
                    _tables[hi] = tmp;
                }
                lo++;
                hi--;
            }
        }
        if (low < hi) sortTables(low, hi);
        if (lo < high) sortTables(lo, high);
    }
}
