/*
 */

package au.gov.ansto.bragg.nexus.tool;
import au.gov.ansto.bragg.forward.JavaFactory_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Class_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.NullTypeException;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.PackageImport_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Package_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Parameter;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.ParameterDirectionKind;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Parameter_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Type;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Type_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.VisibilityKind;
import au.gov.ansto.bragg.nexus.base.NXdataItem;
import au.gov.ansto.bragg.nexus.base.NXobject;
import au.gov.ansto.bragg.nexus.base.NXtyped;
import au.gov.ansto.bragg.nexus.base.NXtypes;
import au.gov.ansto.bragg.forward.javamodel.Constructor;
import au.gov.ansto.bragg.forward.javamodel.Constructor_;
import au.gov.ansto.bragg.forward.javamodel.BeanProperty;
import au.gov.ansto.bragg.forward.javamodel.BeanProperty_;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Class;
import au.gov.ansto.bragg.forward.metamodel.classes.kernel.Package;
import java.util.Iterator;

import java.util.StringTokenizer;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;

/**
 *
 * @author drck
 * @version "%I%, %G%"
 */
public class NeXusFactory_
        extends JavaFactory_
        implements NeXusFactory {
    
    final static public String $String = "String";
    static final public String $type = "type";
    static final private String $description = "description";
    static final private String $distance = "distance";
    static final private String $name = "name";
    
    final static public String $NX = "NX";
    
    static final public String NX_GROUP = "NXgroup_";
    static final public String NX_DATAITEM = "NXdataItem_";
    
    static final public String $PACKAGE_BASE = "au.gov.ansto.bragg.nexus.base";
    static final public String $PACKAGE_NEUTRON_NEXUS = "neutron.nexus";
    
    static final public String $PACKAGE_JAVAX_PERSISTENCE = "javax.persistence";
    
    protected final boolean isNXGroup(String $className) {
        //return e.getName().startsWith(NX); //?FAILS
        return $className.startsWith($NX);
    }
    
    /** convert type strings like NX_TYPE[:] and NX_TYPE[2,:] to [:] and [2,:]
     */
    protected static String $extractArraySize(final String s) {
        return s.substring(s.indexOf('[',s.indexOf(']')));
    }
    
    protected static final String $stripNX(final String s) {
        return s.replaceFirst($NX,"");
    }
    
    /** convert type strings like NX_TYPE[:] and NX_TYPE[2,:] to NX_TYPE */
    protected static String $stripArraySize(final String s) {
        return s.substring(0,s.indexOf('['));
    }
    
    /** Detect type data fields like NX_FLOAT[2,:] */
    protected static boolean isArray(final String s) {
        return s.indexOf("[") > 0 && s.indexOf("]") > 0;
    }
    
    static protected String $extractType(Element e) {
        String type = null;
        for (Object o : e.attributes()) {
            Attribute att = (Attribute) o;
            if ($type == att.getName()) type = att.getValue();
        }
        return type;
    }
    
    protected static boolean isSpecialType(Element e) {
        String $name = e.getName();
        String $type = $extractType(e);
        
        for (String special: NXtypes.SPECIAL_TYPES) {
            if (special.equals($type)) return true;
        }
        return false;
    }
    
    /*
    private Project project_;
    protected Project project() {return project_;}
     
    public NeXusFactory_(Project project) {
        this.project_ = project;
    }
     */
    public NeXusFactory_() {}
    
    /**
     * @param args the command line arguments
     */
//    public static void main(String[] args) {
//        //Project project = new Project_("<$project>");
//        //NeXusFactory f = new NeXusFactory_(project);
//        NeXusFactory f = new NeXusFactory_();
//        //f.newNxClass();
//        //f.newSourceXml("<path1>/<path2>");
//        //f.ne
//    }
    
    /*
    public SourceXml newSourceXml(String $longName) {
        StringTokenizer st = new StringTokenizer($longName, "/");
        int n = st.countTokens();
        String $stem = "";
        for (int i = 0; i < n - 1; i++) {
            $stem += st.nextToken() +"/";
        }
        SourceXml source = new SourceXml_($stem,st.nextToken());
        return source;
    }
     */
    
    
    /** @inheritDoc */
    public Package newNeXusPackage(Project project) {
        
        Package neXusPackage = new Package_(
                project.get$package_out(),
                project.get$directory_out()
                );
        
        Package package_neutron_nexus = new Package_(
                $PACKAGE_NEUTRON_NEXUS
                );
        
        neXusPackage.addPackageImport(
                new PackageImport_(neXusPackage,
                package_neutron_nexus,
                VisibilityKind.PUBLIC
                ));
        
        Package package_nexus_base = new Package_($PACKAGE_BASE);
        
        neXusPackage.addPackageImport(
                new PackageImport_(
                neXusPackage,
                package_nexus_base,//TODO
                VisibilityKind.PUBLIC
                )
                );
        
        if (isHibernated()) {
            Package package_javax_persistence = new Package_($PACKAGE_JAVAX_PERSISTENCE);
            neXusPackage.addPackageImport(new PackageImport_(
                    neXusPackage,
                    package_javax_persistence,
                    VisibilityKind.PUBLIC
                    ));
        }
        return neXusPackage;
    }
    
    /** @inheritDoc
     */
    public Class newNeXusBean(
            String $url,
            org.dom4j.Document document,
            Package owningPackage
            ) {
        
        debug("Document.rootElement",document.getRootElement());
        
        String $name = document.getRootElement().getName();
        
        Class superClassNXgroup = new Class_(
                NX_GROUP,//as super class to NeXusBean
                null, //TODO ? owningPackage is hand-written base package
                null // TODO ? superclass of NXGroup is NXObject, never seen here !
                );
        
        //Class nxClass  = new Class_($name,owningPackage,classNXgroup);
        
        Class nxClass = new Class_(
                $name,
                owningPackage,
                superClassNXgroup
                );
        
        nxClass.addOwnedComment(newLineComment(" Generated from XML template("+$url+")"));
        //nxClass.addOwnedComment()
        
        //extract comments from document by scanning for dome4j Comment short type
        Iterator nodes = document.nodeIterator();
        while (nodes.hasNext()) {
            Node n = (Node)nodes.next();
            if (n.getNodeType()==Node.COMMENT_NODE) {
                nxClass.addOwnedComment(newJavadocComment(n.getText()));
            }
        }
        
        if (isHibernated()) {
            nxClass.addOwnedComment(newAnnotation("@Entity")); //TODO magic
            //NB: also requires then 'javax.persistence' !
        }
        
        Element root = document.getRootElement();
        try {
            Constructor constructor = new Constructor_(nxClass);
            nxClass.addOwnedOperation(constructor);
        } catch (NullTypeException ex) {
            error(ex); //TODO guard
        }
        
        // NeXus "Global Attributes" and "Data Attributes" extracted
        // from XML attributes as JavaProperty
        // CAUTION: this may cause compile clashes with DataItem base class !
        // IDEA: use Enums and filters to avoid clash //TODO
        
        extractDataAttributesAsJavaProperties(root,nxClass);
        
        Iterator kids = root.elementIterator();
        while (kids.hasNext()) {
            Element kid = (Element)kids.next();
            
            /*
            nxClass.addOwnedComment(
                    new Comment_(e.getName())
                    );
             */
            
            remapClashingInnerClassName(kid);
            
            String $kid = kid.getName();
            boolean isGroup = isNXGroup($kid);
            
            String $NXdoc = kid.getTextTrim();
            NeXusMultiplicityKind multiplicity = extractNeXusMultiplicity($NXdoc);
            
            boolean isRequired =  NeXusMultiplicityKind.N_NX_ONE_OR_MORE == multiplicity; //TODO  what about ONE_OR_ONE ? 
            
            String $type = isGroup ? $kid : $upperFirst($kid);
            Type type = new Type_($type,owningPackage);
            
            String $property = isGroup ? $lowerFirst($kid) : $kid;
            BeanProperty property;
            try {
                property = new BeanProperty_($property, type, multiplicity.isIndexed(), isRequired);// TODO isHibernated()
                property.addOwnedComment(newLineComment("Extracted NeXus multiplicity indicator ("+multiplicity+") means ("+multiplicity.getInfo()+")"));
                property.addOwnedComment(newJavadocComment($NXdoc));
                nxClass.addOwnedAttribute(property);
                
                if (!isGroup) {//DataItem: need new inner class
                    Class newDataItem = newDataItem(kid);
                    nxClass.addNestedClassifier(newDataItem);
                    //TODO ? how enforce inner class outputs qualified by outer class
                }
            } catch (NullTypeException ex) {
                error(ex);
            }
            
        }
        //debug("nexusbean",nxClass);
        
        return nxClass;
    }
    
    /*
    public UmlMultiplicityKind extractUmlMultiplicity(final String $NXdoc) {
    }
     */
    
    /** For resolving clashes between name attribute and inner class Name dataitems
     */
    static final private String REMAP_INNER_CLASS_NAME = "extraName";
    
    /** special case: "name" present in both anticipated NXGroup parent and
     * as additional "name" DataItem in some (?4) groups
     * ! remap and question need for extra names with NeXus working group
     * ! do it here in element for flowthru
     */
    private static void remapClashingInnerClassName(Element e) {
        //DEBUG("e.name before remap",e.getName());
        if (e.getName().equals($name)) e.setName(REMAP_INNER_CLASS_NAME);
        //DEBUG("e.name after remap",e.getName());
    }
    
    public Class newDataItem(Element e) {
        Package nullPackage = null;
        Class nxDataItem = new Class_(NX_DATAITEM,nullPackage); // as super class
        Class newDataItem = new Class_($upperFirst(e.getName()),nullPackage,nxDataItem);
        newDataItem.setStatic(true); // !
        extractDataAttributesAsJavaProperties(e,newDataItem);
        
        // Get DataItem type to strength constructor and content
        Attribute aType = e.attribute(NXdataItem.$type);
        
        String $NXtype = "Object";//default //TODO MAGIC
        if (aType != null) $NXtype = aType.getStringValue();        
        
        String $javaType = NXtypes.getJavaClassFor$NXtype($NXtype).getSimpleName();
        
        Constructor constructor_noargs;
        Constructor constructor_object;
        try {
            constructor_noargs = new Constructor_(newDataItem);
            newDataItem.addOwnedOperation(constructor_noargs);

           //! also include special constructor taking typed content
            
            constructor_object = new Constructor_(newDataItem);
            newDataItem.addOwnedOperation(constructor_object);
            
            Parameter content = new Parameter_(NXdataItem.$content, new Type_($javaType,null),ParameterDirectionKind.IN);
            constructor_object.addOwnedParameter(content);
            
            //BeanProperty content = new BeanProperty_(NXdataItem.$content);
            
        } catch (NullTypeException ex) {
            error(ex);
        }
        return newDataItem;
    }
    
    /** Currently blindly extracts all NeXus DataItem attributes as string properties,
     * except for clearly shared, inherited attributes like 'name' (for NXgroup), 
     * 'type' and 'units' (for NXdataItem).
     *
     *
     */
    //TODO assign values ?how
    //TODO assign stronger types ?how
    private void extractDataAttributesAsJavaProperties(
            final Element e,
            Class c) {
        Iterator atts = e.attributeIterator();
        while (atts.hasNext()) {
            Attribute att = (Attribute)atts.next();
            String name = att.getName();            
            //TODO encapsulate block inherited attributes as method
            if ( !(
                    NXobject.$name.equals(name) ||
                    NXdataItem.$type.equals(name) ||
                    NXdataItem.$units.equals(name)
                    )) {
                
                //att.getValue();
                Type type = new Type_($String,null); //TODO ? always as string
                BeanProperty javaProperty;
                try {
                    javaProperty = new BeanProperty_(att.getName(), type);
                    c.addOwnedAttribute(javaProperty);
                } catch (NullTypeException ex) {
                    error(ex);
                }
            }
            //property.//TODO default value
            //javaProperty.addOwnedComment(new Comment_(att.getText()));//TODO ?
        }
    }
    /*
    public Package newNxPackage(Project project) {
        project.$project();
        Package p = new Package_(
                project.$package_out(),
                project.$package_out()
                );
        return p;
    }
     */
    
    protected static NeXusMultiplicityKind extractNeXusMultiplicity(final String $NXdoc) {
        
        String $doc = $NXdoc.trim();
        int l = $doc.length();
        if (l==0) {
            WARN("no NX multiplicity indicator string found: zero length NeXus documentation !");
            return NeXusMultiplicityKind.N_NX_UNKNOWN;//? warn
        }
        
        if ($doc.endsWith(NeXusMultiplicityKind.N_NX_ONE_OR_MORE+""))
            return NeXusMultiplicityKind.N_NX_ONE_OR_MORE;
        
        if ($doc.endsWith(NeXusMultiplicityKind.N_NX_ZERO_OR_MORE+""))
            return NeXusMultiplicityKind.N_NX_ZERO_OR_MORE;
        
        if ($doc.endsWith(NeXusMultiplicityKind.N_NX_ZERO_OR_ONE+""))
            return NeXusMultiplicityKind.N_NX_ZERO_OR_ONE;
        
        WARN("no known NeXus multiplicity indicator found in NeXus documentation("+$doc+")");
        return NeXusMultiplicityKind.N_NX_UNKNOWN;//? warn
    }
    
}
