/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netbeans.modules.j2ee.clientproject;


import java.io.IOException;
import java.net.URI;
import javax.lang.model.element.TypeElement;
import org.netbeans.api.j2ee.core.Profile;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.java.project.classpath.ProjectClassPathModifier;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ant.AntArtifact;
import org.netbeans.api.project.ant.AntArtifactQuery;
import org.netbeans.modules.j2ee.api.ejbjar.EjbReference;
import org.netbeans.modules.j2ee.api.ejbjar.EnterpriseReferenceContainer;
import org.netbeans.modules.j2ee.api.ejbjar.EnterpriseReferenceSupport;
import org.netbeans.modules.j2ee.api.ejbjar.MessageDestinationReference;
import org.netbeans.modules.j2ee.api.ejbjar.ResourceReference;
import org.netbeans.modules.javaee.injection.api.InjectionTargetQuery;
import org.netbeans.modules.j2ee.core.api.support.java.SourceUtils;
import org.netbeans.modules.j2ee.dd.api.client.AppClient;
import org.netbeans.modules.j2ee.dd.api.client.DDProvider;
import org.netbeans.modules.j2ee.dd.api.common.CommonDDBean;
import org.netbeans.modules.j2ee.dd.api.common.EjbRef;
import org.netbeans.modules.j2ee.dd.api.common.MessageDestinationRef;
import org.netbeans.modules.j2ee.dd.api.common.ResourceRef;
import org.netbeans.modules.j2ee.dd.api.common.VersionNotSupportedException;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.support.ant.ReferenceHelper;
import org.openide.filesystems.FileObject;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;

/**
 *
 * @author jungi
 */
public class JarContainerImpl implements EnterpriseReferenceContainer {
    
    private Project webProject;
    private AntProjectHelper antHelper;
    private static final String SERVICE_LOCATOR_PROPERTY = "project.serviceLocator.class"; //NOI18N
    private AppClient webApp;
    
    /** Creates a new instance of JarContainerImpl */
    public JarContainerImpl(Project p, ReferenceHelper helper, AntProjectHelper antHelper) {
        webProject = p;
        this.antHelper = antHelper;
    }
    
    /**
     * set name of service locator fo this project.
     *
     * @param serviceLocator used in this project
     */
    public void setServiceLocatorName(String serviceLocator) throws IOException {
        EditableProperties ep =
                antHelper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
        ep.setProperty(SERVICE_LOCATOR_PROPERTY, serviceLocator);
        antHelper.putProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH, ep);
        ProjectManager.getDefault().saveProject(webProject);
    }
    
    /**
     * Add given resource reference into the deployment descriptor.
     *
     * @param ref reference to resource used
     * @param referencingClass class which will use the resource
     * @return unique jndi name used in deployment descriptor
     */
    public String addResourceRef(ResourceReference ref, FileObject referencingFile, String referencingClass) throws IOException {
        String resourceRefName = ref.getResRefName();
        AppClient ac = getAppClient();
        // see if jdbc resource has already been used in the app
        // this change requested by Ludo
        if (javax.sql.DataSource.class.getName().equals(ref.getResType())) {
            ResourceRef[] refs = ac.getResourceRef();
            for (int i=0; i < refs.length; i++) {
                String newDefaultDescription = ref.getDefaultDescription();
                String existingDefaultDescription = refs[i].getDefaultDescription();
                boolean canCompareDefDesc = (newDefaultDescription != null && existingDefaultDescription != null);
                if (javax.sql.DataSource.class.getName().equals(refs[i].getResType()) &&
                        (canCompareDefDesc ? newDefaultDescription.equals(existingDefaultDescription) : true) &&
                        ref.getResRefName().equals(refs[i].getResRefName())) {
                    return refs[i].getResRefName();
                }
            }
        }
        if (!isResourceRefUsed(ac, ref)) {
            resourceRefName = getUniqueName(ac, "ResourceRef", "ResRefName", ref.getResRefName()); //NOI18N
            ResourceRef resourceRef = ac.newResourceRef();
            EnterpriseReferenceSupport.populate(ref, resourceRefName, resourceRef);
            getAppClient().addResourceRef(resourceRef);
            writeDD(referencingFile, referencingClass);
        }
        return resourceRefName;
    }
    
    /**
     *
     *
     * @see #addEjbReference(EjbRef, String, AntArtifact)
     */
    public String addEjbLocalReference(EjbReference localRef, EjbReference.EjbRefIType refType, String ejbRefName, FileObject referencingFile, String referencedClassName) throws IOException {
        throw new UnsupportedOperationException("Local references are not supported in App Client module.");
    }
    
    /**
     * Add given ejb reference into deployment descriptor. This method should
     * also ensure that the supplied target is added to the class path (as the
     * ejb interfaces will be referenced from this class) as well as the
     * deployed manifest. The deployed manifest is the generic J2EE compliant
     * strategy, application server specific behavior such as delegating to the
     * parent class loader could also be used. The main point is not to
     * include the target in the deployed archive but instead reference the
     * interface jar (or standard ejb module) included in the J2EE application.
     *
     * @param ref -- ejb reference this will include the ejb link which assumes
     * root packaging in the containing application. The name of this ref should
     * be considered a hint and made unique within the deployment descriptor.
     * @param referencedClassName -- name of referenced class, this can be used
     * to determine where to add the deployment descriptor entry. This class
     * will be modified with a method or other strategy to obtain the ejb.
     * @param target to include in the build
     * @return actual jndi name used in deployment descriptor
     */
    public String addEjbReference(EjbReference ref, EjbReference.EjbRefIType refType, String ejbRefName, FileObject referencingFile, String referenceClassName) throws IOException {
        return addReference(ref, refType, ejbRefName, referencingFile, referenceClassName);
    }
    
    /**
     * Add given message destination reference into the deployment descriptor
     *
     * @param ref to destination
     * @param referencingClass class using the destination
     * @return unique jndi name used in the deployment descriptor
     */
    public String addDestinationRef(MessageDestinationReference ref, FileObject referencingFile, String referencingClass) throws IOException {
        AppClient ac = getAppClient();
        String refName = getUniqueName(ac, "MessageDestinationRef", "MessageDestinationRefName", ref.getMessageDestinationRefName()); //NOI18N
        try {
            MessageDestinationRef messageDestinationRef = ac.newMessageDestinationRef();
            EnterpriseReferenceSupport.populate(ref, refName, messageDestinationRef);
            ac.addMessageDestinationRef(messageDestinationRef);
            writeDD(referencingFile, referencingClass);
        } catch (VersionNotSupportedException ex){}
        return refName;
    }
    
    /**
     *
     *
     * @return name of the service locator defined for this project or null
     * if service locator is not being used
     */
    public String getServiceLocatorName() {
        EditableProperties ep =
                antHelper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
        return ep.getProperty(SERVICE_LOCATOR_PROPERTY);
    }
    
    private AppClient getAppClient() throws IOException {
        if (webApp==null) {
            AppClientProvider jp = webProject.getLookup().lookup(AppClientProvider.class);
            FileObject fo = jp.getDeploymentDescriptor();
            webApp = DDProvider.getDefault().getDDRoot(fo);
        }
        return webApp;
    }
    
    private String getUniqueName(AppClient wa, String beanName,
            String property, String originalValue) {
        String proposedValue = originalValue;
        int index = 1;
        while (wa.findBeanByName(beanName, property, proposedValue) != null) {
            proposedValue = originalValue+Integer.toString(index++);
        }
        return proposedValue;
    }
    
    private void writeDD(FileObject referencingFile, final String referencingClassName) throws IOException {
        final AppClientProvider jp = webProject.getLookup().lookup(AppClientProvider.class);
        JavaSource javaSource = JavaSource.forFileObject(referencingFile);
        javaSource.runUserActionTask(new Task<CompilationController>() {
            public void run(CompilationController controller) throws Exception {
                TypeElement typeElement = controller.getElements().getTypeElement(referencingClassName);
                if (isDescriptorMandatory(jp.getJ2eeProfile()) ||
                        !InjectionTargetQuery.isInjectionTarget(controller, typeElement)) {
                    FileObject fo = jp.getDeploymentDescriptor();
                    getAppClient().write(fo);
                }
            }
        }, true);
    }
    
    private String addReference(EjbReference ejbReference, EjbReference.EjbRefIType refType, String ejbRefName, FileObject referencingFile, String referencingClass) throws IOException {
        String refName = ejbRefName;
        AppClient webApp = getAppClient();

        // don't duplicate ejbRefs with same name - issue #193576
        CommonDDBean bean = getAppClient().findBeanByName("EjbRef", "EjbRefName", ejbRefName); //NOI18N
        if (bean != null) {
            // when the name leads to the same bean
            if (bean.getValue(EjbRef.REMOTE).equals(ejbReference.getRemote())) {
                return ejbRefName;
            } else {
                refName = getUniqueName(getAppClient(), "EjbRef", "EjbRefName", ejbRefName); //NOI18N
            }
        }

        // EjbRef can come from Ejb project
        try {
            EjbRef newRef = (EjbRef)webApp.createBean("EjbRef"); //NOI18N
            newRef.setEjbRefName(refName);
            newRef.setEjbRefType(ejbReference.getEjbRefType());
            newRef.setHome(ejbReference.getRemoteHome());
            newRef.setRemote(ejbReference.getRemote());
            getAppClient().addEjbRef(newRef);
        } catch (ClassNotFoundException ex){}
        
        try {
            AntArtifact target = getAntArtifact(ejbReference, refType);
            ProjectClassPathModifier.addAntArtifacts(new AntArtifact[] {target}, new URI[] {BaseUtilities.normalizeURI(target.getArtifactLocations()[0])}, referencingFile, ClassPath.COMPILE);
        } catch (IOException ioe) {
            Exceptions.printStackTrace(ioe);
        }
        
        writeDD(referencingFile, referencingClass);
        return refName;
    }

    private static AntArtifact getAntArtifact(final EjbReference ejbReference, EjbReference.EjbRefIType refType) throws IOException {
        FileObject ejbReferenceEjbClassFO = SourceUtils.getFileObject(ejbReference.getComponentName(refType), ejbReference.getClasspathInfo());
        assert ejbReferenceEjbClassFO != null : "Reference FileObject not found: " + ejbReference.getComponentName(refType);
        Project otherPrj = FileOwnerQuery.getOwner(ejbReferenceEjbClassFO);

        Project project = FileOwnerQuery.getOwner(ejbReferenceEjbClassFO);
        AntArtifact[] antArtifacts = AntArtifactQuery.findArtifactsByType(project, JavaProjectConstants.ARTIFACT_TYPE_JAR);
        boolean hasArtifact = (antArtifacts != null && antArtifacts.length > 0);
        
        return hasArtifact ? antArtifacts[0] : null;
        
    }
    
    private static boolean isDescriptorMandatory(Profile j2eeVersion) {
        if (Profile.J2EE_13.equals(j2eeVersion) || Profile.J2EE_14.equals(j2eeVersion)) {
            return true;
        }
        return false;
    }
    
    /**
     * Searches for given resource reference in given client module.
     * Two resource references are considered equal if their names and types are equal.
     * 
     * @param ac client module where resource reference should be found
     * @param resRef resource reference to find
     * @return true id resource reference was found, false otherwise
     */
    private static boolean isResourceRefUsed(AppClient ac, ResourceReference resRef) {
        String resRefName = resRef.getResRefName();
        String resRefType = resRef.getResType();
        for (ResourceRef existingRef : ac.getResourceRef()) {
            if (resRefName.equals(existingRef.getResRefName()) && resRefType.equals(existingRef.getResType())) {
                return true;
            }
        }
        return false;
    }

}
