/**
 * 
 */


package gov.va.med.cds.testharness.xml;



import gov.va.med.cds.testharness.util.TemplateJarHelper;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.dom4j.io.SAXReader;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;


/**
 * @author vhaislegberb
 *
 */
public class SchemaHelper
{

    private static final Namespace XSD_NAMESPACE = Namespace.get( "xsd", "http://www.w3.org/2001/XMLSchema" );

    private static final QName XSD_ELEMENT = QName.get( "element", XSD_NAMESPACE );

    private static final QName XSD_SIMPLETYPE = QName.get( "simpleType", XSD_NAMESPACE );

    private static final QName XSD_COMPLEXTYPE = QName.get( "complexType", XSD_NAMESPACE );

    private static final QName XSD_SEQUENCE = QName.get( "sequence", XSD_NAMESPACE );
    
    private static final QName XSD_ALL = QName.get( "all", XSD_NAMESPACE );

    private static final QName XSD_IMPORT = QName.get( "import", XSD_NAMESPACE );
    
    private static final QName XSD_EXTENSION = QName.get( "extension", XSD_NAMESPACE );
    
    private static final QName XSD_COMPLEXCONTENT = QName.get( "complexContent", XSD_NAMESPACE );

    private Document schemaDocument;
    
    private List<String> imported = new ArrayList<String>();


    public static SchemaHelper createSchemaHelper( String systemId )
        throws SchemaHelperException
    {
        SchemaHelper schemaHelper = new SchemaHelper( systemId );
        return schemaHelper;
    }
    
    
    public static SchemaHelper createSchemaHelper( String templateCacheDir, String templateId)
        throws SchemaHelperException
    {
        try
        {
            TemplateJarHelper.unjar(templateCacheDir, templateId);
            String systemId = String.format( "%s/%s/template/%s.xsd", templateCacheDir, templateId, templateId );
            SchemaHelper schemaHelper = new SchemaHelper( systemId );
            return schemaHelper;
        }
        catch ( Exception e )
        {
            throw new SchemaHelperException( "Error creating schema helper.", e );
        }
    }


    private SchemaHelper( String systemId )
        throws SchemaHelperException
    {
        try
        {
            SAXReader reader = new SAXReader();
            this.schemaDocument = reader.read( systemId );
            internalBuild( this.schemaDocument );
        }
        catch ( DocumentException e )
        {
            throw new SchemaHelperException("An error occurred reading the document.", e);
        }
    }
    
    private Element elementToComplexTypeResolution(Element xsdElement )
    {
        String type = xsdElement.attributeValue( "type" ).split( ":" )[1];
        return (Element)this.schemaDocument.getRootElement().selectSingleNode(String.format("xsd:complexType[@name='%s']", type));
    }
    
    private Element locateXsdElement( Element parent, String elementName )
    {
        Element element = (Element)parent.selectSingleNode( String.format("xsd:element[@name='%s'] | xsd:choice/xsd:element[@name='%s']", elementName, elementName) );
        if(isComplexType(element))
        {
            element = elementToComplexTypeResolution( element );
        }
        
        return element;
        
    }
    
    private boolean isComplexType(Element element)
    {
        String type = null;
        
        if( element != null )
        {
            type = element.attributeValue( "type" );
        }
        return ( element != null && type != null && !type.startsWith( "xsd:" ));
    }


    public boolean isPathValidForSchema( String path )
    {
        // see the path validator with the root node.
        String[] split = splitPath( path );
        Element clinicalDataComplexType = locateXsdElement( this.schemaDocument.getRootElement(), split[0] );
        
        if(clinicalDataComplexType == null)
        {
            return false;
        }
        else if (clinicalDataComplexType != null && split[1] == null)
        {
            return true;
        }
       
        
        return isPathValidForSchema( clinicalDataComplexType, split[1] );
    }
    
    //Used in tools/vhim-artifacts-validation project
    public Document getSchemaDocument( )
    {
        return schemaDocument;
    }


    @SuppressWarnings( "unchecked" )
    private boolean isPathValidForSchema( Element current, String path)
    {
        Element newCurrent = null;
        String[] split = splitPath( path );
        
        List children = current.elements();
        Element e = (Element)children.get( 0 );
        String elementName = e.getName();
        
        if ( children.size() == 1 && !XSD_ELEMENT.getName().equals( elementName ) )
        {
            if(XSD_COMPLEXTYPE.getName().equals(elementName) || XSD_SEQUENCE.getName().equals( elementName ) || XSD_ALL.getName().equals( elementName )  || XSD_COMPLEXCONTENT.getName().equals( elementName ))
            {
                return isPathValidForSchema(e, path);
            }
            else if(XSD_EXTENSION.getName().equals( elementName ))
            {
                Element rootElement = this.schemaDocument.getRootElement();
                String  typeName = e.attributeValue( "base" ).split( ":" )[1];
                
                Element extension = (Element)rootElement.selectSingleNode( String.format( "xsd:complexType[@name='%s']", typeName ) );
                return isPathValidForSchema( e, path ) || 
                       isPathValidForSchema( extension, path );
            }
        }
        else 
        {
            newCurrent = locateXsdElement(current, split[0]);
        }
    
        if(newCurrent != null && split[1] != null)
        {
            // there's still more of the path to parse, not done yet!
            return isPathValidForSchema( newCurrent, split[1] );
        }
        
        return (newCurrent != null);
    }


    private String[] splitPath(String path)
    {
        String[] split = new String[2];
        
        if(path.startsWith( "/" ))
        {
            path = path.substring( 1 );
        }
        
        int slashIndex = path.indexOf( '/' );
        if(slashIndex != -1)
        {
            split[0] = path.substring( 0, slashIndex );
            split[1] = path.substring( slashIndex+1 );
        }
        else 
        {
            split[0] = path;
        }
        

        int nsSeparatorIndex = split[0].indexOf( ':' ); 
        split[0] = (nsSeparatorIndex != -1 ) ? split[0].substring( nsSeparatorIndex + 1 ) : split[0]; 
        
        return split;
    }
    

    @SuppressWarnings( "unchecked" )
    private void internalBuild( Document schemaDocument ) throws SchemaHelperException
    {
        Element root = schemaDocument.getRootElement();

        if ( root != null )
        {
            // handle schema includes
            Iterator includeIter = root.elementIterator( XSD_IMPORT );

            while ( includeIter.hasNext() )
            {
                Element includeElement = ( Element )includeIter.next();
                String inclSchemaInstanceURI = includeElement.attributeValue( "schemaLocation" );
                
                if(!isImported(inclSchemaInstanceURI))
                {
                    
                    EntityResolver resolver = schemaDocument.getEntityResolver();
    
                    try
                    {
                        if ( resolver == null )
                        {
                            String msg = "No EntityResolver available";
                            throw new SchemaHelperException( msg );
                        }
    
                        InputSource inputSource = resolver.resolveEntity( null, inclSchemaInstanceURI );
    
                        if ( inputSource == null )
                        {
                            String msg = "Could not resolve the schema URI: " + inclSchemaInstanceURI;
                            throw new SchemaHelperException( msg );
                        }
    
                        SAXReader reader = new SAXReader();
                        Document inclSchemaDocument = reader.read( inputSource );
                        internalBuild( inclSchemaDocument );
                    }
                    catch ( Exception e )
                    {
                        e.printStackTrace();
                        throw new SchemaHelperException( "Failed to load schema: " + inclSchemaInstanceURI );
                    }
                }
                
                includeElement.detach();
            }
            
         
            Iterator iter = root.elementIterator(XSD_ELEMENT);

            while (iter.hasNext()) {
                this.schemaDocument.getRootElement().add( ((Element)iter.next()).detach() );
            }

            // handle named simple types
            iter = root.elementIterator(XSD_SIMPLETYPE);

            while (iter.hasNext()) {
                this.schemaDocument.getRootElement().add( ((Element)iter.next()).detach() );
            }

            // hanlde named complex types
            iter = root.elementIterator(XSD_COMPLEXTYPE);

            while (iter.hasNext()) {
                this.schemaDocument.getRootElement().add( ((Element)iter.next()).detach() );
            }
        }
    }
    

    private boolean isImported(String schemaURI)
    {
        int slashIndex = schemaURI.lastIndexOf( '/' );
        schemaURI = (slashIndex == -1) ? schemaURI : schemaURI.substring( slashIndex+1 );
        
        if(!this.imported.contains( schemaURI ))
        {
            this.imported.add( schemaURI );
            return false;
        }
        
        return true;
    }
    
}