/*
 * File:     EAFSkeletonParser.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.dobes;

import mpi.eudico.server.corpora.clomimpl.abstr.ParseException;
import mpi.eudico.server.corpora.clomimpl.abstr.TierImpl;
import mpi.eudico.server.corpora.clomimpl.type.CVEntry;
import mpi.eudico.server.corpora.clomimpl.type.Constraint;
import mpi.eudico.server.corpora.clomimpl.type.ControlledVocabulary;
import mpi.eudico.server.corpora.clomimpl.type.LinguisticType;
import mpi.eudico.server.corpora.clomimpl.type.SymbolicAssociation;
import mpi.eudico.server.corpora.clomimpl.type.SymbolicSubdivision;
import mpi.eudico.server.corpora.clomimpl.type.TimeSubdivision;

import org.apache.xerces.parsers.SAXParser;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;


/**
 * Parses an eaf file, creating objects of ControlledVocabularies,
 * LinguisticTypes and Tiers only. The rest is skipped.
 *
 * @author Han Sloetjes
 * @version 1.0 jan 2006: reflects EAFv2.2.xsd
 */
public class EAFSkeletonParser {
    /** the sax parser */
    private final SAXParser saxParser;

    /** the currently supported eaf version */
    private final String version = "2.2";
    private String fileName;

    /** stores tiername - tierrecord pairs */
    private final HashMap tierMap = new HashMap();
    private ArrayList tiers;

    /** stores linguistic types records! */
    private final ArrayList lingTypeRecords = new ArrayList();
    private ArrayList linguisticTypes;

    /** stores the Locales */
    private final ArrayList locales = new ArrayList();

    /** stores the ControlledVocabulary objects */
    private final ArrayList cvList = new ArrayList();
    private String currentTierId;
    private String currentCVId;
    private ControlledVocabulary currentCV;
    private String currentEntryDesc;
    private String content = "";

    /**
     * Creates a new EAFSkeletonParser instance
     *
     * @param fileName the file to be parsed
     *
     * @throws ParseException any exception that can occur when creating
     * a parser
     * @throws NullPointerException thrown when the filename is null
     */
    public EAFSkeletonParser(String fileName) throws ParseException {
        if (fileName == null) {
            throw new NullPointerException();
        }

        this.fileName = fileName;
        saxParser = new SAXParser();

        try {
            saxParser.setFeature("http://xml.org/sax/features/validation", true);
            saxParser.setFeature("http://apache.org/xml/features/validation/dynamic",
                true);
            saxParser.setProperty("http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation",
                "http://www.mpi.nl/tools/elan/EAFv2.2.xsd");
            saxParser.setContentHandler(new EAFSkeletonHandler());
        } catch (SAXNotRecognizedException e) {
            e.printStackTrace();
            throw new ParseException(e.getMessage());
        } catch (SAXNotSupportedException e) {
            e.printStackTrace();
            throw new ParseException(e.getMessage());
        }
    }

    /*
       public ArrayList getMediaDescriptors() {
           return null;
       }
     */

    /**
     * @see mpi.eudico.server.corpora.clomimpl.abstr.Parser#getLinguisticTypes(java.lang.String)
     */
    public ArrayList getLinguisticTypes() {
        return linguisticTypes;
    }

    /**
     * Returns a list of tier objects.
     *
     * @return a list of tiers
     */
    public ArrayList getTiers() {
        return tiers;
    }

    /**
     * Returns a list of CVs.
     *
     * @return a list of Controlled Vocabularies
     */
    public ArrayList getControlledVocabularies() {
        return cvList;
    }

    /**
     * Returns the current version of the skeleton parser.
     *
     * @return the current version
     */
    public String getVersion() {
        return version;
    }

    /**
     * Starts the actual parsing.
     *
     * @throws ParseException any parse exception
     */
    public void parse() throws ParseException {
        // init maps and lists
        try {
            saxParser.parse(fileName);
            createObjects();
        } catch (SAXException sax) {
            System.out.println("Parsing error: " + sax.getMessage());

            // the SAX parser can have difficulties with certain characters in 
            // the filepath: try to create an InputSource for the parser 
            File f = new File(fileName);

            if (f.exists()) {
                try {
                    FileInputStream fis = new FileInputStream(f);
                    InputSource source = new InputSource(fis);
                    saxParser.parse(source);
                    createObjects();

                    // just catch any exception
                } catch (Exception ee) {
                    System.out.println("Parsing retry error: " +
                        ee.getMessage());
                    throw new ParseException(ee.getMessage(), ee.getCause());
                }
            }
        } catch (IOException ioe) {
            throw new ParseException(ioe.getMessage(), ioe.getCause());
        }
    }

    /**
     * After parsing create objects from the records; tiers and linguistic
     * types. CV's + CVEntries and Locales have already been made.
     */
    private void createObjects() {
        linguisticTypes = new ArrayList(lingTypeRecords.size());

        for (int i = 0; i < lingTypeRecords.size(); i++) {
            LingTypeRecord ltr = (LingTypeRecord) lingTypeRecords.get(i);

            LinguisticType lt = new LinguisticType(ltr.getLingTypeId());

            boolean timeAlignable = true;

            if (ltr.getTimeAlignable().equals("false")) {
                timeAlignable = false;
            }

            lt.setTimeAlignable(timeAlignable);

            boolean graphicReferences = false;

            if (ltr.getGraphicReferences().equals("true")) {
                graphicReferences = true;
            }

            lt.setGraphicReferences(graphicReferences);

            String stereotype = ltr.getStereoType();
            Constraint c = null;

            if (stereotype != null) {
                stereotype = stereotype.replace('_', ' '); // for backwards compatibility

                if (stereotype.equals(
                            Constraint.stereoTypes[Constraint.TIME_SUBDIVISION])) {
                    c = new TimeSubdivision();
                } else if (stereotype.equals(
                            Constraint.stereoTypes[Constraint.SYMBOLIC_SUBDIVISION])) {
                    c = new SymbolicSubdivision();
                } else if (stereotype.equals(
                            Constraint.stereoTypes[Constraint.SYMBOLIC_ASSOCIATION])) {
                    c = new SymbolicAssociation();
                }
            }

            if (c != null) {
                lt.addConstraint(c);
            }

            lt.setControlledVocabularyName(ltr.getControlledVocabulary());

            linguisticTypes.add(lt);
        }

        tiers = new ArrayList(tierMap.size());

        HashMap parentHash = new HashMap();

        Iterator tierIt = tierMap.values().iterator();
        TierRecord rec;
        TierImpl tier;
        LinguisticType type;
        Locale loc;

        while (tierIt.hasNext()) {
            tier = null;
            type = null;

            rec = (TierRecord) tierIt.next();
            tier = new TierImpl(null, rec.getName(), rec.getParticipant(),
                    null, null);

            Iterator typeIter = linguisticTypes.iterator();

            while (typeIter.hasNext()) {
                LinguisticType lt = (LinguisticType) typeIter.next();

                if (lt.getLinguisticTypeName().equals(rec.getLinguisticType())) {
                    type = lt;

                    break;
                }
            }

            if (type == null) {
                // don't add the tier, something's wrong
                continue;
            }

            tier.setLinguisticType(type);

            if (rec.getDefaultLocale() == null) {
                // default, en
                tier.setDefaultLocale(new Locale("en", "", ""));
            } else {
                Iterator locIt = locales.iterator();

                while (locIt.hasNext()) {
                    loc = (Locale) locIt.next();

                    if (loc.getLanguage().equals(rec.getDefaultLocale())) {
                        tier.setDefaultLocale(loc);

                        break;
                    }
                }
            }

            if (rec.getParentTier() != null) {
                parentHash.put(tier, rec.getParentTier());
            }

            tiers.add(tier);
        }

        // all Tiers are created. Now set all parent tiers
        Iterator parentIter = parentHash.keySet().iterator();

        while (parentIter.hasNext()) {
            TierImpl t = (TierImpl) parentIter.next();
            String parent = (String) parentHash.get(t);

            Iterator secIt = tiers.iterator();

            while (secIt.hasNext()) {
                TierImpl pt = (TierImpl) secIt.next();

                if (pt.getName().equals(parent)) {
                    t.setParentTier(pt);

                    break;
                }
            }
        }
    }

    //#######################
    // Content handler
    //#######################
    class EAFSkeletonHandler implements ContentHandler {
        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#endDocument()
         */
        public void endDocument() throws SAXException {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#startDocument()
         */
        public void startDocument() throws SAXException {
        }

        /**
         * ContentHandler method
         *
         * @param ch the characters
         * @param start start index
         * @param end end index
         *
         * @throws SAXException sax exception
         */
        public void characters(char[] ch, int start, int end)
            throws SAXException {
            content += new String(ch, start, end);
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
         */
        public void ignorableWhitespace(char[] ch, int start, int length)
            throws SAXException {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
         */
        public void endPrefixMapping(String prefix) throws SAXException {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
         */
        public void skippedEntity(String name) throws SAXException {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
         */
        public void setDocumentLocator(Locator locator) {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
         */
        public void processingInstruction(String target, String data)
            throws SAXException {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
         */
        public void startPrefixMapping(String prefix, String uri)
            throws SAXException {
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
         */
        public void startElement(String nameSpaceURI, String name,
            String rawName, Attributes attributes) throws SAXException {
            content = "";

            if (name.equals("TIER")) {
                currentTierId = attributes.getValue("TIER_ID");

                // First check whether this tier already exists, prevent duplicates
                if (!tierMap.containsKey(currentTierId)) {
                    // create a record
                    TierRecord tr = new TierRecord();
                    tr.setName(currentTierId);
                    tierMap.put(currentTierId, tr);

                    tr.setParticipant(attributes.getValue("PARTICIPANT"));
                    tr.setLinguisticType(attributes.getValue(
                            "LINGUISTIC_TYPE_REF"));
                    tr.setDefaultLocale(attributes.getValue("DEFAULT_LOCALE"));
                    tr.setParentTier(attributes.getValue("PARENT_REF"));
                }
            } else if (name.equals("LINGUISTIC_TYPE")) {
                LingTypeRecord ltr = new LingTypeRecord();

                ltr.setLingTypeId(attributes.getValue("LINGUISTIC_TYPE_ID"));

                String timeAlignable = "true";

                if ((attributes.getValue("TIME_ALIGNABLE") != null) &&
                        (attributes.getValue("TIME_ALIGNABLE").equals("false"))) {
                    timeAlignable = "false";
                }

                ltr.setTimeAlignable(timeAlignable);

                String graphicReferences = "false";

                if ((attributes.getValue("GRAPHIC_REFERENCES") != null) &&
                        (attributes.getValue("GRAPHIC_REFERENCES").equals("true"))) {
                    graphicReferences = "true";
                }

                ltr.setGraphicReferences(graphicReferences);

                String stereotype = attributes.getValue("CONSTRAINTS");
                ltr.setStereoType(stereotype);

                ltr.setControlledVocabulary(attributes.getValue(
                        "CONTROLLED_VOCABULARY_REF"));

                lingTypeRecords.add(ltr);
            } else if (name.equals("LOCALE")) {
                String langCode = attributes.getValue("LANGUAGE_CODE");
                String countryCode = attributes.getValue("COUNTRY_CODE");

                if (countryCode == null) {
                    countryCode = "";
                }

                String variant = attributes.getValue("VARIANT");

                if (variant == null) {
                    variant = "";
                }

                Locale l = new Locale(langCode, countryCode, variant);
                locales.add(l);
            } else if (name.equals("CONTROLLED_VOCABULARY")) {
                currentCVId = attributes.getValue("CV_ID");
                currentCV = new ControlledVocabulary(currentCVId);

                String desc = attributes.getValue("DESCRIPTION");

                if (desc != null) {
                    currentCV.setDescription(desc);
                }

                cvList.add(currentCV);
            } else if (name.equals("CV_ENTRY")) {
                currentEntryDesc = attributes.getValue("DESCRIPTION");
            }
        }

        /* (non-Javadoc)
         * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
         */
        public void endElement(String nameSpaceURI, String name, String rawName)
            throws SAXException {
            if (name.equals("CV_ENTRY")) {
                currentCV.addEntry(new CVEntry(content, currentEntryDesc));
            }
        }
    }
}
