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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.prefs.Preferences;
import javax.swing.JButton;
import javax.swing.SwingUtilities;
import org.apache.tools.ant.module.api.support.ActionUtils;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.cordova.platforms.spi.BuildPerformer;
import org.netbeans.modules.cordova.platforms.spi.Device;
import org.netbeans.modules.cordova.platforms.api.PlatformManager;
import org.netbeans.modules.cordova.project.MobileConfigurationImpl;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.*;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ServiceProvider;
import static org.netbeans.modules.cordova.PropertyNames.*;
import org.netbeans.modules.cordova.platforms.api.ClientProjectUtilities;
import org.netbeans.modules.cordova.platforms.spi.MobilePlatform;
import org.netbeans.modules.cordova.wizard.CordovaProjectExtender;
import org.netbeans.modules.cordova.platforms.spi.SDK;
import org.netbeans.modules.cordova.platforms.api.WebKitDebuggingSupport;
import org.netbeans.modules.cordova.project.ConfigUtils;
import org.netbeans.modules.cordova.project.CordovaCustomizerPanel;
import org.netbeans.modules.cordova.project.CordovaBrowserFactory;
import org.netbeans.modules.cordova.updatetask.SourceConfig;
import org.netbeans.modules.web.browser.api.BrowserFamilyId;
import org.netbeans.modules.web.browser.api.WebBrowser;
import org.netbeans.modules.web.browser.spi.BrowserURLMapperImplementation;
import org.netbeans.modules.web.browser.spi.ProjectBrowserProvider;
import org.netbeans.modules.web.common.api.ServerURLMapping;
import org.netbeans.spi.project.ui.CustomizerProvider2;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.execution.ExecutorTask;
import org.openide.loaders.DataObject;

/**
 *
 * @author Jan Becicka
 */
@ServiceProvider(service = BuildPerformer.class)
public class CordovaPerformer implements BuildPerformer {
    public static final String NAME_BUILD_XML = "build.xml"; // NOI18N
    public static final String NAME_PLUGINS_PROPERTIES = "plugins.properties"; //NII18N
    public static final String NAME_CONFIG_XML = "config.xml"; // NOI18N
    public static final String PATH_BUILD_XML = "nbproject/" + NAME_BUILD_XML; // NOI18N
    public static final String PATH_PLUGINS_PROPERTIES = "nbproject/" + NAME_PLUGINS_PROPERTIES; // NOI18N
    public static final String PATH_EXTRA_ANT_JAR = "ant/extra/org-netbeans-modules-cordova-projectupdate.jar"; // NOI18N
    public static final String DEFAULT_ID_PREFIX = "com.coolappz"; // NOI18N
    public static final String DEFAULT_EMAIL = "info@com.coolappz"; // NOI18N
    public static final String DEFAULT_WWW = "http://www.coolappz.com";
    public static final String DEFAULT_VERSION = "1.0.0"; // NOI18N
    public static final String DEFAULT_DESCRIPTION = Bundle.DSC_Cordova();
    public static final String PROP_BUILD_SCRIPT_VERSION = "cordova_build_script_version"; // NOI18N
    public static final String PROP_PROVISIONING_PROFILE = "ios.provisioning.profile"; // NOI18N
    public static final String PROP_CERTIFICATE_NAME = "ios.certificate.name"; // NOI18N
    public static final String WWW_NB_TEMP = "www_nb_temp";
    public static final String WWW = "www";

    static final String GRUNT_CUSTOMIZER_IDENT = "Grunt";
    static final String GULP_CUSTOMIZER_IDENT = "Gulp";
    
    private final RequestProcessor RP = new RequestProcessor(CordovaPerformer.class.getName(), 10);

    //must be increased on each change in build.xml
    private final int BUILD_SCRIPT_VERSION = 52;
    
    public static CordovaPerformer getDefault() {
        return Lookup.getDefault().lookup(CordovaPerformer.class);
    }
    
    
    public Task createPlatforms(final Project project) {
        return perform("upgrade-to-cordova-project", project, false);      
    }
    
    @NbBundle.Messages({
        "LBL_InstallThroughItunes=Install application using iTunes and tap on it",
        "CTL_InstallAndRun=Install and Run",
        "DSC_Cordova=Cordova Application",
        "ERR_StartFileNotFound=Start file cannot be found.",
        "ERR_NO_Cordova=NetBeans cannot find cordova or git on your PATH. Please install cordova and git.\n" +
            "NetBeans might require restart for changes to take effect.\n",
        "ERR_NO_Provisioning=Provisioning Profile not found.\nPlease use XCode and install valid Provisioning Profile for your device.",
        "ERR_NOT_Cordova=Create Cordova Resources and rename site root to 'www'?",
        "CTL_SelectBuildTool=Select build tool",
        "# {0} - project name", 
        "# {1} - build file", 
        "# {2} - build tool name", 
        "MSG_SelectBuildTool=The project ''{0}'' contains ''{1}''.\nClick Ant to execute Ant targets for this project.\nClick ''{2}'' to assign IDE actions to ''{2}'' tasks.",
        "ERR_SiteRootNotDefined=Project Site Root Folder must be located in project directory"    
    })
    @Override
    public ExecutorTask perform(final String target, final Project project) {    
        return perform(target, project, true);
    }
    
    private ExecutorTask perform(final String target, final Project project, final boolean checkOtherScripts) {    
        if ((target.startsWith("build") || target.startsWith("sim"))
                && ClientProjectUtilities.getStartFile(project) == null) {
            DialogDisplayer.getDefault().notify(
                    new DialogDescriptor.Message(
                    Bundle.ERR_StartFileNotFound()));
            CustomizerProvider2 cust = project.getLookup().lookup(CustomizerProvider2.class);
            cust.showCustomizer("RUN", null);
            return null;
        }
        
        if (((target.startsWith("build") || target.startsWith("sim") || target.startsWith("rebuild")) // NOI18N
                && !CordovaPlatform.isCordovaProject(project))) { // NOI18N
            NotifyDescriptor desc = new NotifyDescriptor(
                    Bundle.ERR_NOT_Cordova(),
                    NbBundle.getMessage(CordovaCustomizerPanel.class, "CordovaPanel.createConfigs.text"),
                    NotifyDescriptor.OK_CANCEL_OPTION,
                    NotifyDescriptor.QUESTION_MESSAGE,
                    null,
                    NotifyDescriptor.OK_OPTION);
            DialogDisplayer.getDefault().notify(desc);
            if (desc.getValue() != NotifyDescriptor.OK_OPTION) {
                return null;
            }
       }
        
        final FileObject siteRoot = ClientProjectUtilities.getSiteRoot(project);
        if (siteRoot == null || FileUtil.getRelativePath(project.getProjectDirectory(), siteRoot) == null) {
            DialogDisplayer.getDefault().notify(
                    new DialogDescriptor.Message(
                    Bundle.ERR_SiteRootNotDefined()));
            CustomizerProvider2 cust = project.getLookup().lookup(CustomizerProvider2.class);
            cust.showCustomizer("SOURCES", null);
            return null;
        }
        

        if (!CordovaPlatform.getDefault().isReady()) {
            throw new UnsupportedOperationException(Bundle.ERR_NO_Cordova());
        }
        
        ProjectBrowserProvider provider = project.getLookup().lookup(ProjectBrowserProvider.class);
        if (provider != null &&
                "ios_1".equals(provider.getActiveBrowser().getId()) && 
                (target.equals(BuildPerformer.RUN_IOS) || target.equals(BuildPerformer.BUILD_IOS)) &&
                PlatformManager.getPlatform(PlatformManager.IOS_TYPE) != null &&
                PlatformManager.getPlatform(PlatformManager.IOS_TYPE).getProvisioningProfilePath() == null
                ) {
            throw new IllegalStateException(Bundle.ERR_NO_Provisioning());
       }        

        
        final ExecutorTask runTarget[] = new ExecutorTask[1];
        Runnable run = new Runnable() {
            @Override
            public void run() {
                try {
                    FileObject siteRoot = ClientProjectUtilities.getSiteRoot(project);
                    final DataObject siteRootDOB = DataObject.find(siteRoot);
                    final boolean rename = target.startsWith("build") || target.startsWith("sim") || target.startsWith("rebuild") || target.startsWith("upgrade");
                    if (rename) {
                        if (!CordovaPlatform.isCordovaProject(project)) {
                            siteRootDOB.rename(WWW_NB_TEMP);
                        } else {
                            siteRootDOB.rename(WWW);
                        }
                    }
                    
                    if(checkOtherScripts && 
                       project.getProjectDirectory().getFileObject(PATH_BUILD_XML) == null && 
                       checkOtherBuildScripts(project)) 
                    {
                        return;
                    }
                    generateBuildScripts(project);
                    FileObject buildFo = project.getProjectDirectory().getFileObject(PATH_BUILD_XML);//NOI18N
                    
                    final Properties properties = properties(project);
                    runTarget[0] = ActionUtils.runTarget(buildFo, new String[]{target}, properties);
                    final int result = runTarget[0].result();

                    project.getProjectDirectory().refresh();
                    if (rename && !WWW.equals(siteRootDOB.getName())) {
                        FileObject www = project.getProjectDirectory().getFileObject(WWW);
                        if (www !=null) {
                            DataObject.find(www).delete();
                        }
                        siteRootDOB.rename(WWW);
                    }

                    if (target.equals(BuildPerformer.RUN_IOS) || (target.equals(BuildPerformer.RUN_ANDROID)) && isAndroidDebugSupported(project)) {
                        if (result == 0) {
                            ProjectBrowserProvider provider = project.getLookup().lookup(ProjectBrowserProvider.class);
                            if (provider != null) {
                                WebBrowser activeConfiguration = provider.getActiveBrowser();
                                MobileConfigurationImpl mobileConfig = MobileConfigurationImpl.create(project, activeConfiguration.getId());
                                Device device = mobileConfig.getDevice();
                                if (!device.isWebViewDebugSupported()) {
                                    return;
                                }
                                final FileObject startFile = ClientProjectUtilities.getStartFile(project);
                                
                                //#231037
                                URL u = ServerURLMapping.toServer(project, startFile);
                                activeConfiguration.toBrowserURL(project, startFile, u);
                                
                                BrowserURLMapperImplementation.BrowserURLMapper mapper = ((CordovaBrowserFactory) activeConfiguration.getHtmlBrowserFactory()).getMapper();
                                if (!device.isEmulator() && target.equals(BuildPerformer.RUN_IOS)) {
                                    DialogDescriptor dd = new DialogDescriptor(Bundle.LBL_InstallThroughItunes(), Bundle.CTL_InstallAndRun());
                                    if (DialogDisplayer.getDefault().notify(dd) != DialogDescriptor.OK_OPTION) {
                                        return;
                                    }
                                } else {
                                    try {
                                        Thread.sleep(target.equals(BuildPerformer.RUN_IOS)?2000:5000);
                                    } catch (InterruptedException ex) {
                                        Exceptions.printStackTrace(ex);
                                } 
                                }
                                WebKitDebuggingSupport.getDefault().startDebugging(device, 
                                        project, 
                                        Lookups.fixed(
                                            mapper, 
                                            ImageUtilities.loadImage("org/netbeans/modules/cordova/platforms/ios/ios" + (String) (device.isEmulator()?"simulator16.png":"device16.png")), 
                                            getConfig(project).getId()), 
                                        false);
                            }
                        }
                   }
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                } catch (IllegalArgumentException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }

            private boolean isAndroidDebugSupported(Project project) {
                if (CordovaPlatform.getDefault().getVersion().getApiVersion().compareTo(new CordovaPlatform.Version.SubVersion("3.3.0")) >= 0) {
                    try {
                        FileObject manifestFile = project.getProjectDirectory().getFileObject("platforms/android/AndroidManifest.xml");
                        if (manifestFile == null) {
                            return false;
                        }
                        FileObject propertiesFile = project.getProjectDirectory().getFileObject("platforms/android/project.properties");
                        if (propertiesFile == null) {
                            return false;
                        }
                        
                        try (InputStream s=propertiesFile.getInputStream()) {
                            Properties props = new Properties();
                            props.load(s);
                            String target = props.getProperty("target"); //NOI18N
                            if (target == null || target.trim().compareTo("android-19") < 0) { //NOI18N
                                return false;
                            }
                        }
                        return true;
                    } catch (IOException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
                return false;
            }
        };
        
        if (SwingUtilities.isEventDispatchThread()) {
            RP.post(run);
        } else {
            run.run();
        }
        return runTarget[0];
    }

    private Properties properties(Project p) {
        Properties props = new Properties();
        final CordovaPlatform cordovaPlatform = CordovaPlatform.getDefault();
//        props.put(PROP_CORDOVA_HOME, cordovaPlatform.getSdkLocation());//NOI18N
        props.put(PROP_CORDOVA_VERSION, cordovaPlatform.getVersion().toString());//NOI18N
        final FileObject siteRoot = ClientProjectUtilities.getSiteRoot(p);
        if (siteRoot != null) {
            final String siteRootRelative = FileUtil.getRelativePath(p.getProjectDirectory(), siteRoot);
                props.put(PROP_SITE_ROOT, siteRootRelative);
        } else {
            props.put(PROP_SITE_ROOT, WWW_NB_TEMP);
        }
        final FileObject startFile = ClientProjectUtilities.getStartFile(p);
        if (startFile!=null) {
            final String startFileRelative = FileUtil.getRelativePath(siteRoot, startFile);
            props.put(PROP_START_FILE, startFileRelative);
        }
        final File antTaskJar = InstalledFileLocator.getDefault().locate(
           PATH_EXTRA_ANT_JAR, 
           "org.netbeans.modules.cordova" , true); // NOI18N
        props.put(PROP_UPDATE_TASK_JAR, antTaskJar.getAbsolutePath());
        final String id = getConfig(p).getId();
        String activity = id.substring(id.lastIndexOf(".")+1, id.length()); // NOI18N
        props.put(PROP_ANDROID_PROJECT_ACTIVITY, activity);//NOI18N
        
        MobilePlatform iosPlatform = PlatformManager.getPlatform(PlatformManager.IOS_TYPE);
        if (iosPlatform != null) {
            final String provisioningProfilePath = iosPlatform.getProvisioningProfilePath();
            if (provisioningProfilePath != null) {
                props.put(PROP_PROVISIONING_PROFILE, provisioningProfilePath);
            }
            final String codeSignIdentity = iosPlatform.getCodeSignIdentity();

            if (codeSignIdentity != null) {
                props.put(PROP_CERTIFICATE_NAME, codeSignIdentity);
            }
        }

        //workaround for some strange behavior of ant execution in netbeans
        props.put(PROP_ENV_DISPLAY, ":0.0");//NOI18N
        final String sdkLocation = PlatformManager.getPlatform(PlatformManager.ANDROID_TYPE).getSdkLocation();
        if (sdkLocation!=null) {
            props.put(PROP_ANDROID_SDK_HOME, sdkLocation);
        }

        ProjectBrowserProvider provider = p.getLookup().lookup(ProjectBrowserProvider.class);
        if (provider != null && provider.getActiveBrowser().getBrowserFamily()==BrowserFamilyId.PHONEGAP) {
            WebBrowser activeConfiguration = provider.getActiveBrowser();
            MobileConfigurationImpl mobileConfig = MobileConfigurationImpl.create(p, activeConfiguration.getId());

            props.put(PROP_CONFIG, mobileConfig.getId());
            mobileConfig.getDevice().addProperties(props);
            if (mobileConfig.getId().equals("ios") && iosPlatform != null) { // NOI18N
                boolean sdkVerified = false;
                try {
                    for (SDK sdk:iosPlatform.getSDKs()) {
                        if (sdk.getIdentifier().equals(mobileConfig.getProperty("ios.build.sdk"))) {
                            sdkVerified = true;
                            break;
                        }
                    }
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
                if (!sdkVerified) {
                    mobileConfig.putProperty("ios.build.sdk", iosPlatform.getPrefferedTarget().getIdentifier()); // NOI18N
                    mobileConfig.save();
                }
            }
        }

        return props;
    }

    private void generateBuildScripts(Project project) {
        try {
            CordovaProjectExtender.createMobileConfigs(project.getProjectDirectory());
            Preferences preferences = ProjectUtils.getPreferences(project, CordovaPlatform.class, true);
            int version = preferences.getInt(PROP_BUILD_SCRIPT_VERSION, 0);

            boolean fresh;
            if (version < BUILD_SCRIPT_VERSION) {
                fresh = createScript(project, NAME_BUILD_XML, PATH_BUILD_XML, true);//NOI18N
            } else {
                fresh = createScript(project, NAME_BUILD_XML, PATH_BUILD_XML, false);//NOI18N
            }
            if (fresh) {
                preferences.putInt(PROP_BUILD_SCRIPT_VERSION, BUILD_SCRIPT_VERSION);
                Map<String, String> map = new HashMap<String, String>();
                map.put("__PROJECT_NAME__", ProjectUtils.getInformation(project).getName());// NOI18N
                ConfigUtils.replaceToken(project.getProjectDirectory().getFileObject(PATH_BUILD_XML), map);
                createScript(project, "empty.properties", PATH_PLUGINS_PROPERTIES, false);
            }

            getConfig(project);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
    }
    
    private boolean checkOtherBuildScripts(Project project) {
        return hasOtherBuildTool(project, "Gruntfile.js", "Grunt", () -> { project.getLookup().lookup(CustomizerProvider2.class).showCustomizer(GRUNT_CUSTOMIZER_IDENT, null); }) ||
               hasOtherBuildTool(project, "gulpfile.js",  "Gulp", () -> { project.getLookup().lookup(CustomizerProvider2.class).showCustomizer(GULP_CUSTOMIZER_IDENT, null); }) 
               ? true : false;            
    }
    
    private boolean hasOtherBuildTool(Project project, String toolfile, String toolName, Runnable r) {
        if(project.getProjectDirectory().getFileObject(toolfile) != null) {
            ProjectInformation info = ProjectUtils.getInformation(project);
            String name = info != null ? info.getDisplayName() : project.getProjectDirectory().getNameExt();
            JButton tool = new JButton(toolName);
            NotifyDescriptor desc = new NotifyDescriptor(
                Bundle.MSG_SelectBuildTool(name, toolfile, toolName),
                Bundle.CTL_SelectBuildTool(),
                NotifyDescriptor.OK_CANCEL_OPTION,
                NotifyDescriptor.QUESTION_MESSAGE,
                new Object[] {tool, new JButton("Ant"), NotifyDescriptor.CANCEL_OPTION},
                NotifyDescriptor.OK_OPTION);
            DialogDisplayer.getDefault().notify(desc);
            if(desc.getValue() == tool) {
                r.run();
                return true;
            } else if (desc.getValue() == NotifyDescriptor.CANCEL_OPTION) {
                return true;
            }
        }
        return false;
    }
    
    private static String getConfigPath(Project project) {
        final FileObject siteRoot = ClientProjectUtilities.getSiteRoot(project);
        String configPath = (siteRoot==null?WWW_NB_TEMP:siteRoot.getNameExt()) + "/" + NAME_CONFIG_XML; // NOI18N
        boolean configExists = project.getProjectDirectory().getFileObject(configPath) != null;

        if (CordovaPlatform.getDefault().getVersion().getApiVersion().compareTo(new CordovaPlatform.Version.SubVersion("3.4.0")) >= 0) {
            String newConfigPath = "/" + NAME_CONFIG_XML;
            boolean newConfigPathExists = project.getProjectDirectory().getFileObject(newConfigPath) !=null;
            if (newConfigPathExists) {
                return newConfigPath;
            } else {
                if (configExists) {
                    return configPath;
                } else {
                    return newConfigPath;
                }
            }
        } else {
            return configPath;
        }
    }
    
    public static SourceConfig getConfig(Project project)  {
        try {
            String configPath = getConfigPath(project);
            boolean fresh = createScript(project, NAME_CONFIG_XML, configPath, false);//NOI18N

            FileObject config = project.getProjectDirectory().getFileObject(configPath);
            SourceConfig conf = new SourceConfig(FileUtil.toFile(config));
            if (fresh) {
                final String appName = ProjectUtils.getInformation(project).getDisplayName().replaceAll(" ", "_").replaceAll("-", "_").replaceAll("\\.","_"); // NOI18N
                conf.setId(DEFAULT_ID_PREFIX + "." + appName); // NOI18N
                conf.setName(appName);
                conf.setDescription(DEFAULT_DESCRIPTION);
                conf.setAuthor(System.getProperty("user.name"));
                conf.setAuthorEmail(DEFAULT_EMAIL);
                conf.setAuthorHref(DEFAULT_WWW);
                conf.setVersion(DEFAULT_VERSION);
                conf.save();
            }
            return conf;
        } catch (IOException iOException) {
            throw new IllegalStateException(iOException);
        }
    }

    /**
     * 
     * @param project
     * @param source
     * @param target
     * @param overwrite
     * @return true if script was created. False if script was already there
     * @throws IOException 
     */
    public static boolean createScript(Project project, String source, String target, boolean overwrite) throws IOException {        
        FileObject build = null;
        if (!overwrite) {
            build = project.getProjectDirectory().getFileObject(target);
        }
        if (build == null) {
            build = FileUtil.createData(project.getProjectDirectory(), target);
            InputStream resourceAsStream = CordovaPerformer.class.getResourceAsStream(source);
            OutputStream outputStream = build.getOutputStream();
            try {
                FileUtil.copy(resourceAsStream, outputStream);
            } finally {
                outputStream.close();
                resourceAsStream.close();
            }
            return true;
        }
        return false;
    }
    
    private class CompoundTask extends Task {

        private final Task task1;
        private final Task task2;

        public CompoundTask(Task task1, Task task2) {
            this.task1 = task1;
            this.task2 = task2;
        }
        @Override
        public void waitFinished() {
            if (task1!=null) {
                task1.waitFinished();
            }
            if (task2!=null) {
                task2.waitFinished();
            }
        }
    }

}
