/*
 * File:     ShoeboxTypFile.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.server.corpora.clomimpl.shoebox;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;


/**
 * Reads the database description file (*.typ) of a Shoeboxfile.
 */
public class ShoeboxTypFile {
    private static Vector markers = new Vector();
    private static String typFileName = "";
    private static String databaseType = "";

    /**  each tier that is parent in a from-to relation */
    public Vector fromArray = new Vector();

    /**  each tier that is child in a from-to relation */
    private Vector toArray = new Vector();

    /**  hash of the above */
    public Hashtable tofromHash = new Hashtable();

    // HB, 24 jul 02: store procedureType, to derive constraint stereotype from.
    // Key values are same as for tofromHash.
    public Hashtable procedureTypeHash = new Hashtable();

    /** watch out: no \\ backslash on recordMarker */
    public String recordMarker = null;
    public String interlinearRootMarker = null;

    /** shoebox stores the name of a tier */
    private Vector tiersWithIPA = new Vector();
    private Vector tiersWithUnicode = new Vector();
    private Vector excludedTiers = new Vector();
    private boolean ddebug = "true".equals(System.getProperty("ddebug"));
    private boolean debug = "true".equals(System.getProperty("debug"));

    /**
       @param strict1 line must start with label (e.g. \ref).
       @param label00 name of block starting label, including leading \
     */
    public ShoeboxTypFile(File file) throws IllegalArgumentException, Exception {
        if (file == null) { // ShoeboxMarkerDialog MarkerRecords to be use

            //		initializeFromMarkerRecords();
        } else {
            if (!file.canRead()) {
                throw new IllegalArgumentException("cannot read \"" + file +
                    "\"");
            }

            readFile(file);
        }
    }

    public ShoeboxTypFile(Vector markerRecords) {
        if (markerRecords != null) {
            initializeFromMarkerRecords(markerRecords);
        }
    }

    /**
     * Fake typ-file, needed for WAC
     */
    public ShoeboxTypFile() throws IllegalArgumentException, Exception {
    }

    private void ddebug(String s) {
        if (ddebug) {
            System.out.println("---- ShoeboxFile3: " + s);
        }
    }

    private void debug(String s) {
        if (debug) {
            System.out.println("-- ShoeboxFile3: " + s);
        }
    }

    public static Vector getMarkers() {
        if ((typFileName != null) && typFileName.equals("")) {
            return markers;
        } else {
            return null;
        }
    }

    public static void setTypFileName(String theName) {
        typFileName = theName;
    }

    public static String getTypFileName() {
        return typFileName;
    }

    public static void setDatabaseType(String theType) {
        databaseType = theType;
    }

    public static String getDatabaseType() {
        if (databaseType.equals("") && !typFileName.equals("")) {
            // get database type from typFileName
            if (typFileName.endsWith(".typ") || typFileName.endsWith(".TYP")) {
                int leafIndex = typFileName.lastIndexOf("/") + 1;

                if (leafIndex <= 0) {
                    leafIndex = typFileName.lastIndexOf("\\") + 1;
                }

                int endIndex = typFileName.lastIndexOf(".");

                if ((leafIndex > 0) && (endIndex > 0) &&
                        (leafIndex < endIndex)) {
                    databaseType = typFileName.substring(leafIndex, endIndex);
                }
            }
        }

        return databaseType;
    }

    private void initializeFromMarkerRecords(Vector markerRecords) {
        //	Vector markerRecords = ShoeboxMarkerDialog.getMarkers();
        MarkerRecord topMarker = null;

        // assume that there is only one top marker
        // take the first marker without a parent
        Iterator mrIter = markerRecords.iterator();

        while (mrIter.hasNext()) {
            MarkerRecord mr = (MarkerRecord) mrIter.next();

            if ((mr.getParentMarker() == null) && !mr.isExcluded()) {
                if (topMarker == null) {
                    topMarker = mr;
                } else { // more than 1 root marker, assume that we deal with ELAN exported Toolbox files
                    topMarker = null; // reset

                    break;
                }
            }
        }

        if (topMarker != null) {
            interlinearRootMarker = topMarker.getMarker();
        }

        //System.out.println("Root: " + interlinearRootMarker);
        // if interlinearRootMarker == null parsing seems to silently return 
        // a transcription with zero tiers ??
        // fill fromArray, toArray, toFromHash and procedureTypeHash from mr's
        mrIter = markerRecords.iterator();

        while (mrIter.hasNext()) {
            MarkerRecord mr = (MarkerRecord) mrIter.next();

            if (mr.getParentMarker() != null) {
                fromArray.add("\\" + mr.getParentMarker());
                toArray.add("\\" + mr.getMarker());
                tofromHash.put("\\" + mr.getMarker(),
                    "\\" + mr.getParentMarker());

                if ((mr.getStereoType() != null) &&
                        mr.getStereoType().equals("Symbolic Association")) {
                    procedureTypeHash.put("\\" + mr.getMarker(), "Lookup");
                } else if ((mr.getStereoType() != null) &&
                        mr.getStereoType().equals("Time Subdivision")) {
                    procedureTypeHash.put("\\" + mr.getMarker(),
                        "TimeSubdivision");
                } else {
                    procedureTypeHash.put("\\" + mr.getMarker(), "Parse");
                }
            }

            if (mr.getCharsetString().equals(MarkerRecord.SILIPASTRING)) {
                tiersWithIPA.add(mr.getMarker());
            }

            if (mr.getCharsetString().equals(MarkerRecord.UNICODESTRING)) {
                tiersWithUnicode.add(mr.getMarker());
            }

            if (mr.getParticipantMarker()) {
                ShoeboxArray.label_eudicoparticipant = "\\" + mr.getMarker();
            }

            if (mr.isExcluded()) {
                excludedTiers.add(mr.getMarker());
            }
        }

        // HS jul 2005: if there are any markers marked for exclusion,
        // add their descendants to the excluded tiers array as well
        if (excludedTiers.size() > 0) {
            for (int i = 0; i < excludedTiers.size(); i++) {
                String parent = "\\" + (String) excludedTiers.get(i);

                if (fromArray.contains(parent)) {
                    addDescendantsToExcludedTiers(parent);
                }
            }
        }
    }

    public static void main(String[] arg) throws Exception {
        ShoeboxTypFile s = new ShoeboxTypFile(new File(arg[0]));
        System.out.println(s.interlinearRootMarker);
        System.out.println(s.fromArray);
        System.out.println(s.toArray);
        System.out.println("");
        System.out.println(s.procedureTypeHash.keySet());
        System.out.println(s.procedureTypeHash.values());
    }

    /*
      Used for preparation (counting) and storing.
    */
    private final void readFile(File file) throws Exception {
        String line = null;

        /*
          A shoebox file may contain 8byte characters from custom fonts.
          Treating it as isolatin-1 may introduce character errors!
        */
        Reader filereader;

        // HB, 24 jul 02: FAKE IMPLEMENTATION IS TO BE SUBSTITUTED !
        boolean useDedicatedCharacterset = false;

        if (useDedicatedCharacterset) {
            InputStream fis = new FileInputStream(file);
            filereader = new InputStreamReader(fis, "DedicatedCharacterset");
        } else {
            // use the default encoding
            filereader = new FileReader(file);
        }

        // explicit performance care: buffering the filereader
        BufferedReader br = new BufferedReader(filereader);

        String label = null;
        String content = null;
        String tierToSetLanguageFor = null;
        int linenumber = 0;
        String lastFrom = "";
        String lastTo = "";
        String procType = "Lookup";

        while ((line = br.readLine()) != null) {
            linenumber++;
            line = line.trim();
            debug("  ..." + line);

            if (line.length() == 0) {
                // skip white lines
                continue;
            }

            if (linenumber == 1) {
                // HB, 24 jul 02: accept only DatabaseType TEXT, only works for Shoebox text databases
                // MK/02/10/13 loosening check
                if (line.startsWith("\\+DatabaseType")) {
                    continue;
                } else {
                    throw new Exception(
                        "Shoebox typ file must begin with '\\+DatabaseType', found '" +
                        line + "'");
                }
            }

            // tokenize the shoebox line into label and content
            {
                StringTokenizer xxx = new StringTokenizer(line);
                label = xxx.nextToken(); // the first word

                // label contains trailing backslash!
            }

            content = (line.substring(label.length())).trim();

            if ((recordMarker == null) && label.equals("\\mkrRecord")) {
                interlinearRootMarker = content;
                recordMarker = content;
            }

            /*
                if (label.equals("\\mkrFrom")) {
                    fromArray.add("\\" + content);
                    lastFrom = content;
                }

                if (label.equals("\\mkrTo")) {
                    toArray.add("\\" + content);
                    tofromHash.put("\\" + content, "\\" + lastFrom);
                    //System.out.println("tofromHash.put "+ content + "--" + lastFrom+ "'");
                }
            */

            // HB, 24 jul 02: new logic, also storing procedureType, and insensitive to
            // order of markers.
            if (label.equals("\\+intprc")) {
                // reset
                lastFrom = "";
                lastTo = "";

                //			procType = "Lookup";
                procType = "Parse"; // hb, 7 sep 04, change default
            }

            if (label.equals("\\mkrFrom")) {
                lastFrom = content;
            }

            if (label.equals("\\mkrTo")) {
                lastTo = content;
            }

            if (line.indexOf("Lookup") >= 0) { // line contains ParseProc

                //			procType = "Parse";
                procType = "Lookup"; // hb, 7 sep 04
            }

            // hb, 16 sep 04
            if (line.indexOf("ParseProc") >= 0) {
                procType = "Parse";
            }

            if (label.equals("\\-intprc")) {
                // store results in Hashtables and arrays
                fromArray.add("\\" + lastFrom);
                toArray.add("\\" + lastTo);
                tofromHash.put("\\" + lastTo, "\\" + lastFrom);
                procedureTypeHash.put("\\" + lastTo, procType);
            }

            //MK:02/08/16 add language for tier
            if (label.equals("\\+mkr")) {
                tierToSetLanguageFor = content;
            }

            if (label.equals("\\lng")) {
                if (content == null) {
                    continue;
                }

                if (!content.equals("IPA") && !content.equals("Phonetic")) {
                    continue;
                }

                if (tierToSetLanguageFor == null) {
                    continue;
                }

                tiersWithIPA.add(tierToSetLanguageFor);
            }

            if (label.equals("\\-mkr")) {
                tierToSetLanguageFor = null;
            }
        }

        br.close();
        filereader.close();

        // HS july 2005 do some post processing; the first mkrFrom under the RecordMarker
        // is typically not listed in the 'intproclst'. Now assume that a marker that is 
        // direct child of the RecordMarker and has a child with proc "Parse" that this 
        // in between marker also has a "Parse" relation to the RecordMarker and add it 
        // to the hash
        // HS 29 sep 2005: it seems to be save to assume a "Parse" relation in the above 
        // situation even this direct child of the RecordMarker only has a "Lookup" child.
        int size = toArray.size(); //loop only over existing entries

        for (int i = 0; i < size; i++) {
            String key = (String) toArray.get(i);
            String val = (String) procedureTypeHash.get(key);

            if ((val != null) && (val.equals("Parse") || val.equals("Lookup"))) {
                String from = (String) tofromHash.get(key);

                if ((from != null) && !toArray.contains(from)) {
                    // assume a Parse relation to RecordMarker
                    fromArray.add("\\" + recordMarker);
                    toArray.add(from);
                    tofromHash.put(from, "\\" + recordMarker);
                    procedureTypeHash.put(from, "Parse");
                }
            }
        }
    }

    // HB, 30 jul 02: added because necessary in ShoeboxArray class to determine number of spaces
    // to pad lines in case of 'broken' interlinear blocks.

    /**
     * Returns all shoebox tier markers that take part in the interlinear setup.
     *
     * Note: HS july 2005
     * Changed the way the set of markers that participate in the interlinear setup
     * is build: (based on a very basic understanding of what is possible in a Shoebox file)<br>
     * - before:
     *   - include all markers except the root tier marker
     * - after:
     *   - include all markers (and their descendants) that have a "Parse" or "TimeSubdivision"
     *     relationship with the root tier marker or that have at least one descendant marker
     *     that has a "Parse" or "TimeSubdivision" relationship with its parent
     *
     * Question: should markers that are marked for exclusion from import (and their descendants)
     * be excluded from this Set (or is this handled by ShoeboxArray/Parser)??
     */
    public HashSet getInterlinearTierMarkers() {
        //printStats();
        HashSet markers = new HashSet();

        String rootMkr = "\\" + interlinearRootMarker;

        for (int i = 0; i < toArray.size(); i++) {
            String mkrLabel = (String) toArray.get(i);

            if (mkrLabel.equals(rootMkr) || excludeFromImport(mkrLabel)) {
                //System.out.println("Skipping marker: " + mkrLabel);
                continue;
            }

            String proc = (String) procedureTypeHash.get(mkrLabel);
            String parent = (String) tofromHash.get(mkrLabel);

            if ((proc != null) && (parent != null) && parent.equals(rootMkr)) {
                if (proc.equals("Parse") || proc.equals("TimeSubdivision")) {
                    markers.add(mkrLabel);
                    markers.addAll(getDescendantsOf(mkrLabel));

                    //System.out.println("Adding... " + mkrLabel);
                } else {
                    // only add the marker + sub markers if somewhere down the tree a 
                    // "Parse" relationship is encountered
                    if (atLeastOneParseInTree(mkrLabel)) {
                        //System.out.println("Parse found in tree: " + mkrLabel);
                        markers.add(mkrLabel);
                        markers.addAll(getDescendantsOf(mkrLabel));
                    }
                }
            }
        }

        //markers.addAll(toArray);
        //markers.addAll(fromArray);
        // HB, 15-9-04
        if (markers.contains("\\" + interlinearRootMarker)) {
            markers.remove("\\" + interlinearRootMarker);
        }

        /*
        System.out.println("\nMarkers...");
        Iterator hsIter = markers.iterator();
        while (hsIter.hasNext()) {
            String mkrLabel = (String) hsIter.next();
            System.out.println(mkrLabel);
        }
        */
        return markers;
    }

    /**
     * Recursively get the children (of any 'procedure' type) of the
     * specified marker.
     *
     * @param mkrLabel the parent marker
     * @return a (flat) list of descendant markers
     */
    private List getDescendantsOf(String mkrLabel) {
        List desc = new ArrayList();

        if (fromArray.contains(mkrLabel) && !excludeFromImport(mkrLabel)) {
            Iterator keysIt = tofromHash.keySet().iterator();
            String key = null;
            String val = null;

            while (keysIt.hasNext()) {
                key = (String) keysIt.next();
                val = (String) tofromHash.get(key);

                if (val.equals(mkrLabel) && !excludeFromImport(key)) {
                    desc.add(key); //key is the "toMkr"
                    desc.addAll(getDescendantsOf(key));
                }
            }
        }

        return desc;
    }

    /**
     * Check the marker's subtree to see if there is somewhere down the tree
     * a "Parse" or "TimeSubdivision" relationship.
     *
     * @param mkrLabel probe the descendants of this marker
     * @return true if a Parse relationship has been found, false otherwise
     */
    private boolean atLeastOneParseInTree(String mkrLabel) {
        boolean parse = false;

        if (toArray.contains(mkrLabel)) {
            Iterator keysIt = tofromHash.keySet().iterator();
            String key = null;
            String val = null;
            String proc = null;

            while (keysIt.hasNext()) {
                key = (String) keysIt.next();
                val = (String) tofromHash.get(key);
                proc = (String) procedureTypeHash.get(key);

                if (val.equals(mkrLabel)) {
                    if ((proc != null) &&
                            (proc.equals("Parse") ||
                            proc.equals("TimeSubdivision"))) {
                        return true;
                    } else {
                        return atLeastOneParseInTree(key);
                    }
                }
            }
        }

        return parse;
    }

    /**
     * When a marker is marked for exclusion recursively add the descendant markers
     * to the array of excluded tiers as well.
     *
     * @param parent the parent marker
     */
    private void addDescendantsToExcludedTiers(String parent) {
        Iterator it = tofromHash.keySet().iterator();
        String toKey;
        String fromVal;

        while (it.hasNext()) {
            toKey = (String) it.next();
            fromVal = (String) tofromHash.get(toKey);

            if (fromVal.equals(parent)) {
                excludedTiers.add(toKey.substring(1));
                addDescendantsToExcludedTiers(toKey);
            }
        }
    }

    /**
     * @return All shoebox tiers (without backslash) with language IPA
     */
    public boolean isIPAtier(String name) {
        if (name.startsWith("\\")) {
            name = name.substring(1);
        }

        //		System.out.println("isIPAtier("+name+") --> " + tiersWithIPA.contains(name));
        return tiersWithIPA.contains(name);
    }

    public boolean isUnicodeTier(String name) {
        if (name.startsWith("\\")) {
            name = name.substring(1);
        }

        //		System.out.println("isIPAtier("+name+") --> " + tiersWithIPA.contains(name));
        return tiersWithUnicode.contains(name);
    }

    public boolean excludeFromImport(String name) {
        if (name.startsWith("\\")) {
            name = name.substring(1);
        }

        return excludedTiers.contains(name);
    }

    /**
     * Prints the contents of the arrays and hashtables, to get an idea of what
     * is inside.
     */
    private void printStats() {
        System.out.println("Root: " + interlinearRootMarker);

        System.out.println("\nTo-From hash...");

        Iterator keysIt = tofromHash.keySet().iterator();

        while (keysIt.hasNext()) {
            String key = (String) keysIt.next();
            String val = (String) tofromHash.get(key);
            System.out.println("Key-value: " + key + " - " + val);
        }

        // print procedure Hash
        System.out.println("\nProcedure hash...");
        keysIt = procedureTypeHash.keySet().iterator();

        while (keysIt.hasNext()) {
            String key = (String) keysIt.next();
            String val = (String) procedureTypeHash.get(key);
            System.out.println("Key-value: " + key + " - " + val);
        }

        String rootMkr = "\\" + interlinearRootMarker;
        System.out.println("\nTo array...");

        for (int i = 0; i < toArray.size(); i++) {
            String mkrLabel = (String) toArray.get(i);

            if (mkrLabel.equals(rootMkr)) {
                System.out.println(mkrLabel + " (root)");
            } else {
                System.out.println(mkrLabel);
            }
        }

        System.out.println("\nFrom array...");

        for (int i = 0; i < fromArray.size(); i++) {
            String mkrLabel = (String) fromArray.get(i);

            if (mkrLabel.equals(rootMkr)) {
                System.out.println(mkrLabel + " (root)");
            } else {
                System.out.println(mkrLabel);
            }
        }

        System.out.println("\nExcluded array...");

        for (int i = 0; i < excludedTiers.size(); i++) {
            String mkrLabel = (String) excludedTiers.get(i);

            if (mkrLabel.equals(rootMkr)) {
                System.out.println(mkrLabel + " (root)");
            } else {
                System.out.println(mkrLabel);
            }
        }
    }
}
