/*
 * 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.rest.codegen;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.modules.j2ee.deployment.devmodules.api.Deployment;
import org.netbeans.modules.j2ee.deployment.devmodules.api.InstanceRemovedException;
import org.netbeans.modules.j2ee.deployment.devmodules.api.ServerInstance;
import org.netbeans.modules.j2ee.deployment.devmodules.api.ServerInstance.Descriptor;
import org.netbeans.modules.j2ee.deployment.devmodules.spi.J2eeModuleProvider;
import org.netbeans.modules.web.api.webmodule.WebModule;
import org.netbeans.modules.websvc.rest.RestUtils;
import org.openide.filesystems.FileUtil;
import org.netbeans.modules.websvc.rest.codegen.model.ClientStubModel;
import org.netbeans.modules.websvc.rest.codegen.model.Resource;
import org.netbeans.modules.websvc.rest.codegen.model.ResourceModel;
import org.netbeans.modules.websvc.rest.codegen.model.WadlModeler;
import org.netbeans.modules.websvc.rest.codegen.model.ClientStubModel.*;
import org.netbeans.modules.websvc.rest.spi.MiscUtilities;
import org.netbeans.modules.websvc.rest.spi.RestSupport;
import org.openide.filesystems.FileSystem;
import org.openide.loaders.DataObject;
import org.openide.util.NbBundle;

/**
 * Code generator for plain REST resource class.
 * The generator takes as paramenters:
 *  - target directory
 *  - REST resource bean meta getModel().
 *
 * @author Ayub Khan
 * @author (rewritten by) ads
 */
public class ClientStubsGenerator extends AbstractGenerator {

    public static final String REST = "rest"; //NOI18N
    public static final String RJS = "rjs"; //NOI18N
    public static final String CSS = "css"; //NOI18N
    public static final String JS = "js"; //NOI18N
    public static final String HTML = "html"; //NOI18N
    public static final String HTM = "htm"; //NOI18N
    public static final String TXT = "txt"; //NOI18N
    public static final String JSON = "json"; //NOI18N
    public static final String GIF = "gif"; //NOI18N
    public static final String IMAGES = "images"; //NOI18N
    public static final String BUNDLE = "Bundle"; //NOI18N
    public static final String PROPERTIES = "properties"; //NOI18N

    public static final String JS_SUPPORT = "Support"; //NOI18N
    public static final String JS_TESTSTUBS = "TestStubs"; //NOI18N
    public static final String JS_README = "Readme"; //NOI18N
    public static final String JS_TESTSTUBS_TEMPLATE = "Templates/WebServices/JsTestStubs.html"; //NOI18N
    public static final String JS_STUBSUPPORT_TEMPLATE = "Templates/WebServices/JsStubSupport.js"; //NOI18N
    public static final String JS_PROJECTSTUB_TEMPLATE = "Templates/WebServices/JsProjectStub.js"; //NOI18N
    public static final String JS_STUB_TEMPLATE = "Templates/WebServices/JsStub.js"; //NOI18N
    public static final String JS_ENTITY_TEMPLATE = "Templates/WebServices/JsEntity.js"; //NOI18N
    public static final String JS_README_TEMPLATE = "Templates/WebServices/JsReadme.html"; //NOI18N

    public static final String PROXY = "RestProxyServlet"; //NOI18N
    public static final String PROXY_URL = "/restproxy";   //NOI18N
    public static final String PROXY_TEMPLATE = "Templates/WebServices/RestProxyServlet.txt"; //NOI18N

    public static final String MSG_Readme = "MSG_Readme";               //NOI18N
    public static final String README_VAR ="msg_readme";             //NOI18N
    public static final String MSG_TestPage = "MSG_TestPage";           //NOI18N
    public static final String TESTPAGE_VAR = "test_page";           //NOI18N
    public static final String TTL_RestClient_Stubs = "TTL_RestClient_Stubs";//NOI18N
    public static final String RESTCLIENT_STABS_VAR="rest_client_stubs";//NOI18N
    public static final String MSG_JS_Readme_Content = "MSG_JS_Readme_Content";//NOI18N
    public static final String README_CONTENT_VAR = "readme_content";       //NOI18N 

    public static final String DEFAULT_PROTOCOL = "http";               //NOI18N
    public static final String DEFAULT_HOST = "localhost";              //NOI18N
    public static final String DEFAULT_PORT = "8080";                   //NOI18N
    public static final String DEFAULT_BASE_URL = DEFAULT_PROTOCOL+"://"+DEFAULT_HOST+":"+DEFAULT_PORT;
    public static final String BASE_URL_TOKEN = "base_url";             //NOI18N

    private FileObject stubFolder;
    private Project p;
    private boolean overwrite;
    private String projectName;
    private ResourceModel model;
    private FileObject rjsDir;
    private FileObject wadl;
    private String folderName;
    private String baseUrl;
    private String proxyUrl;
    private Charset baseEncoding;
    private FileObject rootFolder;

    public ClientStubsGenerator(FileObject rootFolder, String folderName, Project p,
            boolean overwrite) throws IOException {
        assert p != null;
        this.rootFolder = rootFolder;
        this.folderName = folderName;
        this.p = p;
        this.overwrite = overwrite;
        this.projectName = ProjectUtils.getInformation(getProject()).getName();
        this.baseEncoding = FileEncodingQuery.getEncoding(rootFolder);
    }

    public ClientStubsGenerator(FileObject rootFolder, String folderName, FileObject wadl,
            boolean overwrite) throws IOException {
        this.rootFolder = rootFolder;
        this.folderName = folderName;
        this.wadl = wadl;
        this.overwrite = overwrite;
        this.projectName = "NewProject";
        this.baseEncoding = FileEncodingQuery.getEncoding(rootFolder);
    }

    public FileObject getRootFolder() {
        return rootFolder;
    }

    public FileObject getStubFolder() {
        if(stubFolder == null) {
            try {
                stubFolder = createFolder(getRootFolder(), getFolderName());
            } catch (IOException ex) {
            }
        }
        return stubFolder;
    }

    public void setStubFolder(FileObject stubFolder) {
        this.stubFolder = stubFolder;
    }

    public String getFolderName() {
        return folderName;
    }

    public Project getProject() {
        return p;
    }

    public boolean canOverwrite() {
        return overwrite;
    }

    public String getProjectName() {
        return projectName;
    }

    public ResourceModel getModel() {
        return model;
    }

    public String getDefaultBaseUrl() {
        return DEFAULT_BASE_URL;
    }

    public String getBaseUrl() {
        return baseUrl;
    }

    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    public String getProxyUrl() {
        return proxyUrl;
    }

    public void setProxyUrl(String proxyUrl) {
        this.proxyUrl = proxyUrl;
    }

    public Charset getBaseEncoding() {
        return baseEncoding;
    }

    private String getApplicationNameFromUrl(String url) {
        String appName = url.replaceAll(DEFAULT_PROTOCOL+"://", "");
        if(appName.endsWith("/"))
            appName = appName.substring(0, appName.length()-1);
        String[] paths = appName.split("/");
        if(paths != null && paths.length > 0) {
            for(int i=0;i<paths.length;i++) {
                String path = paths[i];
                if(path != null && path.startsWith(DEFAULT_HOST) &&
                        i+1 < paths.length && paths[i+1] != null &&
                        paths[i+1].trim().length() > 0) {
                    return ClientStubModel.normalizeName(paths[i+1]);
                }
            }
        }
        return ClientStubModel.normalizeName(appName);
    }

    private String findBaseUrl(Project p) {
        String url = null;
        J2eeModuleProvider provider = p.getLookup().lookup(J2eeModuleProvider.class);
        if (provider != null) {
            String sID = provider.getServerInstanceID();
            ServerInstance si = Deployment.getDefault().getServerInstance(sID);
            if (si != null) {
                try {
                    Descriptor descriptor = si.getDescriptor();
                    url = (descriptor.getHttpPort() == 80)
                            ? descriptor.getHostname()
                            : descriptor.getHostname() + ":" + descriptor.getHttpPort(); //NOI18N
                    //XXX - should somehow support/expect HTTPS too?
                    url = "http://" + url; //NOI18N
                } catch (InstanceRemovedException ex) {
                    url = null;
                }
            }
        }
        return url;
    }

    private String findAppContext(Project p) {
        String cPath = WebModule.getWebModule(p.getProjectDirectory()).getContextPath();
        if (cPath != null) {
            cPath = cPath.substring(1);
        } else {
            cPath = ProjectUtils.getInformation(p).getName();
        }
        return cPath;
    }

    public Set<FileObject> generate(ProgressHandle pHandle) throws IOException {
        if(pHandle != null)
            initProgressReporting(pHandle, false);
        Project targetPrj = FileOwnerQuery.getOwner(getRootFolder());
        if(p != null) {
            this.model = new ClientStubModel().createModel(p);
            this.model.build();
            String url = findBaseUrl(p);
            if(url == null)
                url = getDefaultBaseUrl();
            String proxyUrl2 = findBaseUrl(targetPrj);
            if(proxyUrl2 == null){
                proxyUrl2 = url;
            }
            RestSupport restSupport = p.getLookup().lookup(RestSupport.class);
            String path = restSupport.getApplicationPath();
            setBaseUrl((url.endsWith("/")?url:url+"/") + findAppContext(getProject()) + (path.startsWith("/")?path:"/"+path));
            setProxyUrl((proxyUrl2.endsWith("/")?proxyUrl2:proxyUrl2+"/") + findAppContext(targetPrj) + PROXY_URL);
        } else if(wadl != null) {
            this.model = new ClientStubModel().createModel(wadl);
            this.model.build();
            String url = ((WadlModeler)this.model).getBaseUrl();
            if(url == null) {
                url = getDefaultBaseUrl();
            }
            setBaseUrl(url);
            setProxyUrl(url+".."+PROXY_URL);
            this.projectName = getApplicationNameFromUrl(url);
        }
        List<Resource> resourceList = getModel().getResources();

        rjsDir = getStubFolder();

        FileObject prjStubDir = createFolder(rjsDir, getProjectName().toLowerCase());
        createDataObjectFromTemplate(JS_PROJECTSTUB_TEMPLATE, prjStubDir, 
                getProjectName(), JS, canOverwrite(), getProjectStubParameters(  
                        getProjectName()));
        Set<String> entities = new HashSet<String>();
        for (Resource r : resourceList) {
            if(pHandle != null) {
                reportProgress(NbBundle.getMessage(ClientStubsGenerator.class,
                    "MSG_GeneratingClass", r.getName(), JS));           // NOI18N
            }
            ResourceJavaScript js = new ResourceJavaScript( this , r, prjStubDir,
                    entities );
            js.generate();
            r.getEntities();
            Set<String> generated = r.getEntities();
            entities.addAll( generated );
        }
        
        initJs(p, getRestStubContentParams( resourceList, ""));

        Set<FileObject> files = new HashSet<FileObject>();
        FileObject rjsTest = rjsDir.getFileObject(JS_TESTSTUBS, HTML);
        if(rjsTest != null)
            files.add(rjsTest);
        FileObject readme = rjsDir.getFileObject(JS_README, TXT);
        if(readme != null)
            files.add(readme);
        return files;
    }

    protected FileObject createDataObjectFromTemplate(final String template, 
            final FileObject dir,final String fileName, final String ext, 
            final boolean overwrite, Map<String,String> parameters ) throws IOException 
   {
        FileObject rF0 = dir.getFileObject(fileName, ext);
        if(rF0 != null) {
            if(overwrite) {
                DataObject d = DataObject.find(rF0);
                if(d != null)
                    d.delete();
            } else {
                return rF0;
            }
        }
        DataObject d0 = RestUtils.createDataObjectFromTemplate(template, dir, 
                fileName, parameters);
        return d0.getPrimaryFile();
    }
    
    protected FileObject createDataObjectFromTemplate(final String template, 
            final FileObject dir,final String fileName, final String ext, 
            final boolean overwrite) throws IOException 
    {
        return createDataObjectFromTemplate(template, dir, fileName, ext, 
                overwrite, null);
    }

    protected void copyDirectory(final FileSystem fs, final File src, final File dst)
            throws IOException {
        if (src.isDirectory()) {
            if (!dst.exists()) {
                dst.mkdir();
            }
            String files[] = src.list();
            for (int i = 0; i < files.length; i++) {
                copyDirectory(fs, new File(src, files[i]),
                        new File(dst, files[i]));
            }
        } else {
            if (!src.exists()) {
                throw new IOException("File or directory does not exist.");
            } else {
                fs.runAtomicAction(new FileSystem.AtomicAction() {
                    public void run() throws IOException {
                        InputStream in = null;
                        OutputStream out = null;
                        try {
                            in = new FileInputStream(src);
                            out = new FileOutputStream(dst);
                            byte[] buf = new byte[1024];
                            int len;
                            while ((len = in.read(buf)) > 0) {
                                out.write(buf, 0, len);
                            }
                        } finally {
                            if ( in!= null ){
                                in.close();
                            }
                            if ( out!= null ){
                                out.close();
                            }
                        }
                    }
                });
            }
        }
    }

    private void initJs(Project p, Map<String,String> testStubsParams) throws IOException {
        Map<String,String> map = new HashMap<String, String>();
        map.put(RESTCLIENT_STABS_VAR, NbBundle.getMessage(ClientStubsGenerator.class, TTL_RestClient_Stubs));
        map.put(README_VAR, NbBundle.getMessage(ClientStubsGenerator.class, MSG_Readme));
        map.put(TESTPAGE_VAR, NbBundle.getMessage(ClientStubsGenerator.class, MSG_TestPage));
        map.put(README_CONTENT_VAR, NbBundle.getMessage(ClientStubsGenerator.class, MSG_JS_Readme_Content));
        

        testStubsParams.putAll(map);
        FileObject fo = createDataObjectFromTemplate(JS_TESTSTUBS_TEMPLATE, 
                rjsDir, JS_TESTSTUBS, HTML, false, testStubsParams );

        createDataObjectFromTemplate(JS_STUBSUPPORT_TEMPLATE, rjsDir, JS_SUPPORT, 
                JS, false);

        fo = createDataObjectFromTemplate(JS_README_TEMPLATE, rjsDir, JS_README, 
                HTML, false , map );

        fo = createDataObjectFromTemplate(PROXY_TEMPLATE, rjsDir, PROXY, TXT, false);

        File cssDir = new File(FileUtil.toFile(rjsDir), "css");
        cssDir.mkdirs();
        copySupportFiles(cssDir);
    }

    private void copySupportFiles(File cssDir) throws IOException {
        String[] fileNames = {
            "clientstubs.css",
            "css_master-all.css",
            "images/background_border_bottom.gif",
            "images/pbsel.png",
            "images/bg_gradient.gif",
            "images/pname-clientstubs.png",
            "images/level1_selected-1lvl.jpg",
            "images/primary-enabled.gif",
            "images/masthead.png",
            "images/primary-roll.gif",
            "images/pbdis.png",
            "images/secondary-enabled.gif",
            "images/pbena.png",
            "images/tbsel.png",
            "images/pbmou.png",
            "images/tbuns.png"
        };
        File imagesDir = new File(cssDir, "images");
        imagesDir.mkdirs();
        for(String file: fileNames) {
            MiscUtilities.copyFile(cssDir, file);
        }
    }

    protected void copyFile(String resourceName, File destFile) throws IOException {
        String path = "resources/"+resourceName;
        if(!destFile.exists()) {
            InputStream is = null;
            OutputStream os = null;
            try {
                is = ClientStubsGenerator.class.getResourceAsStream(path);
                os = new FileOutputStream(destFile);
                int c;
                while ((c = is.read()) != -1) {
                    os.write(c);
                }
            } finally {
                if(os != null) {
                    os.flush();
                    os.close();
                }
                if(is != null)
                    is.close();
            }
        }
    }

    protected FileObject createFolder(FileObject parent, String folderName) throws IOException {
        FileObject folder = parent.getFileObject(folderName);
        if(folder == null)
            folder = parent.createFolder(folderName);
        return folder;
    }

    private Map<String,String> getProjectStubParameters( String prjName) 
    {
        Map<String,String> map = new HashMap<String, String>();
        
        String url = getBaseUrl();
        if ( url.endsWith("/")){                        // NOI18N
            url = url.substring(0, url.length()-1);
        }
        map.put(BASE_URL_TOKEN, url);
        map.put("project_name", prjName);               // NOI18N
        
        StringBuilder initBody = new StringBuilder();
        int count = 0;
        List<Resource> resourceList = getModel().getResources();
        for (Resource resource : resourceList) {
            if (resource.hasDefaultGet()) {
                
                initBody.append( "      this.resources[");// NOI18N
                initBody.append( count++ );              // NOI18N
                initBody.append( "] = new " );           // NOI18N
                initBody.append( resource.getName());   
                initBody.append("(this.uri+'" );         // NOI18N
                String path = resource.getPath();
                if (!path.startsWith("/")) {             // NOI18N
                    initBody.append( '/' );
                }
                initBody.append( path );
                initBody.append("');\n");                // NOI18N
            }
        }
        map.put("project_init_body", initBody.toString());// NOI18N
        
        return map;
    }

    private Map<String,String> getRestStubContentParams(List<Resource> resourceList, 
            String pkg) throws IOException 
    {
        String prjName = getProjectName();
        String prjStubDir = prjName.toLowerCase();
        StringBuilder sb1 = new StringBuilder();
        sb1.append("\t<script type='text/javascript' src='./" );
        sb1.append(prjStubDir); 
        sb1.append( '/');
        sb1.append(prjName );
        sb1.append('.');
        sb1.append(JS ); 
        sb1.append("'></script>\n");
        for (Resource r : resourceList) {
            sb1.append("\t<script type='text/javascript' src='./" );
            sb1.append(prjStubDir); 
            sb1.append( '/');
            sb1.append(r.getName() );
            sb1.append('.');
            sb1.append(JS ); 
            sb1.append("'></script>\n");
            Iterator<String> iterator = r.getEntities().iterator();
            while ( iterator.hasNext() ){
                sb1.append("\t<script type='text/javascript' src='./" );
                sb1.append(prjStubDir); 
                sb1.append( '/');
                sb1.append(iterator.next() );
                sb1.append('.');
                sb1.append(JS ); 
                sb1.append("'></script>\n");
            }
        }
        StringBuilder sb2 = new StringBuilder();
        String url = getBaseUrl();
        if ( url.endsWith("/")) {
            url = url.substring(0, url.length()-1);
        }
        sb2.append("\n\t<!-- Using JavaScript files for project " + prjName + "-->\n");
        sb2.append("\t<script language='Javascript'>\n");
        sb2.append("\t\tvar str = '';\n");
        sb2.append("\t\t//Example test code for " + prjName + "\n");
        sb2.append("\t\tstr = '<h2>Resources for " + prjName + ":</h2><br><table border=\"1\">';\n");
        sb2.append("\t\tvar app = new " + pkg+prjName + "('"+url+"');\n");
        sb2.append("\t\t//Uncomment below if using proxy for javascript cross-domain.\n");
        sb2.append("\t\t//app.setProxy(\""+getProxyUrl()+"\");\n");
        sb2.append("\t\tvar resources = app.getResources();\n");
        sb2.append("\t\tfor(i=0;i<resources.length;i++) {\n");
        sb2.append("\t\t  var resource = resources[i];\n");
        sb2.append("\t\t  var uri = resource.getUri();\n");
        sb2.append("\t\t  str += '<tr><td valign=\"top\"><a href=\"'+uri+'\" target=\"_blank\">'+uri+'</a></td><td>';\n");
        sb2.append("\t\t  var items  = resource.getEntities();\n");
        sb2.append("\t\t  if (items != undefined) {\n");
        sb2.append("\t\t    if (items.length > 0) {\n");
        sb2.append("\t\t      for(j=0;j<items.length;j++) {\n");
        sb2.append("\t\t        var item = items[j];\n");
        sb2.append("\t\t        var uri2 = item.getUri();\n");
        sb2.append("\t\t        if ( uri2 != null && uri2 != undefined ){\n");
        sb2.append("\t\t            str += '<a href=\"'+uri2+'\" target=\"_blank\">'+uri2+'</a><br/>';\n");
        sb2.append("\t\t        }\n");
        sb2.append("\t\t        str += '&nbsp;&nbsp;<font size=\"-3\">'+item.toString()+'</font><br/>';\n");
        sb2.append("\t\t      }\n");
        sb2.append("\t\t    } else {\n");
        sb2.append("\t\t      str += 'No items detected';\n");
        sb2.append("\t\t    }\n");
        sb2.append("\t\t  } else {\n");
        sb2.append("\t\t    str += 'No items, please check the url: <a href=\"'+uri+'\" target=\"_blank\">'+uri+'</a>.<br/>" +
                "Set proxy if RESTful web service is not running on the same domain as this application.';\n");
        sb2.append("\t\t  }\n");
        sb2.append("\t\t  str += '</td></tr>';\n");
        sb2.append("\t\t}\n");
        sb2.append("\t\tstr += '</table><br>';\n");
        sb2.append("\t\tvar n = document.getElementById('containerContent');\n");
        sb2.append("\t\tn.innerHTML = n.innerHTML + str;\n\n");
        sb2.append("\t</script>\n");
        Map<String,String> map = new HashMap<String, String>();
        map.put("js_scripts_declaration", sb1.toString());      // NOI18N
        map.put("js_usage", sb2.toString());                    // NOI18N
        
        return map;
    }
}
