

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


import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.template.SchemaResourceResolver;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.sql.Blob;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.xsd.XSDAttributeUse;
import org.eclipse.xsd.XSDComplexTypeDefinition;
import org.eclipse.xsd.XSDElementDeclaration;
import org.eclipse.xsd.XSDModelGroup;
import org.eclipse.xsd.XSDModelGroupDefinition;
import org.eclipse.xsd.XSDParticle;
import org.eclipse.xsd.XSDSchema;
import org.eclipse.xsd.XSDTypeDefinition;
import org.eclipse.xsd.util.XSDResourceFactoryImpl;
import org.eclipse.xsd.util.XSDResourceImpl;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;


@SuppressWarnings( "unchecked" )
public class SchemaHelper
    implements
        SchemaHelperInterface
{
    static
    {
        Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put( "xsd", new XSDResourceFactoryImpl() );
    }
    private Log logger = LogFactory.getLog( SchemaHelper.class );
    private Map<String, List<String>> elementMap = null;
    private Schema compiledSchema = null;
    private String schemaName = null;
    private SchemaResourceResolver schemaResourceResolver = null;
    private ArrayList<XSDSchema> xsdSchemasList = null;


    public SchemaHelper( Blob aSchemaJar, String aSchemaName )
        throws SchemaException
    {
        schemaName = aSchemaName;
        if ( null != aSchemaJar )
        {
            try
            {
                schemaResourceResolver = new SchemaResourceResolver( aSchemaJar );
                setupSymbolMap();
                createCompiledSchema();
            }
            catch ( Exception e )
            {
                throw new SchemaException( ErrorCodeEnum.UNABLE_TO_LOAD_SCHEMAS_INTO_RESOURCE, schemaName, e.getMessage() );
            }
        }
        else
        {
            throw new SchemaException( ErrorCodeEnum.MISSING_SCHEMA_JAR, schemaName );
        }

        if ( logger.isDebugEnabled() )
        {
            logger.debug( "Loaded compiledSchema " + schemaName );
        }
    }


    private void setupSymbolMap( )
        throws Exception
    {
        Map<String, InputSource> schemaInputSourceMap = null;
        
        try
        {
            elementMap = new HashMap<String, List<String>>();

            schemaInputSourceMap = schemaResourceResolver.getSchemaMap();

            Map<String, Object> options = new HashMap<String, Object>();
            options.put( XMLResource.OPTION_ENCODING, "UTF-8" );

            String activeSchemaName = null;
            XSDResourceImpl xsdResource = null;
            URI uri = null;
            InputSource inputSourceSchema = null;
            XSDSchema xsdSchema = null;
            xsdSchemasList = new ArrayList();

            for ( Map.Entry entry : schemaInputSourceMap.entrySet() )
            {
                inputSourceSchema = ( InputSource )entry.getValue();
                activeSchemaName = ( String )entry.getKey();
                uri = URI.createURI( activeSchemaName );
                xsdResource = new XSDResourceImpl();
                xsdResource.setURI( uri );
                xsdResource.load( inputSourceSchema, options );
                xsdSchema = xsdResource.getSchema();
                xsdSchemasList.add( xsdSchema );
                mapTemplateSchema( xsdSchema );
            }
        }
        finally
        {
            releaseSchemaInputSources( schemaInputSourceMap );
        }
    }


    private void releaseSchemaInputSources( Map<String, InputSource> aSchemaInputSourceMap )
    {
        InputSource inputSourceSchema = null;
        InputStream inputStream = null;
        Reader reader = null;

        for ( Map.Entry entry : aSchemaInputSourceMap.entrySet() )
        {
            inputSourceSchema = ( InputSource )entry.getValue();
            if ( null != inputSourceSchema )
            {
                inputStream = inputSourceSchema.getByteStream();
                reader = inputSourceSchema.getCharacterStream();
                if ( null != inputStream )
                {
                    try
                    {
                        inputStream.close();
                    }
                    catch ( IOException ioe )
                    {
                        if ( logger.isDebugEnabled() )
                        {
                            logger.debug( "Problem closing inputStream" );
                        }
                    }
                    finally
                    {
                        inputStream = null;
                    }
                }
                
                if ( null != reader )
                {
                    try
                    {
                        reader.close();
                    }
                    catch ( IOException ioe )
                    {
                        if ( logger.isDebugEnabled() )
                        {
                            logger.debug( "Problem closing reader" );
                        }
                    }
                    finally
                    {
                        reader = null;
                    }
                }
            }
        }        
    }


    private void createCompiledSchema( )
        throws Exception
    {
        String language = "http://www.w3.org/2001/XMLSchema";
        SchemaFactory factory = SchemaFactory.newInstance( language );

        LocalErrorHandler localErrorHandler = new LocalErrorHandler();
        factory.setErrorHandler( localErrorHandler );
        //Source[] schemaSourceArray = schemaResourceResolver.getSchemaSources();
        //compiledSchema = factory.newSchema( schemaSourceArray );

        factory.setResourceResolver( schemaResourceResolver );
        compiledSchema = factory.newSchema( schemaResourceResolver.getSchemaSource() );

        if ( localErrorHandler.parseException != null )
        {
            throw localErrorHandler.parseException;
        }
    }


    /**
     * 
     * @see gov.va.med.cds.xml.schema.SchemaHelperInterface#getSymbolMap()
     */
    public Map<String, List<String>> getSymbolMap( )
    {
        return elementMap;
    }


    /**
     * 
     * @see gov.va.med.cds.xml.schema.SchemaHelperInterface#getValidator()
     */
    public Validator getValidator( )
    {
        Validator validator = compiledSchema.newValidator();

        return validator;
    }


    /**
     * 
     * @see gov.va.med.cds.xml.schema.SchemaHelperInterface#getXsdSchemasList()
     */
    public ArrayList<XSDSchema> getXsdSchemasList( )
    {
        return xsdSchemasList;
    }


    private void mapTemplateSchema( XSDSchema aSchema )
    {
        List types = aSchema.getTypeDefinitions();
        for ( Object type : types )
        {
            XSDTypeDefinition typedef = ( XSDTypeDefinition )type;
            if ( typedef instanceof XSDComplexTypeDefinition )
            {
                String classKey = typedef.getTargetNamespace() + "." + typedef.getName();
                List<String> memberList = new ArrayList<String>();
                if ( logger.isDebugEnabled() )
                {
                    logger.debug( "adding " + classKey.toUpperCase() + " to the symbol map." );
                }
                elementMap.put( classKey.toUpperCase(), memberList );
                XSDComplexTypeDefinition cType = ( XSDComplexTypeDefinition )typedef;
                Object baseTypeObj = cType.getBaseType();
                if ( !cType.getDerivationMethod().getName().equals( "restriction" ) )
                {
                    processParent( baseTypeObj, memberList );
                }
                listAttributes( cType, memberList );
                listContents( cType, memberList );
            }
        }

        List groups = aSchema.getModelGroupDefinitions();
        for ( Object group : groups )
        {
            XSDModelGroupDefinition groupdef = ( XSDModelGroupDefinition )group;
            String classKey = groupdef.getTargetNamespace() + "." + groupdef.getName();
            List<String> memberList = new ArrayList<String>();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( "adding " + classKey.toUpperCase() + " to the symbol map." );
            }
            elementMap.put( classKey.toUpperCase(), memberList );
            XSDModelGroup modelGroup = groupdef.getModelGroup();
            listModelGroup( modelGroup, memberList );
        }
    }


    private void listContents( XSDComplexTypeDefinition aXSDComplexTypeDefinition, List aMemberList )
    {
        Object contentObj = aXSDComplexTypeDefinition.getContent();
        if ( ( contentObj != null ) && ( contentObj instanceof XSDParticle ) )
        {
            XSDParticle content = ( XSDParticle )contentObj;
            Object modContent = content.getContent();
            if ( modContent instanceof XSDModelGroup )
            {
                listModelGroup( modContent, aMemberList );
            }
        }
    }


    private void listModelGroup( Object aModelGroupObject, List aMemberList )
    {
        XSDModelGroup modelContent = ( XSDModelGroup )aModelGroupObject;
        List modelContents = modelContent.getParticles();
        for ( Object partObj : modelContents )
        {
            if ( partObj instanceof XSDParticle )
            {
                XSDParticle particle = ( XSDParticle )partObj;
                Object decObj = particle.getContent();
                if ( decObj instanceof XSDElementDeclaration )
                {
                    XSDElementDeclaration declaration = ( XSDElementDeclaration )decObj;
                    String declarationName = declaration.getName();
                    declarationName = StringUtils.remove( declarationName, "_" );
                    if ( declarationName != null )
                    {
                        if ( logger.isDebugEnabled() )
                        {
                            logger.debug( "adding " + declarationName + " to the symbol set." );
                        }

                        String declarationTypeName = declaration.getType().getName();
                        String declarationTypeTargetNamespace = declaration.getType().getTargetNamespace();

                        String elementType = ( declarationTypeName == null ) ? "" : declarationTypeName.toUpperCase();

                        if ( declarationTypeName != null && declarationTypeTargetNamespace != null
                                        && !declarationTypeTargetNamespace.toUpperCase().contains( "W3.ORG" ) )
                        {
                            elementType = declarationTypeTargetNamespace.toUpperCase() + "." + elementType;
                        }
                        aMemberList.add( aMemberList.size(), declarationName + "/" + elementType );
                    }
                    else
                    {
                        declarationName = declaration.getResolvedElementDeclaration().getName();
                        if ( logger.isDebugEnabled() )
                        {
                            logger.debug( "adding resolved name " + declarationName + " to the symbol set." );
                        }

                        String declarationTypeName = declaration.getResolvedElementDeclaration().getType().getName();
                        String declarationTypeTargetNamespace = declaration.getResolvedElementDeclaration().getType().getTargetNamespace();

                        String elementType = ( declarationTypeName == null ) ? "" : declarationTypeName.toUpperCase();

                        if ( declarationTypeName != null && declarationTypeTargetNamespace != null
                                        && !declarationTypeTargetNamespace.toUpperCase().contains( "W3.ORG" ) )
                        {
                            elementType = declarationTypeTargetNamespace.toUpperCase() + "." + elementType;
                        }
                        aMemberList.add( aMemberList.size(), declarationName + "/" + elementType );
                    }
                }
                else if ( decObj instanceof XSDModelGroup )
                {
                    listModelGroup( decObj, aMemberList );
                }
                else if ( decObj instanceof XSDModelGroupDefinition )
                {
                    listModelGroupDefinition( decObj, aMemberList );
                }
            }
        }
    }


    private void listModelGroupDefinition( Object aXSDModelGroupDefinition, List aMemberList )
    {
        XSDModelGroupDefinition modelGroupDefinition = ( XSDModelGroupDefinition )aXSDModelGroupDefinition;
        XSDModelGroupDefinition realGroupDefinition = modelGroupDefinition.getResolvedModelGroupDefinition();
        aMemberList.add( realGroupDefinition.getName().toUpperCase() );
    }


    private void listAttributes( XSDComplexTypeDefinition aXSDComplexTypeDefinition, List aMemberList )
    {
        String attributeName = null;
        List attributes = aXSDComplexTypeDefinition.getAttributeUses();
        for ( Object attrib : attributes )
        {
            XSDAttributeUse attribute = ( XSDAttributeUse )attrib;
            attributeName = attribute.getAttributeDeclaration().getName().toUpperCase();
            attributeName = StringUtils.remove( attributeName, "_" );
            aMemberList.add( attributeName );
        }
    }


    private void processParent( Object aBaseTypeObject, List aMemberList )
    {
        if ( ( aBaseTypeObject == null ) || !( aBaseTypeObject instanceof XSDComplexTypeDefinition ) )
        {
            return;
        }

        XSDComplexTypeDefinition type = ( XSDComplexTypeDefinition )aBaseTypeObject;
        if ( "anyType".equals( type.getName() ) )
        {
            return;
        }

        listAttributes( type, aMemberList );
        listContents( type, aMemberList );
        Object parent = type.getBaseType();
        processParent( parent, aMemberList );
    }


    public String toString( )
    {
        StringBuffer buffer = new StringBuffer();
        buffer.append( "Schema: " + schemaName + "\n" );
        if ( elementMap != null )
        {
            List<String> sortedKeys = new ArrayList<String>( elementMap.keySet() );
            Collections.sort( sortedKeys );
            for ( String key : sortedKeys )
            {
                List<String> sortedAttributes = new ArrayList<String>( elementMap.get( key ) );
                Collections.sort( sortedAttributes );
                buffer.append( "  " + key + ": " );
                boolean first = true;
                for ( String attribute : sortedAttributes )
                {
                    if ( first )
                    {
                        first = false;
                    }
                    else
                    {
                        buffer.append( ", " );
                    }
                    buffer.append( " " + attribute );
                }
                buffer.append( "\n" );
            }
        }

        return buffer.toString();
    }

    private class LocalErrorHandler
        extends
            DefaultHandler
        implements
            ErrorHandler
    {
        public SchemaValidationException parseException = null;


        public void error( SAXParseException e )
            throws SAXException
        {
            parseException = new SchemaValidationException( ErrorCodeEnum.ERROR_VALIDATIING_XML_AGAINST_COMPILED_SCHEMA, e, e.getMessage() );
        }


        public void fatalError( SAXParseException e )
            throws SAXException
        {
            parseException = new SchemaValidationException( ErrorCodeEnum.FATAL_ERROR_VALIDATIING_XML_AGAINST_COMPILED_SCHEMA, e, e.getMessage() );
        }


        public void warning( SAXParseException e )
            throws SAXException
        {
            if ( logger.isWarnEnabled() )
            {
                logger.warn( "A problem occurred when validating an xml instance against a compiledSchema", e );
            }
        }
    }
}
