/*
 * Copyright 2005 JBoss Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.drools.guvnor.server.contenthandler;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Pattern;

import org.drools.guvnor.client.rpc.RuleAsset;
import org.drools.guvnor.server.builder.ClassLoaderBuilder;
import org.drools.guvnor.server.util.DroolsHeader;
import org.drools.repository.AssetItem;
import org.drools.repository.PackageItem;

import com.google.gwt.user.client.rpc.SerializationException;

/**
 * This is used for handling jar models for the rules.
 */
public class ModelContentHandler extends ContentHandler
    implements
    ICanHasAttachment {

    public void retrieveAssetContent(RuleAsset asset,
                                     AssetItem item) throws SerializationException {
        // do nothing, as we have an attachment
    }

    public void storeAssetContent(RuleAsset asset,
                                  AssetItem repoAsset) throws SerializationException {
        // do nothing, as we have an attachment
    }

    /**
     * This is called when a model jar is attached, it will peer into it, and
     * then automatically add imports if there aren't any already in the package
     * header configuration.
     */
    public void onAttachmentAdded(AssetItem asset) throws IOException {

        PackageItem pkg = asset.getPackage();
        StringBuilder header = createNewHeader( DroolsHeader.getDroolsHeader( pkg ) );

        Set<String> imports = getImportsFromJar( asset );

        for ( String importLine : imports ) {
            Pattern pattern = Pattern.compile( "\\b" + importLine.replace( ".",
                                                                           "\\." ) + "\\b" );
            if ( !pattern.matcher( header ).find() ) {
                header.append( importLine ).append( "\n" );
            }
        }

        DroolsHeader.updateDroolsHeader( header.toString(),
                                         pkg );
        pkg.checkin( "Imports setup automatically on model import." );

    }

    public void onAttachmentRemoved(AssetItem item) throws IOException {

        PackageItem pkg = item.getPackage();
        StringBuilder header = createNewHeader( DroolsHeader.getDroolsHeader( pkg ) );

        Set<String> imports = getImportsFromJar( item );

        for ( String importLine : imports ) {
            String importLineWithLineEnd = importLine + "\n";

            header = removeImportIfItExists( header,
                                             importLineWithLineEnd );
        }

        DroolsHeader.updateDroolsHeader( header.toString(),
                                                  pkg );

        pkg.checkin( "Imports removed automatically on model archiving." );

    }

    private StringBuilder removeImportIfItExists(StringBuilder header,
                                                 String importLine) {
        if ( header.indexOf( importLine ) >= 0 ) {
            int indexOfImportLine = header.indexOf( importLine );
            header = header.replace( indexOfImportLine,
                                     indexOfImportLine + importLine.length(),
                                     "" );
        }
        return header;
    }

    private StringBuilder createNewHeader(String header) {
        StringBuilder buf = new StringBuilder();

        if ( header != null ) {
            buf.append( header );
            buf.append( '\n' );
        }
        return buf;
    }

    private Set<String> getImportsFromJar(AssetItem assetItem) throws IOException {

        Set<String> imports = new HashSet<String>();
        Map<String, String> nonCollidingImports = new HashMap<String, String>();
        String assetPackageName = assetItem.getPackageName();

        //Setup class-loader to check for class visibility
        JarInputStream cljis = new JarInputStream( assetItem.getBinaryContentAttachment() );
        List<JarInputStream> jarInputStreams = new ArrayList<JarInputStream>();
        jarInputStreams.add( cljis );
        ClassLoaderBuilder clb = new ClassLoaderBuilder( jarInputStreams );
        ClassLoader cl = clb.buildClassLoader();

        //Reset stream to read classes
        JarInputStream jis = new JarInputStream( assetItem.getBinaryContentAttachment() );
        JarEntry entry = null;

        //Get Class names from JAR, only the first occurrence of a given Class leaf name will be inserted. Thus 
        //"org.apache.commons.lang.NumberUtils" will be imported but "org.apache.commons.lang.math.NumberUtils"
        //will not, assuming it follows later in the JAR structure.
        while ( (entry = jis.getNextJarEntry()) != null ) {
            if ( !entry.isDirectory() ) {
                if ( entry.getName().endsWith( ".class" ) && entry.getName().indexOf( '$' ) == -1 && !entry.getName().endsWith( "package-info.class" ) ) {
                    String fullyQualifiedName = convertPathToName( entry.getName() );
                    if ( isClassVisible( cl,
                                         fullyQualifiedName,
                                         assetPackageName ) ) {
                        String leafName = getLeafName( fullyQualifiedName );
                        if(!nonCollidingImports.containsKey( leafName )) {
                        nonCollidingImports.put( leafName,
                                                 fullyQualifiedName );
                        }
                    }
                }
            }
        }

        //Build list of imports
        for ( String value : nonCollidingImports.values() ) {
            String line = "import " + value;
            imports.add( line );
        }

        return imports;
    }

    private String getLeafName(String fullyQualifiedName) {
        int index = fullyQualifiedName.lastIndexOf( "." );
        if ( index == -1 ) {
            return fullyQualifiedName;
        }
        return fullyQualifiedName.substring( index + 1 );
    }

    //Only import public classes; or those in the same package as the Asset
    private boolean isClassVisible(ClassLoader cl,
                                   String className,
                                   String assetPackageName) {
        try {
            Class< ? > cls = cl.loadClass( className );
            int modifiers = cls.getModifiers();
            if ( Modifier.isPublic( modifiers ) ) {
                return true;
            }
            String packageName = className.substring( 0,
                                                      className.lastIndexOf( "." ) );
            if ( !packageName.equals( assetPackageName ) ) {
                return false;
            }
        } catch ( Exception e ) {
            return false;
        }
        return true;
    }

    public static String convertPathToName(String name) {
        return name.replace( ".class",
                             "" ).replace( "/",
                                           "." );
    }

}
