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

import java.io.BufferedReader;
import org.netbeans.modules.cordova.platforms.spi.MobilePlatform;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingUtilities;
import org.apache.tools.ant.module.api.support.ActionUtils;
import org.netbeans.modules.cordova.platforms.spi.Device;
import org.netbeans.modules.cordova.platforms.api.PlatformManager;
import org.netbeans.modules.cordova.platforms.api.ProcessUtilities;
import org.netbeans.modules.cordova.platforms.spi.ProvisioningProfile;
import org.netbeans.modules.cordova.platforms.spi.SDK;
import org.openide.execution.ExecutorTask;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.EditableProperties;
import org.openide.util.Exceptions;
import org.openide.util.NbPreferences;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.lookup.ServiceProvider;

/**
 *
 * @author Jan Becicka
 */
@ServiceProvider(service=MobilePlatform.class)
public class AndroidPlatform implements MobilePlatform {
    
    private static String ANDROID_SDK_ROOT_PREF = "android.sdk.home"; //NOI18N
    
    public static int DEFAULT_TIMEOUT = 30000;

    private transient final java.beans.PropertyChangeSupport propertyChangeSupport = new java.beans.PropertyChangeSupport(this);
    
    public AndroidPlatform() {
    }
    
    public static AndroidPlatform getDefault() {
        return (AndroidPlatform) PlatformManager.getPlatform(PlatformManager.ANDROID_TYPE);
    }

//    public void createProject(File dir, String targetId, String projectName, String activityName, String packageName) throws IOException {
//        ProcessBuilder pb = ProcessBuilder.getLocal();
//        pb.setExecutable(getSdkLocation() + "/tools/android");
//        pb.setArguments(
//        Arrays.asList(
//                "create", "project", 
//                "--target", targetId,
//                "--name", projectName,
//                "--path", dir.getPath(),
//                "--activity", activityName,
//                "--package", packageName
//                ));
//        pb.setWorkingDirectory(dir.getParentFile().getAbsolutePath());
//        try {
//            Process call = pb.call();
//            call.waitFor();
//            InputStreamReader inputStreamReader = new InputStreamReader(new BufferedInputStream(call.getErrorStream()));
//            if (call.exitValue() != 0) {
//                StringBuilder error = new StringBuilder();
//                char[] ch = new char[1];
//                while (inputStreamReader.ready()) {
//                    inputStreamReader.read(ch);
//                    error.append(ch);
//                }
//                throw new IOException(error.toString());
//            }
//        } catch (InterruptedException ex) {
//            throw new IOException(ex);
//        }
//    }
    
    @Override
    public Collection<Device> getVirtualDevices() throws IOException {
        assert !SwingUtilities.isEventDispatchThread();
        String avdString = ProcessUtilities.callProcess(getAndroidCommand(), true, AndroidPlatform.DEFAULT_TIMEOUT, "list", "avd"); //NOI18N
        return AVD.parse(avdString);
    }
    
    private String getAndroidCommand() {
        if (Utilities.isWindows()) {
            return getSdkLocation() + "\\tools\\android.bat"; // NOI18N
        } else {
            return getSdkLocation() + "/tools/android"; // NOI18N
        }
    }
    
    String getAdbCommand() {
        if (Utilities.isWindows()) {
            return getSdkLocation() + "\\platform-tools\\adb.exe"; // NOI18N
        } else {
            return getSdkLocation() + "/platform-tools/adb"; // NOI18N

        }
    }
    

    @Override
    public Collection<SDK> getSDKs() throws IOException {
        //assert !SwingUtilities.isEventDispatchThread();
        String avdString = ProcessUtilities.callProcess(getAndroidCommand(), true, 30000, "list", "target");//NOI18N
        return Target.parse(avdString);
    }
    
    private final HashSet<String> targets = new HashSet<String>(Arrays.asList(new String[]{
            "android-14", //NOI18N
            "android-15", //NOI18N
            "android-16", //NOI18N
            "android-17", //NOI18N
            "android-18", //NOI18N
            "android-19", //NOI18N
            "android-20", //NOI18N
            "android-21", //NOI18N
            "android-22"})); //NOI18N
    
    
    @Override
    public SDK getPrefferedTarget() {
        try {
            final Collection<SDK> targets1 = getSDKs();
            for (SDK t: targets1) {
                if (targets.contains(t.getName())) {
                    return t;
                }
            }
            if (targets1.iterator().hasNext()) {
                return targets1.iterator().next();
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }
    
    
    @Override
    public Collection<org.netbeans.modules.cordova.platforms.spi.Device> getConnectedDevices() throws IOException {
        //assert !SwingUtilities.isEventDispatchThread();
        String avdString = ProcessUtilities.callProcess(getAdbCommand(), true, AndroidPlatform.DEFAULT_TIMEOUT, "devices"); //NOI18N
        Collection<org.netbeans.modules.cordova.platforms.spi.Device> devices = AndroidDevice.parse(avdString);
        if (devices.isEmpty()) {
            //maybe adb is just down. try to restart adb
            try {
                ProcessUtilities.callProcess(getAdbCommand(), true, AndroidPlatform.DEFAULT_TIMEOUT, "kill-server"); //NOI18N
                ProcessUtilities.callProcess(getAdbCommand(), true, AndroidPlatform.DEFAULT_TIMEOUT, "start-server"); //NOI18N
            } catch (IOException ioe) {
                //ignore
            }
        }
        avdString = ProcessUtilities.callProcess(getAdbCommand(), true, AndroidPlatform.DEFAULT_TIMEOUT, "devices"); //NOI18N
        devices = AndroidDevice.parse(avdString);
        return devices;
    }
    
    
    public ExecutorTask buildProject(File dir, String... targets) throws IOException {
        File build = new File(dir.getAbsolutePath() + File.separator + "build.xml"); //NOI18N
        FileObject buildFo = FileUtil.toFileObject(build);
        return ActionUtils.runTarget(buildFo, targets, null);
    }
    
    /**
     * Deletes dir and all subdirectories/files!
     * @param dir
     * @throws IOException 
     */
    public void cleanProject(File dir) throws IOException {
        FileUtil.toFileObject(dir).delete();
    }

    @Override
    public String getSdkLocation() {
        String sdkLocation = NbPreferences.forModule(AndroidPlatform.class).get(ANDROID_SDK_ROOT_PREF, null);
        if (sdkLocation != null && !sdkLocation.isEmpty()) {
            return sdkLocation;
        } else {
            return getSdkFromAndroidHome();
        }
    }

    @Override
    public void setSdkLocation(String sdkLocation) {
        NbPreferences.forModule(AndroidPlatform.class).put(ANDROID_SDK_ROOT_PREF, sdkLocation);
        propertyChangeSupport.firePropertyChange("SDK", null, sdkLocation);//NOI18N
    }
    
    @Override
    public boolean waitEmulatorReady(int timeout) {
        try {
            return RequestProcessor.getDefault().invokeAny(Collections.singleton(new Callable<Boolean>() {

                    @Override
                    public Boolean call() throws Exception {
                        return waitEmulatorReady();
                    }
                }), timeout, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
        } catch (ExecutionException ex) {
        } catch (TimeoutException ex) {
        }
        return false;
        
    }
    
    private boolean waitEmulatorReady() {
        try {
            String value;
            for(;;) {
                value = ProcessUtilities.callProcess(
                        getAdbCommand(), 
                        true, 
                        -1, 
                        "-e", // NOI18N
                        "wait-for-device", // NOI18N
                        "shell", // NOI18N
                        "getprop", // NOI18N
                        "init.svc.bootanim"); //NOI18N
                if ("stopped".equals(value.trim())) { //NOI18N
                    return true;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                    Exceptions.printStackTrace(ex);
                }
            } 
            
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return false;
        
    }

    @Override
    public void manageDevices() {
        assert !SwingUtilities.isEventDispatchThread();
        try {
            ProcessUtilities.callProcess(getAndroidCommand(), true, -1, "avd"); //NOI18N
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    @Override
    public boolean isReady() {
        return getSdkLocation() != null && !getSdkLocation().isEmpty();
    }
    
    /**
     * Add PropertyChangeListener.
     *
     * @param listener
     */
    @Override
    public void addPropertyChangeListener(java.beans.PropertyChangeListener listener ) {
        propertyChangeSupport.addPropertyChangeListener( listener );
    }

    /**
     * Remove PropertyChangeListener.
     *
     * @param listener
     */
    @Override
    public void removePropertyChangeListener(java.beans.PropertyChangeListener listener ) {
        propertyChangeSupport.removePropertyChangeListener( listener );
    }


    @Override
    public String getType() {
        return PlatformManager.ANDROID_TYPE;
    }

    @Override
    public String getSimulatorPath() {
        throw new UnsupportedOperationException("Not supported yet."); // NOI18N
    }

    @Override
    public Device getDevice(String name, EditableProperties props) {
        return AndroidDevice.get(name, props);
    }

    @Override
    public String getCodeSignIdentity() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. // NOI18N
    }

    @Override
    public String getProvisioningProfilePath() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. // NOI18N
    }

    @Override
    public void setCodeSignIdentity(String identity) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. // NOI18N
    }

    @Override
    public void setProvisioningProfilePath(String path) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. // NOI18N
    }

    @Override
    public Collection<? extends ProvisioningProfile> getProvisioningProfiles() {
        return Collections.emptyList();
    }

    public String getProcessIdByName(String appName) {
        try {
            String result = ProcessUtilities.callProcess(getAdbCommand(), true, AndroidPlatform.DEFAULT_TIMEOUT, "shell", "ps"); //NOI18N
            BufferedReader r = new BufferedReader(new StringReader(result));
            String line;
            while ((line = r.readLine()) != null) {
                if (line.trim().endsWith(appName)) {
                    Pattern column = Pattern.compile("(\\S+)(\\s+)(\\S+)(\\s+)(.+)");
                    Matcher matcher = column.matcher(line);
                    if (matcher.matches()) {
                        return matcher.group(3);
                    }
                }
            } 
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }

    boolean isWebViewDebugSupported(boolean emulator) {
        try {
            String version = ProcessUtilities.callProcess(
                    getAdbCommand(),
                    true,
                    -1,
                    emulator?"-e":"-d", // NOI18N
                    "wait-for-device", // NOI18N
                    "shell", // NOI18N
                    "getprop", //NOI18N
                    "ro.build.version.release"); //NOI18N
            
            return version.compareTo("4.4") >= 0;
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
            return false;
        }
        
        
    }

    /**
     * Sets the Android SDK location from ANDROID_HOME environment variable, if
     * available.
     *
     * @return Android SDK Location or <code>null</code> if Android SDK could
     * not be found.
     */
    private String getSdkFromAndroidHome() {
        String androidHomeValue = System.getenv("ANDROID_HOME"); //NOI18N
        if (androidHomeValue != null && !androidHomeValue.isEmpty()) {
            File androidLoc = new File(androidHomeValue);
            File androidTools = new File(androidLoc, "platform-tools"); //NOI18N
            boolean validSdk = androidLoc.exists() && androidLoc.isDirectory()
                    && androidTools.exists() && androidTools.isDirectory();
            if (validSdk) {
                NbPreferences.forModule(AndroidPlatform.class).put(ANDROID_SDK_ROOT_PREF, androidHomeValue);
                return androidHomeValue;
            }
        }
        return null;
    }

    /**
     * Performs a check whether adb executable is available
     *
     * @return <code>true</code> if adb command has been found,
     * <code>false</code> otherwise
     */
    boolean adbCommandExists() {
        return Files.exists(Paths.get(getAdbCommand()));
    }
}
