/**
 * StAXModelBuilder.java
 * 
 * @author Charles Groves
 */
package edu.sc.seis.sod.validator.model;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import edu.sc.seis.sod.SodUtil;
import edu.sc.seis.sod.Start;
import edu.sc.seis.sod.XMLUtil;
import edu.sc.seis.sod.validator.ModelWalker;
import edu.sc.seis.sod.validator.model.datatype.AnyText;
import edu.sc.seis.sod.validator.model.datatype.BooleanDatatype;
import edu.sc.seis.sod.validator.model.datatype.DoubleDatatype;
import edu.sc.seis.sod.validator.model.datatype.FloatDatatype;
import edu.sc.seis.sod.validator.model.datatype.IntegerDatatype;
import edu.sc.seis.sod.validator.model.datatype.NonnegativeIntegerDatatype;
import edu.sc.seis.sod.validator.model.datatype.Token;

public class StAXModelBuilder implements XMLStreamConstants {

    public StAXModelBuilder(String relaxLoc) throws XMLStreamException,
            IOException {
        if(parsedGrammars.containsKey(relaxLoc)) {
            definedGrammar = parsedGrammars.get(relaxLoc);
        } else {
            if(relaxLoc.endsWith("anyXML.rng")) {
                definedGrammar = new Grammar(relaxLoc);
                NamedElement anyXML = new NamedElement(1, 1, "anyXML");
                anyXML.setChild(new Empty(anyXML));
                Definition start = new Definition("", definedGrammar);
                start.set(anyXML);
                Annotation ann = new Annotation();
                ann.setSummary("Any well-formed XML");
                ann.setDescription("This ingredient describes any well formed XML document.  You must replace it with a root element, and then inside of it, any well formed XML will do.");
                definedGrammar.add(start);
            } else {
                ClassLoader cl = getClass().getClassLoader();
                InputStream relaxSource = Start.createInputStream(cl, relaxLoc);
                reader = XMLUtil.getStaxInputFactory().createXMLStreamReader(relaxSource);
                definedGrammar = new Grammar(relaxLoc);
                reader.next();// SKIP SPACE
                reader.next();// GET TO GRAMMAR START TAG
                try {
                    handleGrammar();
                } catch(XMLStreamException e) {
                    System.out.println("ERROR ON " + relaxLoc);
                    e.printStackTrace();
                    System.exit(0);
                }
                reader.close();
            }
            parsedGrammars.put(relaxLoc, definedGrammar);
            if(waiters.size() > 0) {
                Iterator<Map.Entry<String, String>> it = waiters.entrySet().iterator();
                while(it.hasNext()) {
                    Map.Entry<String, String> entry = it.next();
                    definedGrammar.add(entry.getKey(),
                                       definedGrammar.getDef(entry.getValue()));
                }
            }
        }
    }

    private void handleGrammar() throws XMLStreamException {
        while(reader.hasNext()) {
            switch(reader.next()){
                case START_ELEMENT:
                    String tag = reader.getLocalName();
                    if(tag.equals("start")) {
                        Definition root = handleStart();
                        definedGrammar.add(root);
                    } else if (tag.equals("define")) {
                        Definition def = handleDef();
                        if(def != null) {
                            definedGrammar.add(def);
                        }
                    } else if(tag.equals("include")) {
                        handleInclude();
                    } else {
                        System.out.println("I DON'T THINK THIS SHOULD BE HERE");
                        whatIs();
                    }
                    break;
                default:
                    break;
            }
        }
    }

    public void whatIs() {
        switch(reader.getEventType()){
            case START_ELEMENT:
                System.out.println("START " + reader.getLocalName());
                break;
            case END_ELEMENT:
                System.out.println("END " + reader.getLocalName());
                break;
            case COMMENT:
                System.out.println("COMMENT " + reader.getText());
                break;
            default:
                System.out.println("DUNNO");
                break;
        }
    }

    private void handleInclude() {
        // TODO handle inclusion overrides
        definedGrammar.include(getGrammar(getAbsPath()));
    }


    private Definition handleStart() throws XMLStreamException {
        Definition def = new Definition("", definedGrammar);
        nextTag();
        FormProvider result = handleAll();
        def.set(result);
        return def;
    }
    
    private Definition handleDef() throws XMLStreamException {
        int combo = Definition.UNDEFINED;
        String name = "";
        for(int i = 0; i < reader.getAttributeCount(); i++) {
            if(reader.getAttributeLocalName(i).equals("combine")) {
                String value = reader.getAttributeValue(i);
                if(value.equals("choice")) {
                    combo = Definition.CHOICE;
                } else if(value.equals("interleave")) {
                    combo = Definition.INTERLEAVE;
                }
            } else if(reader.getAttributeLocalName(i).equals("name")) {
                name = reader.getAttributeValue(i);
            }
        }
        Definition def = new Definition(name, definedGrammar, combo);
        nextTag();
        FormProvider result = handleAll();
        def.set(result);
        if(result instanceof Ref) {
            Ref ref = (Ref)result;
            def = ref.getDef();
            waiters.put(name, ref.getName());
        }
        return def;
    }

    private void nextTag() throws XMLStreamException {
        if(reader.hasNext()) {
            int ev = reader.next();
            while(reader.hasNext() && ev != START_ELEMENT && ev != END_ELEMENT) {
                ev = reader.next();
            }
        }
    }

    private FormProvider handleAll() throws XMLStreamException {
        List<Object> kids = new ArrayList<Object>();
        while(reader.getEventType() == START_ELEMENT) {
            String tag = reader.getLocalName();
            if(isCardinality(tag)) {
                kids.add(handleCardinality());
            } else if(tag.equals("element")) {
                kids.add(handleElement());
            } else if(tag.equals("attribute")) {
                kids.add(handleAttr());
            } else if(isMulitgen(tag)) {
                kids.add(handleMultigen());
            } else if(tag.equals("ref")) {
                kids.add(handleRef());
            } else if(tag.equals("externalRef")) {
                kids.add(handleExtRef());
            } else if(tag.equals("anyName")) {
                // ignore...
                nextTag();
                nextTag();
            } else if(isData(tag)) {
                kids.add(handleData());
            } else {
                throw new RuntimeException("Unknown tag! " + tag + " " + definedGrammar);
            }
        }
        if(kids.size() == 0) {
            return null;
        }// Hopefully an attribute called
        // handleAll
        if(kids.size() == 1) {
            return (FormProvider)kids.get(0);
        }
        Group g = new Group(1, 1);
        Iterator<Object> it = kids.iterator();
        while(it.hasNext()) {
            g.add((FormProvider)it.next());
        }
        return g;
    }

    private boolean isAnn(String tag) {
        return tag.equals("annotation");
    }

    private Annotation handleAnn() throws XMLStreamException {
        Annotation note = new Annotation();
        if(isAnn(reader.getLocalName())) {
            while(reader.next() != END_ELEMENT
                    || !reader.getLocalName().equals("annotation")) {
                if(reader.getEventType() == START_ELEMENT) {
                    if(reader.getLocalName().equals("summary")) {
                        reader.next();
                        note.setSummary(reader.getText());
                    } else if(reader.getLocalName().equals("description")) {
                        note.setDescription(extractSubstructure());
                    } else if(reader.getLocalName().equals("deprecated")) {
                        note.setDeprecation(extractSubstructure());
                    } else if(reader.getLocalName().equals("include")) {
                        reader.next();
                        note.setInclude(true);
                    } else if(reader.getLocalName().equals("example")) {
                        String example = extractSubstructure();
                        example = example.replaceAll("\\t", "        ");
                        int j = 0;
                        for(int i = 0; i < example.length(); i++) {
                            if(example.charAt(i) == '\n') {
                                j = 0;
                            } else if(example.charAt(i) != ' ') {
                                break;
                            } else {
                                j++;
                            }
                        }
                        example = example.trim();
                        note.setExample(example.replaceAll("[ \\t]{" + j + "}",
                                                           ""));
                    } else if(reader.getLocalName().equals("velocity")) {
                        reader.next();
                        note.setVelocity(reader.getText());
                    } else if(!reader.getLocalName().equals("documentation")) {
                        System.out.println("Unrecognized tag "
                                + reader.getLocalName() + " in annotation in "
                                + definedGrammar);
                    }
                }
            }
            reader.nextTag();
        }
        return note;
    }

    public String extractSubstructure() throws XMLStreamException {
        String stopTagName = reader.getLocalName();
        StringBuffer buf = new StringBuffer();
        reader.next();
        int prevEventType = -1;
        while(reader.getEventType() != END_ELEMENT
                || !reader.getLocalName().equals(stopTagName)) {
            int curEventType = reader.getEventType();
            // this if-else block takes care of empty tags
            if(prevEventType == START_ELEMENT && curEventType == END_ELEMENT) {
                buf.setCharAt(buf.length() - 1, ' ');
                buf.append("/>");
            } else {
                buf.append(XMLUtil.readEvent(reader));
            }
            reader.next();
            prevEventType = curEventType;
        }
        return buf.toString();
    }

    /**
     * Method handleCardinality assumes that the reader has been advanced to a
     * START_ELEMENT with a local name of one of the cardinality elements:
     * zeroOrMore, optional or oneOrMore It returns a FormProvider representing
     * the internals of that cardinality and advances the reader to the next tag
     * past the END_ELEMENT of the cardinality The parent on the returned
     * FormProvider is not set, so this must be handled by the object calling
     * this.
     */
    private FormProvider handleCardinality() throws XMLStreamException {
        // get cardinality based on the tag name
        int min = 1;
        int max = 1;
        String tag = reader.getLocalName();
        if(tag.equals("zeroOrMore") || tag.equals("optional")) {
            min = 0;
        }
        if(tag.equals("zeroOrMore") || tag.equals("oneOrMore")) {
            max = Integer.MAX_VALUE;
        }
        // make sub structure
        reader.nextTag();
        if(isAnn(reader.getLocalName())) {
            Annotation note = handleAnn();
            throw new RuntimeException("Annotation with summary "
                    + note.getSummary()
                    + " not allowed directly in cardinality tag");
        }
        FormProvider result = handleAll();
        // set cardinality on substructure
        if(min == 0) {
            result.setMin(min);
        }
        if(max == Integer.MAX_VALUE) {
            result.setMax(max);
        }
        // advance past the end of cardinality end element and return
        reader.nextTag();
        return result;
    }

    private boolean isCardinality(String tag) {
        return tag.equals("oneOrMore") || tag.equals("zeroOrMore")
                || tag.equals("optional");
    }

    /**
     * Method handleElement assumes that the reader has been advanced to a
     * START_ELEMENT with a local name of element It returns a NamedElement
     * representing that element and its children and advances the reader to the
     * next tag past the END_ELEMENT of the element handle started The parent on
     * the returned FormProvider is not set, so this must be handled by the
     * object calling this.
     */
    private FormProvider handleElement() throws XMLStreamException {
        if (reader.getAttributeCount() == 0) {
            // no attributes, so no name specified in attribute
            // we only do this (hopefully) for cases where any xml is valid
            nextTag();
            AnyXMLElement out = new AnyXMLElement(1, 1);
            out.setNamespace(ModelWalker.getNamespaceFromAncestors(out));
            out.setAnnotation(handleAnn());
            out.setChild(handleAll());
            nextTag();
            return out;
        }
        String name = reader.getAttributeValue(0);
        String ns = reader.getAttributeValue(null, "ns");
        nextTag();
        NamedElement result = new NamedElement(1, 1, name);
        if(ns == null) {
            ns = ModelWalker.getNamespaceFromAncestors(result);
        }
        result.setNamespace(ns);
        result.setAnnotation(handleAnn());
        try {
        result.setChild(handleAll());
        } catch (RuntimeException e) {
            throw new RuntimeException("in "+name, e);
        }
        nextTag();
        return result;
    }

    private Object handleAttr() throws XMLStreamException {
        String name;
        if (reader.getAttributeCount() == 0) {
            name = "anyAttribute";
        } else {
            name = reader.getAttributeValue(0);
        }
        String ns = reader.getAttributeValue(null, "ns");
        nextTag();
        Attribute result = new Attribute(1, 1, name);
        if(ns == null) {
            ns = ModelWalker.getNamespaceFromAncestors(result);
        } // inherit ns from parents if there is no specified ns
        result.setNamespace(ns);
        result.setAnnotation(handleAnn());
        FormProvider child = handleAll();
        if(child == null) {
            child = new Text();
        } // An empty attribute has text as a value
        result.setChild(child);
        nextTag();
        return result;
    }

    private FormProvider handleMultigen() throws XMLStreamException {
        String tag = reader.getLocalName();
        AbstractMultigenitorForm parent;
        if(tag.equals("choice")) {
            parent = new Choice(1, 1);
        } else if(tag.equals("group")) {
            parent = new Group(1, 1);
        } else if(tag.equals("list")) {
            parent = new DataList(1, 1);
        } else {
            parent = new Interleave(1, 1);
        }
        nextTag();
        Annotation followingNote = handleAnn();
        FormProvider child = handleAll();
        nextTag();
        if(child instanceof AbstractMultigenitorForm) {
            // If the child is an AbstractMultigenitorForm, there were multiple
            // child FormProviders, so suck all of them out of the child and put
            // them in this one
            FormProvider[] myKids = ((AbstractMultigenitorForm)child).getFormProviders();
            for(int i = 0; i < myKids.length; i++) {
                parent.add(myKids[i].copyWithNewParent(parent));
            }
            parent.setAnnotation(followingNote);
            return parent;
        }
        return child;
    }

    private boolean isMulitgen(String tag) {
        return tag.equals("choice") || tag.equals("group")
                || tag.equals("interleave") || tag.equals("list");
    }

    /**
     * Method handleElement assumes that the reader has been advanced to a
     * START_ELEMENT with a local name of ref It returns a Ref object with the
     * name contained in the name value of the ref element and belonging to the
     * current Grammar. It advances the reader to the next tag past the
     * END_ELEMENT tag of the Ref. The parent is not set, so this must be
     * handled by the caller.
     */
    private Ref handleRef() throws XMLStreamException {
        String name = reader.getAttributeValue(0);
        nextTag();
        Ref ref = new Ref(definedGrammar, name);
        ref.setAnnotation(handleAnn());
        nextTag();
        return ref;
    }

    private Ref handleExtRef() throws XMLStreamException {
        String refGramLoc = getAbsPath();
        nextTag();
        Ref ref = new Ref(getGrammar(refGramLoc));
        ref.setAnnotation(handleAnn());
        nextTag();
        return ref;
    }

    private Form handleData() throws XMLStreamException {
        String tag = reader.getLocalName();
        Form result = null;
        if(tag.equals("notAllowed")) {
            result = new NotAllowed();
        } else if(tag.equals("empty")) {
            result = new Empty();
        } else if(tag.equals("text")) {
            result = new Text();
        } else if(tag.equals("value") || tag.equals("data")) {
            ModelDatatype type = handleType();
            if(tag.equals("data")) {
                result = new Data(1, 1, type);
            } else {
                reader.next();
                result = new Value(1, 1, reader.getText(), type);
            }
        } else if(tag.equals("list")) {
            FormProvider listInternals = handleAll();
            if(listInternals instanceof AbstractMultigenitorForm) {}
        }
        nextTag();
        result.setAnnotation(handleAnn());
        while(reader.getEventType() == START_ELEMENT
                && reader.getLocalName().equals("param")) {
            handleParam((Data)result);
        }
        nextTag();
        return result;
    }

    private void handleParam(Data result) throws XMLStreamException {
        // TODO handle params
        while(reader.getEventType() != END_ELEMENT) {
            reader.next();
        }
        reader.nextTag();
    }

    private boolean isData(String tag) {
        return tag.equals("empty") || tag.equals("data") || tag.equals("value")
                || tag.equals("text") || tag.equals("notAllowed");
    }

    private ModelDatatype handleType() {
        if(reader.getAttributeCount() > 0) {
            String type = reader.getAttributeValue(0);
            if(type.equals("float")) {
                return new FloatDatatype();
            } else if(type.equals("string")) {
                return new AnyText();
            } else if(type.equals("double")) {
                return new DoubleDatatype();
            } else if(type.equals("integer")) {
                return new IntegerDatatype();
            } else if(type.equals("nonNegativeInteger")) {
                return new NonnegativeIntegerDatatype();
            } else if(type.equals("boolean")) {
                return new BooleanDatatype();
            }
            return new Token();
        }
        return null;
    }

    private String getAbsPath() {
        String href = reader.getAttributeValue(0);
        String curLoc = definedGrammar.getLoc();
        try {
            return SodUtil.getAbsolutePath(curLoc, href);
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
    }

    public Form getRoot() {
        return definedGrammar.getRoot();
    }

    Grammar getGrammar(String loc) {
        if(!parsedGrammars.containsKey(loc)) {
            try {
                new StAXModelBuilder(loc);
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
        return parsedGrammars.get(loc);
    }

    public static Collection<Definition> getAllDefinitions() {
        Set<Definition> defs = new HashSet<Definition>();
        Iterator<Grammar> it = parsedGrammars.values().iterator();
        while(it.hasNext()) {
            Grammar cur = it.next();
            defs.addAll(cur.getDefs());
        }
        return defs;
    }

    private Map<String, String> waiters = new HashMap<String, String>();

    private XMLStreamReader reader;

    private Grammar definedGrammar;

    private static Map<String, Grammar> parsedGrammars = new HashMap<String, Grammar>();
}