/*
 * 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.websvc.spi.jaxws.client;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import org.apache.tools.ant.module.api.support.ActionUtils;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.modules.websvc.api.jaxws.project.CatalogUtils;
import org.netbeans.modules.websvc.api.jaxws.project.JAXWSVersionProvider;
import org.netbeans.modules.websvc.api.jaxws.project.config.Client;
import org.netbeans.modules.websvc.api.jaxws.project.config.ClientAlreadyExistsExeption;
import org.netbeans.modules.websvc.api.jaxws.project.config.JaxWsModel;
import org.netbeans.modules.websvc.api.jaxws.project.WSUtils;
import org.netbeans.modules.websvc.api.jaxws.project.config.WsimportOption;
import org.netbeans.modules.websvc.api.jaxws.project.config.WsimportOptions;
import org.netbeans.modules.websvc.api.jaxws.wsdlmodel.WsdlModel;
import org.netbeans.modules.websvc.api.jaxws.wsdlmodel.WsdlModelListener;
import org.netbeans.modules.websvc.api.jaxws.wsdlmodel.WsdlModeler;
import org.netbeans.modules.websvc.api.jaxws.wsdlmodel.WsdlModelerFactory;
import org.netbeans.modules.websvc.api.jaxws.wsdlmodel.WsdlService;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.GeneratedFilesHelper;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.NotifyDescriptor;
import org.openide.execution.ExecutorTask;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.nodes.Node;
import org.openide.util.Mutex;
import org.openide.util.MutexException;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;

/**
 *
 * @author mkuchtiak
 */
public abstract class ProjectJAXWSClientSupport implements JAXWSClientSupportImpl {

    private static final String[] DEFAULT_WSIMPORT_OPTIONS = {"extension", "verbose", "fork"};  //NOI18N
    private static final String[] DEFAULT_WSIMPORT_VALUES = {"true", "true", "false"};  //NOI18N
    private static final String XNOCOMPILE_OPTION = "xnocompile"; //NOI18N
    private static final String XENDORSED_OPTION = "xendorsed"; //NOI18N
    private static final String PACKAGE_OPTION = "package"; //NOI18N
    private static final String WSDL_LOCATION_OPTION = "wsdlLocation"; //NOI18N
    private static final String TARGET_OPTION = "target"; //NOI18N
    protected static final String JAVA_EE_VERSION_NONE="java-ee-version-none"; //NOI18N
    protected static final String JAVA_EE_VERSION_15="java-ee-version-15"; //NOI18N
    protected static final String JAVA_EE_VERSION_16="java-ee-version-16"; //NOI18N
    protected static final String JAVA_EE_VERSION_17="java-ee-version-17"; //NOI18N
    protected static final String JAVA_EE_VERSION_18="java-ee-version-18";
    protected static final String JAKARTA_EE_VERSION_8="jakarta-ee-version-8"; //NOI18N
    protected static final String JAKARTA_EE_VERSION_9="jakarta-ee-version-9"; //NOI18N
    protected static final String JAKARTA_EE_VERSION_91="jakarta-ee-version-91"; //NOI18N
    protected static final String JAKARTA_EE_VERSION_10="jakarta-ee-version-10"; //NOI18N

    Project project;
    private AntProjectHelper helper;
    private FileObject clientArtifactsFolder;

    /** Creates a new instance of WebProjectJAXWSClientSupport */
    public ProjectJAXWSClientSupport(Project project, AntProjectHelper helper ) {
        this.project=project;
        this.helper = helper;
    }

    public void removeServiceClient(String serviceName) {
        JaxWsModel jaxWsModel = project.getLookup().lookup(JaxWsModel.class);
        if (jaxWsModel!=null && jaxWsModel.removeClient(serviceName)) {
            writeJaxWsModel(jaxWsModel);
        }
    }

    public AntProjectHelper getAntProjectHelper() {
        return helper;
    }

    public String getWsdlUrl(String serviceName) {
        JaxWsModel jaxWsModel = project.getLookup().lookup(JaxWsModel.class);
        if (jaxWsModel!=null) {
            Client client = jaxWsModel.findClientByName(serviceName);
            if (client!=null) return client.getWsdlUrl();
        }
        return null;
    }

    public String addServiceClient(String clientName, String wsdlUrl, String packageName, boolean isJsr109) {

        // create jax-ws.xml if necessary
        FileObject fo = WSUtils.findJaxWsFileObject(project);
        if (fo==null) {
            try {
                WSUtils.createJaxWsFileObject(project);
            } catch (IOException ex) {
                ErrorManager.getDefault().notify(ex);
            }
        }

        final JaxWsModel jaxWsModel = project.getLookup().lookup(JaxWsModel.class);
        String finalClientName=clientName;
        boolean clientAdded=false;
        if (jaxWsModel!=null) {

            if(!isJsr109){
                try{
                    addJaxWs20Library();
                } catch(Exception e){  //TODO handle this
                    ErrorManager.getDefault().notify(e);
                }
            }

            Client client=null;
            finalClientName = findProperClientName(clientName, jaxWsModel);
            FileObject xmlResourcesFo = getLocalWsdlFolderForClient(finalClientName,true);
            FileObject localWsdl=null;
            try {
                localWsdl = WSUtils.retrieveResource(
                        xmlResourcesFo,
                        new URI(wsdlUrl));
            } catch (URISyntaxException ex) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
                String mes = NbBundle.getMessage(ProjectJAXWSClientSupport.class, "ERR_IncorrectURI", wsdlUrl); // NOI18N
                NotifyDescriptor desc = new NotifyDescriptor.Message(mes, NotifyDescriptor.Message.ERROR_MESSAGE);
                DialogDisplayer.getDefault().notify(desc);
            } catch (UnknownHostException ex) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
                String mes = NbBundle.getMessage(ProjectJAXWSClientSupport.class, "ERR_UnknownHost", ex.getMessage()); // NOI18N
                NotifyDescriptor desc = new NotifyDescriptor.Message(mes, NotifyDescriptor.Message.ERROR_MESSAGE);
                DialogDisplayer.getDefault().notify(desc);
            } catch (IOException ex) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
                String mes = NbBundle.getMessage(ProjectJAXWSClientSupport.class, "ERR_WsdlRetrieverFailure", wsdlUrl); // NOI18N
                NotifyDescriptor desc = new NotifyDescriptor.Message(mes, NotifyDescriptor.Message.ERROR_MESSAGE);
                DialogDisplayer.getDefault().notify(desc);
            }

            if (localWsdl!=null) {
                try {
                    String localWsdlUrl = FileUtil.getRelativePath(xmlResourcesFo, localWsdl);
                    client = jaxWsModel.addClient(finalClientName, wsdlUrl, packageName);
                    client.setLocalWsdlFile(localWsdlUrl);
                    if (packageName == null) {
                        // compute package name from namespace
                        client.setPackageName(
                                WSUtils.getPackageNameForWsdl(FileUtil.toFile(localWsdl)));
                    }
                } catch (ClientAlreadyExistsExeption ex) {
                    //this shouldn't happen
                }

                FileObject catalog = getCatalogFileObject();
                if (catalog!=null) client.setCatalogFile(CATALOG_FILE);

                WsimportOptions wsimportOptions = client.getWsImportOptions();
                WsimportOption wsimportOption = null;
                if (wsimportOptions != null) {
                    int i=0;
                    for (String option:DEFAULT_WSIMPORT_OPTIONS) {
                        wsimportOption = wsimportOptions.newWsimportOption();
                        wsimportOption.setWsimportOptionName(option);
                        wsimportOption.setWsimportOptionValue(DEFAULT_WSIMPORT_VALUES[i++]); //NOI18N
                        wsimportOptions.addWsimportOption(wsimportOption);
                    }
                    wsimportOption = wsimportOptions.newWsimportOption();
                    wsimportOption.setWsimportOptionName(WSDL_LOCATION_OPTION);
                    wsimportOption.setWsimportOptionValue(wsdlUrl);
                    wsimportOptions.addWsimportOption(wsimportOption);
                    if (isXnocompile(project)) {
                        wsimportOption = wsimportOptions.newWsimportOption();
                        wsimportOption.setWsimportOptionName(XNOCOMPILE_OPTION);
                        wsimportOption.setWsimportOptionValue("true"); //NOI18N
                        wsimportOptions.addWsimportOption(wsimportOption);
                    }
                    if (isXendorsed(project)) {
                        wsimportOption = wsimportOptions.newWsimportOption();
                        wsimportOption.setWsimportOptionName(XENDORSED_OPTION);
                        wsimportOption.setWsimportOptionValue("true"); //NOI18N
                        wsimportOptions.addWsimportOption(wsimportOption);
                    }
                    if (packageName != null) {
                        wsimportOption = wsimportOptions.newWsimportOption();
                        wsimportOption.setWsimportOptionName(PACKAGE_OPTION);
                        wsimportOption.setWsimportOptionValue(packageName); //NOI18N
                        wsimportOptions.addWsimportOption(wsimportOption);
                    }
                    if (JAVA_EE_VERSION_15.equals(getProjectJavaEEVersion())) {
                        wsimportOption = wsimportOptions.newWsimportOption();
                        wsimportOption.setWsimportOptionName(TARGET_OPTION);
                        wsimportOption.setWsimportOptionValue("2.1"); //NOI18N
                        wsimportOptions.addWsimportOption(wsimportOption);
                    }
                }
                writeJaxWsModel(jaxWsModel);
                clientAdded=true;
                // get jax-ws-catalog.xml
                if (catalog != null) {
                    try {
                        FileObject webInfWsdl = getWsdlFolder(true);
                        if (webInfWsdl != null) {
                            FileObject jaxWsCatalog = webInfWsdl.getParent().getFileObject("jax-ws-catalog.xml");
                            if (jaxWsCatalog == null) {
                                jaxWsCatalog = FileUtil.copyFile(catalog, webInfWsdl.getParent(), "jax-ws-catalog"); //NOI18N
                                // update system elements in jax-ws-catalog.xml
                                CatalogUtils.updateCatalogEntriesForClient(jaxWsCatalog, clientName);
                            } else {
                                // copy, and modify catalog entries from catalog.xml to jax-ws-catalog.xml
                                CatalogUtils.copyCatalogEntriesForClient(catalog, jaxWsCatalog, clientName);
                            }
                            // copy files
                            WSUtils.copyFiles(xmlResourcesFo, webInfWsdl);
                        }

                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }

                // generate wsdl model immediately
                final String clientName2 = finalClientName;
                final WsdlModeler modeler = WsdlModelerFactory.getDefault().getWsdlModeler(localWsdl.toURL());

                if (modeler!=null) {
                    modeler.setPackageName(packageName);
                    if (catalog != null) {
                        modeler.setCatalog(catalog.toURL());
                    }
                    modeler.generateWsdlModel(new WsdlModelListener() {
                        public void modelCreated(WsdlModel model) {
                            if (model==null) {
                                RequestProcessor.getDefault().post(new Runnable() {
                                    public void run() {
                                        DialogDisplayer.getDefault().notify(new WsImportFailedMessage(modeler.getCreationException()));
                                    }
                                });
                            } else {
                                Client client = jaxWsModel.findClientByName(clientName2);
                                String packName = client.getPackageName();
                                // this shuldn't normally happen
                                // this applies only for case when package name cannot be resolved for namespace
                                if (packName == null) {
                                    if (model.getServices().size() > 0) {
                                        WsdlService service = model.getServices().get(0);
                                        String javaName = service.getJavaName();
                                        int index = javaName.lastIndexOf(".");
                                        if (index != -1){
                                            packName = javaName.substring(0,index );
                                        } else {
                                            packName = javaName;
                                        }
                                        client.setPackageName(packName);
                                        writeJaxWsModel(jaxWsModel);
                                    }
                                }
                                runWsimport(clientName2);
                            }
                        }
                    });
                }
            }
            return finalClientName;
        }
        return null;
    }

    private void runWsimport(String finalClientName){
        final FileObject buildImplFo = project.getProjectDirectory().getFileObject(GeneratedFilesHelper.BUILD_XML_PATH);
        final String finalName = finalClientName;

        if (SwingUtilities.isEventDispatchThread()) {
            openOutputWindow();
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    openOutputWindow();
                }
            });
        }

        try {
            ProjectManager.mutex().readAccess(new Mutex.ExceptionAction<Boolean>() {
                public Boolean run() throws IOException {
                    Properties props = WSUtils.identifyWsimport(helper);
                    ExecutorTask wsimportTask =
                            ActionUtils.runTarget(buildImplFo,new String[]{"wsimport-client-"+finalName},props); //NOI18N
                    return Boolean.TRUE;
                }
            });
        } catch (MutexException e) {
            ErrorManager.getDefault().notify(e);
        }
    }

    private void openOutputWindow() {
        TopComponent outputTc = WindowManager.getDefault().findTopComponent("output"); //NOI18N
        if (outputTc != null) {
            outputTc.open();
        }
    }

    private String findProperClientName(String name, JaxWsModel jaxWsModel) {
        String firstName=name.length()==0?NbBundle.getMessage(ProjectJAXWSClientSupport.class,"LBL_defaultClientName"):name;
        if (jaxWsModel.findClientByName(firstName)==null) return firstName;
        for (int i = 1;; i++) {
            String finalName = firstName + "_" + i; // NOI18N
            if (jaxWsModel.findClientByName(finalName)==null)
                return finalName;
        }
    }

    private void writeJaxWsModel(JaxWsModel jaxWsModel) {
        try {
            jaxWsModel.write();
        } catch (IOException ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.FINE, "failed to save jax-ws.xml", ex); //NOI18N
        }
    }

    public List<Client> getServiceClients() {
        List<Client> jaxWsClients = new ArrayList<>();
        JaxWsModel jaxWsModel = project.getLookup().lookup(JaxWsModel.class);
        if (jaxWsModel!=null) {
            Client[] clients = jaxWsModel.getClients();
            for (int i=0;i<clients.length;i++) jaxWsClients.add(clients[i]);
        }
        return jaxWsClients;
    }

    /**
     *  return root folder for wsdl artifacts
     */
    public FileObject getLocalWsdlFolderForClient(String clientName, boolean createFolder) {
        return getArtifactsFolder(clientName, createFolder, true);
    }

    /**
     *  return folder for local wsdl bindings
     */
    public FileObject getBindingsFolderForClient(String clientName, boolean createFolder) {
        return getArtifactsFolder(clientName, createFolder, false);
    }

    private FileObject getArtifactsFolder(String clientName, boolean createFolder, boolean forWsdl) {
        String folderName = forWsdl?"wsdl":"bindings"; //NOI18N
        FileObject root = getXmlArtifactsRoot();
        assert root!=null;
        FileObject wsdlLocalFolder = root.getFileObject(XML_RESOURCES_FOLDER+"/"+CLIENTS_LOCAL_FOLDER+"/"+clientName+"/"+folderName); //NOI18N
        if (wsdlLocalFolder==null && createFolder) {
            try {
                FileObject xmlLocalFolder = root.getFileObject(XML_RESOURCES_FOLDER);
                if (xmlLocalFolder==null) xmlLocalFolder = root.createFolder(XML_RESOURCES_FOLDER);
                FileObject servicesLocalFolder = xmlLocalFolder.getFileObject(CLIENTS_LOCAL_FOLDER);
                if (servicesLocalFolder==null) servicesLocalFolder = xmlLocalFolder.createFolder(CLIENTS_LOCAL_FOLDER);
                FileObject serviceLocalFolder = servicesLocalFolder.getFileObject(clientName);
                if (serviceLocalFolder==null) serviceLocalFolder = servicesLocalFolder.createFolder(clientName);
                wsdlLocalFolder=serviceLocalFolder.getFileObject(folderName);
                if (wsdlLocalFolder==null) wsdlLocalFolder = serviceLocalFolder.createFolder(folderName);
            } catch (IOException ex) {
                return null;
            }
        }
        return wsdlLocalFolder;
    }

    /** return root folder for xml artifacts
     */
    protected FileObject getXmlArtifactsRoot() {
        return project.getProjectDirectory();
    }

    private FileObject getCatalogFileObject() {
        return project.getProjectDirectory().getFileObject(CATALOG_FILE);
    }

    public URL getCatalog() {
        FileObject catalog = getCatalogFileObject();
        return ((catalog==null) ? null : catalog.toURL());
    }

    protected abstract void addJaxWs20Library() throws Exception;

    public abstract FileObject getWsdlFolder(boolean create) throws IOException;

    public String getServiceRefName(Node clientNode) {
        WsdlService service = clientNode.getLookup().lookup(WsdlService.class);
        String serviceName = service.getName();
        return "service/" + serviceName;
    }

    private class WsImportFailedMessage extends NotifyDescriptor.Message {
        public WsImportFailedMessage(Throwable ex) {
            super(NbBundle.getMessage(ProjectJAXWSClientSupport.class,"TXT_CannotGenerateClient",ex.getLocalizedMessage()),
                    NotifyDescriptor.ERROR_MESSAGE);
        }

    }

    /** folder where xml client artifacts should be saved, e.g. WEB-INF/wsdl/client/SampleClient
     */
    protected FileObject getWsdlFolderForClient(String name) throws IOException {
        FileObject globalWsdlFolder = getWsdlFolder(true);
        FileObject oldWsdlFolder = globalWsdlFolder.getFileObject("client/"+name); //NOI18N
        if (oldWsdlFolder!=null) {
            FileLock lock = oldWsdlFolder.lock();
            try {
                oldWsdlFolder.delete(lock);
            } finally {
                lock.releaseLock();
            }
        }
        FileObject clientWsdlFolder = globalWsdlFolder.getFileObject("client"); //NOI18N
        if (clientWsdlFolder==null) clientWsdlFolder = globalWsdlFolder.createFolder("client"); //NOI18N
        return clientWsdlFolder.createFolder(name);
    }

    private static boolean isXnocompile(Project project){
        JAXWSVersionProvider jvp = project.getLookup().lookup(JAXWSVersionProvider.class);
        if (jvp != null) {
            String version = jvp.getJAXWSVersion();
            if (version != null) {
                return isVersionSatisfied(version, "2.1.3");
            }
        }
        // Defaultly return true
        return true;
    }

    private static boolean isXendorsed(Project project){
        JAXWSVersionProvider jvp = project.getLookup().lookup(JAXWSVersionProvider.class);
        if (jvp != null) {
            String version = jvp.getJAXWSVersion();
            if (version != null) {
                return isVersionSatisfied(version, "2.1.1"); //NOI18N
            }
        }
        // Defaultly return false
        return false;
    }

    private static boolean isVersionSatisfied(String version, String requiredVersion) {
        int len1 = version.length();
        int len2 = requiredVersion.length();
        for (int i=0;i<Math.min(len1, len2);i++) {
            if (version.charAt(i) < requiredVersion.charAt(i)) {
                return false;
            } else if (version.charAt(i) > requiredVersion.charAt(i)) {
                return true;
            }
        }
        if (len1 > len2) return true;
        else if (len1 < len2) return false;
        return true;
    }

    protected String getProjectJavaEEVersion() {
        return JAVA_EE_VERSION_NONE;
    }

}
