/*
 * Copyright (C) 2015 RoboVM AB
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
 */
package org.robovm.idea;

import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.robovm.compiler.Version;
import org.robovm.compiler.config.Arch;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.Resource;
import org.robovm.compiler.log.Logger;
import org.robovm.compiler.util.InfoPList;
import org.robovm.idea.compilation.RoboVmCompileTask;
import org.robovm.idea.config.RoboVmGlobalConfig;
import org.robovm.idea.interfacebuilder.RoboVmFileEditorManagerListener;
import org.robovm.idea.sdk.RoboVmSdkType;

import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompileScope;
import com.intellij.openapi.compiler.CompileTask;
import com.intellij.openapi.compiler.CompilerManager;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.util.ui.UIUtil;

/**
 * Provides util for the other components of the plugin such
 * as logging.
 */
public class RoboVmPlugin {
    public enum OS {
        MacOsX,
        Windows,
        Linux
    }

    static {
        if(System.getProperty("os.name").contains("Mac")) {
            os = OS.MacOsX;
        } else if(System.getProperty("os.name").contains("Windows")) {
            os = OS.Windows;
        } else if(System.getProperty("os.name").contains("Linux")) {
            os = OS.Linux;
        }
    }

    private static final String ROBOVM_TOOLWINDOW_ID = "RoboVM";
    private static OS os;
    static volatile Map<Project, ConsoleView> consoleViews = new ConcurrentHashMap<>();
    static volatile Map<Project, ToolWindow> toolWindows = new ConcurrentHashMap<>();
    static volatile Map<Project, VirtualFileListener> fileListeners = new ConcurrentHashMap<>();
    static final List<UnprintedMessage> unprintedMessages = new ArrayList<UnprintedMessage>();

    public static OS getOs() {
        return os;
    }

    public static String getBestAndroidSdkVersion() {
        int androidSdkVersion = 0;
        File androidSdkDir = getBestAndroidSdkDir();
        if(androidSdkDir == null) {
            return "23";
        }
        File platformsDir = new File(androidSdkDir, "platforms");
        for(File file: platformsDir.listFiles()) {
            String[] tokens = file.getName().split("-");
            if(tokens.length == 2) {
                try {
                    int version = Integer.parseInt(tokens[1]);
                    if(version > androidSdkVersion) {
                        androidSdkVersion = version;
                    }
                } catch(NumberFormatException e) {
                    // nothing we can do
                }
            }
        }
        if(androidSdkVersion == 0) {
            return "23";
        } else {
            return Integer.toString(androidSdkVersion);
        }
    }

    public static String getBestAndroidBuildToolsVersion() {
        int androidBuildToolsVersion = 0;
        String androidBuildToolsVersionString = "";
        File androidSdkDir = getBestAndroidSdkDir();
        if(androidSdkDir == null) {
            return "23.0.1";
        }
        File platformsDir = new File(androidSdkDir, "build-tools");
        for(File file: platformsDir.listFiles()) {
            String[] tokens = file.getName().split("\\.");
            if(tokens.length == 3) {
                try {
                    int version = Integer.parseInt(tokens[0]) * 1000 * 1000 +
                                  Integer.parseInt(tokens[1]) * 1000 +
                                  Integer.parseInt(tokens[2]);
                    if(version > androidBuildToolsVersion) {
                        androidBuildToolsVersion = version;
                        androidBuildToolsVersionString = file.getName();
                    }
                } catch(NumberFormatException e) {
                    // nothing we can do
                }
            }
        }
        if(androidBuildToolsVersion == 0) {
            return "23.0.1";
        } else {
            return androidBuildToolsVersionString;
        }
    }

    public static File getBestAndroidSdkDir() {
        Sdk bestSdk = null;
        for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
            if (sdk.getSdkType().getName().equals("Android SDK")) {
                if(sdk.getHomePath().contains("/Library/RoboVM/")) {
                    return new File(sdk.getHomePath());
                } else {
                    bestSdk = sdk;
                }
            }
        }
        return new File(bestSdk.getHomePath());
    }


    public static boolean isAndroidSdkInstalled(String sdkDir) {
        File sdk = new File(sdkDir, os == OS.Windows? "tools/android.bat": "tools/android");
        return sdk.exists();
    }

    public static boolean isAndroidSdkSetup() {
        for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
            if (sdk.getSdkType().getName().equals("Android SDK")) {
                return true;
            }
        }
        return false;
    }

    public static boolean areAndroidComponentsInstalled(String sdkDir) {
        return new File(sdkDir, "platforms").list().length > 0;
    }

    static class UnprintedMessage {
        final String string;
        final ConsoleViewContentType type;

        public UnprintedMessage(String string, ConsoleViewContentType type) {
            this.string = string;
            this.type = type;
        }
    }

    public static void logBalloon(final Project project, final MessageType messageType, final String message) {
        UIUtil.invokeLaterIfNeeded(new Runnable() {
            @Override
            public void run() {
                if (project != null) {
                    // this may throw an exception, see #88. It appears to be a timing
                    // issue
                    try {
                        ToolWindowManager.getInstance(project).notifyByBalloon(ROBOVM_TOOLWINDOW_ID, MessageType.ERROR, message);
                    } catch (Throwable t) {
                        logError(project, message, t);
                    }
                }
            }
        });
    }

    public static void logInfo(Project project, String format, Object... args) {
        log(project, ConsoleViewContentType.SYSTEM_OUTPUT, "[INFO] " + format, args);
    }

    public static void logError(Project project, String format, Object... args) {
        log(project, ConsoleViewContentType.ERROR_OUTPUT, "[ERROR] " + format, args);
    }

    public static void logErrorThrowable(Project project, String s, Throwable t, boolean showBalloon) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);
        t.printStackTrace(writer);
        log(project, ConsoleViewContentType.ERROR_OUTPUT, "[ERROR] %s\n%s", s, stringWriter.toString());
        logBalloon(project, MessageType.ERROR, s);
    }

    public static void logWarn(Project project, String format, Object... args) {
        log(project, ConsoleViewContentType.ERROR_OUTPUT, "[WARNING] " + format, args);
    }

    public static void logDebug(Project project, String format, Object... args) {
        log(project, ConsoleViewContentType.NORMAL_OUTPUT, "[DEBUG] " + format, args);
    }

    private static void log(final Project project, final ConsoleViewContentType type, String format, Object... args) {
        final String s = String.format(format, args) + "\n";
        UIUtil.invokeLaterIfNeeded(new Runnable() {
            @Override
            public void run() {
                ConsoleView consoleView = project == null ? null : consoleViews.get(project);
                if (consoleView != null) {
                    for (UnprintedMessage unprinted : unprintedMessages) {
                        consoleView.print(unprinted.string, unprinted.type);
                    }
                    unprintedMessages.clear();
                    consoleView.print(s, type);
                } else {
                    unprintedMessages.add(new UnprintedMessage(s, type));
                    if (type == ConsoleViewContentType.ERROR_OUTPUT) {
                        System.err.print(s);
                    } else {
                        System.out.print(s);
                    }
                }
            }
        });
    }

    public static Logger getLogger(final Project project) {
        return new Logger() {
            @Override
            public void debug(String s, Object... objects) {
                logDebug(project, s, objects);
            }

            @Override
            public void info(String s, Object... objects) {
                logInfo(project, s, objects);
            }

            @Override
            public void warn(String s, Object... objects) {
                logWarn(project, s, objects);
            }

            @Override
            public void error(String s, Object... objects) {
                logError(project, s, objects);
            }
        };
    }

    public static void initializeProject(final Project project) {
        // setup a compile task if there isn't one yet
        boolean found = false;
        for (CompileTask task : CompilerManager.getInstance(project).getAfterTasks()) {
            if (task instanceof RoboVmCompileTask) {
                found = true;
                break;
            }
        }
        if (!found) {
            CompilerManager.getInstance(project).addAfterTask(new RoboVmCompileTask());
        }

        // hook ito the message bus so we get to know if a storyboard/xib
        // file is opened
        project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new RoboVmFileEditorManagerListener(project));

        // initialize our tool window to which we
        // log all messages
        UIUtil.invokeLaterIfNeeded(new Runnable() {
            @Override
            public void run() {
                if (project.isDisposed()) {
                    return;
                }
                ToolWindow toolWindow = ToolWindowManager.getInstance(project).registerToolWindow(ROBOVM_TOOLWINDOW_ID, false, ToolWindowAnchor.BOTTOM, project, true);
                ConsoleView consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole();
                Content content = toolWindow.getContentManager().getFactory().createContent(consoleView.getComponent(), "Console", true);
                toolWindow.getContentManager().addContent(content);
                toolWindow.setIcon(RoboVmIcons.ROBOVM_SMALL);
                consoleViews.put(project, consoleView);
                toolWindows.put(project, toolWindow);
                logInfo(project, "RoboVM plugin initialized");
            }
        });

        // initialize virtual file change listener so we can
        // trigger recompiles on file saves
        VirtualFileListener listener = new VirtualFileAdapter() {
            @Override
            public void contentsChanged(@NotNull VirtualFileEvent event) {
                compileIfChanged(event, project);
            }
        };
        VirtualFileManager.getInstance().addVirtualFileListener(listener);
        fileListeners.put(project, listener);
    }

    private static void compileIfChanged(VirtualFileEvent event, final Project project) {
        if(!RoboVmGlobalConfig.isCompileOnSave()) {
            return;
        }
        VirtualFile file = event.getFile();
        Module module = null;
        for(Module m: ModuleManager.getInstance(project).getModules()) {
            if(ModuleRootManager.getInstance(m).getFileIndex().isInContent(file)) {
                module = m;
                break;
            }
        }

        if(module != null) {
            if(isRoboVmModule(module)) {
                final Module foundModule = module;
                OrderEntry orderEntry = ModuleRootManager.getInstance(module).getFileIndex().getOrderEntryForFile(file);
                if(orderEntry != null && orderEntry.getFiles(OrderRootType.SOURCES).length != 0) {
                    ApplicationManager.getApplication().invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            if(!CompilerManager.getInstance(project).isCompilationActive()) {
                                CompileScope scope = CompilerManager.getInstance(project).createModuleCompileScope(foundModule, true);
                                CompilerManager.getInstance(project).compile(scope, null);
                            }
                        }
                    });
                }
            }
        }
    }

    public static void unregisterProject(Project project) {
        ConsoleView consoleView = consoleViews.remove(project);
        if (consoleView != null) {
            consoleView.dispose();
        }
        toolWindows.remove(project);
        ToolWindowManager.getInstance(project).unregisterToolWindow(ROBOVM_TOOLWINDOW_ID);
        VirtualFileManager.getInstance().removeVirtualFileListener(fileListeners.remove(project));
    }

    public static void extractSdk() {
        File sdkHome = getSdkHomeBase();
        if (!sdkHome.exists()) {
            if (!sdkHome.mkdirs()) {
                logError(null, "Couldn't create sdk dir in %s", sdkHome.getAbsolutePath());
                throw new RuntimeException("Couldn't create sdk dir in " + sdkHome.getAbsolutePath());
            }
        }
        extractArchive("robovm-dist", sdkHome);

        // create an SDK if it doesn't exist yet
        RoboVmSdkType.createSdkIfNotExists();
    }

    public static File getSdkHome() {
        File sdkHome = new File(getSdkHomeBase(), "robovm-" + Version.getVersion());
        return sdkHome;
    }

    public static File getSdkHomeBase() {
        return new File(System.getProperty("user.home"), ".robovm-sdks");
    }

    public static Sdk getSdk() {
        RoboVmSdkType sdkType = new RoboVmSdkType();
        for(Sdk sdk: ProjectJdkTable.getInstance().getAllJdks()) {
            if(sdkType.suggestSdkName(null, null).equals(sdk.getName())) {
                return sdk;
            }
        }
        return null;
    }

    private static void extractArchive(String archive, File dest) {
        archive = "/" + archive;
        TarArchiveInputStream in = null;
        boolean isSnapshot = Version.getVersion().toLowerCase().contains("snapshot");
        try {
            in = new TarArchiveInputStream(new GZIPInputStream(RoboVmPlugin.class.getResourceAsStream(archive)));
            ArchiveEntry entry = null;
            while ((entry = in.getNextEntry()) != null) {
                File f = new File(dest, entry.getName());
                if (entry.isDirectory()) {
                    f.mkdirs();
                } else {
                    if(!isSnapshot && f.exists()) {
                        continue;
                    }
                    f.getParentFile().mkdirs();
                    OutputStream out = null;
                    try {
                        out = new FileOutputStream(f);
                        IOUtils.copy(in, out);
                    } finally {
                        IOUtils.closeQuietly(out);
                    }
                }
            }
            logInfo(null, "Installed RoboVM SDK %s to %s", Version.getVersion(), dest.getAbsolutePath());

            // make all files in bin executable
            for (File file : new File(getSdkHome(), "bin").listFiles()) {
                file.setExecutable(true);
            }
        } catch (Throwable t) {
            logError(null, "Couldn't extract SDK to %s", dest.getAbsolutePath());
            throw new RuntimeException("Couldn't extract SDK to " + dest.getAbsolutePath(), t);
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * @return all sdk runtime libraries and their source jars
     */
    public static List<File> getSdkLibraries() {
        List<File> libs = new ArrayList<File>();
        File libsDir = new File(getSdkHome(), "lib");
        for (File file : libsDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".jar");
            }
        })) {
            libs.add(file);
        }
        return libs;
    }

    /**
     * @return the source jars of all runtime libraries
     */
    public static List<File> getSdkLibrariesWithoutSources() {
        List<File> libs = getSdkLibraries();
        Iterator<File> iter = libs.iterator();
        while(iter.hasNext()) {
            File file = iter.next();
            if(file.getName().endsWith("-sources.jar")) {
                iter.remove();
            }
        }
        return libs;
    }

    /**
     * @return the source jars of all runtime libraries
     */
    public static List<File> getSdkLibrarySources() {
        List<File> libs = getSdkLibraries();
        Iterator<File> iter = libs.iterator();
        while(iter.hasNext()) {
            File file = iter.next();
            if(!file.getName().endsWith("-sources.jar")) {
                iter.remove();
            }
        }
        return libs;
    }

    public static Config.Home getRoboVmHome() {
        try {
            return Config.Home.find();
        } catch(Throwable t) {
            return new Config.Home(getSdkHome());
        }
    }

    public static List<Module> getRoboVmModules(Project project) {
        List<Module> validModules = new ArrayList<Module>();
        for (Module module : ModuleManager.getInstance(project).getModules()) {
            if (isRoboVmModule(module)) {
                validModules.add(module);
            }
        }
        return validModules;
    }

    public static boolean isRoboVmModule(Module module) {
        // HACK! to identify if the module uses a robovm sdk
        if (ModuleRootManager.getInstance(module).getSdk() != null) {
            if (ModuleRootManager.getInstance(module).getSdk().getSdkType().getName().toLowerCase().contains("robovm")) {
                return true;
            }
        }

        // check if there's any RoboVM RT libs in the classpath
        OrderEnumerator classes = ModuleRootManager.getInstance(module).orderEntries().recursively().withoutSdk().compileOnly();
        for (String path : classes.getPathsList().getPathList()) {
            if (isSdkLibrary(path)) {
                return true;
            }
        }

        // check if there's a robovm.xml file in the root of the module
        for(VirtualFile file: ModuleRootManager.getInstance(module).getContentRoots()) {
            if(file.findChild("robovm.xml") != null) {
                return true;
            }
        }

        return false;
    }

    public static void focusToolWindow(final Project project) {
        UIUtil.invokeLaterIfNeeded(new Runnable() {
            @Override
            public void run() {
                ToolWindow toolWindow = toolWindows.get(project);
                if(toolWindow != null) {
                    toolWindow.show(new Runnable() {
                        @Override
                        public void run() {

                        }
                    });
                }
            }
        });
    }

    public static File getModuleLogDir(Module module) {
        File logDir = new File(getModuleBaseDir(module), "robovm-build/logs/");
        if (!logDir.exists()) {
            if (!logDir.mkdirs()) {
                throw new RuntimeException("Couldn't create log dir '" + logDir.getAbsolutePath() + "'");
            }
        }
        return logDir;
    }

    public static File getModuleXcodeDir(Module module) {
        File buildDir = new File(getModuleBaseDir(module), "robovm-build/xcode/");
        if (!buildDir.exists()) {
            if (!buildDir.mkdirs()) {
                throw new RuntimeException("Couldn't create build dir '" + buildDir.getAbsolutePath() + "'");
            }
        }
        return buildDir;
    }

    public static File getModuleBuildDir(Module module, String runConfigName, org.robovm.compiler.config.OS os, Arch arch) {
        File buildDir = new File(getModuleBaseDir(module), "robovm-build/tmp/" + runConfigName + "/" + os + "/" + arch);
        if (!buildDir.exists()) {
            if (!buildDir.mkdirs()) {
                throw new RuntimeException("Couldn't create build dir '" + buildDir.getAbsolutePath() + "'");
            }
        }
        return buildDir;
    }

    public static File getModuleClassesDir(String moduleBaseDir) {
        File classesDir = new File(moduleBaseDir, "robovm-build/classes/");
        if(!classesDir.exists()) {
            if (!classesDir.mkdirs()) {
                throw new RuntimeException("Couldn't create classes dir '" + classesDir.getAbsolutePath() + "'");
            }
        }
        return classesDir;
    }

    public static File getModuleBaseDir(Module module) {
        return new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
    }

    public static Set<File> getModuleResourcePaths(Module module) {
        try {
            File moduleBaseDir = new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
            Config.Builder configBuilder = new Config.Builder();
            configBuilder.home(RoboVmPlugin.getRoboVmHome());
            configBuilder.addClasspathEntry(new File(".")); // Fake a classpath to make Config happy
            configBuilder.skipLinking(true);
            RoboVmCompileTask.loadConfig(module.getProject(), configBuilder, moduleBaseDir, false);
            Config config = configBuilder.build();
            Set<File> paths = new HashSet<>();
            for (Resource r : config.getResources()) {
                if (r.getPath() != null) {
                    if (r.getPath().exists() && r.getPath().isDirectory()) {
                        paths.add(r.getPath());
                    }
                } else if (r.getDirectory() != null) {
                    if (r.getDirectory().exists() && r.getDirectory().isDirectory()) {
                        paths.add(r.getDirectory());
                    }
                }
            }
            return paths;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Module isRoboVmModuleResourcePath(Project project, VirtualFile file) {
        try {
            // using reflection here as building the config takes an
            // immense amount of time
            Field field = Config.Builder.class.getDeclaredField("config");
            field.setAccessible(true);

            for (Module module : ModuleManager.getInstance(project).getModules()) {
                File moduleBaseDir = new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
                Config.Builder builder = new Config.Builder();
                builder.home(RoboVmPlugin.getRoboVmHome());
                builder.addClasspathEntry(new File(".")); // Fake a classpath to make Config happy
                builder.skipLinking(true);
                builder.readProjectProperties(moduleBaseDir, false);
                builder.readProjectConfig(moduleBaseDir, false);
                Config config = (Config)field.get(builder);
                for(Resource res: config.getResources()) {
                    if(new File(file.getCanonicalPath()).getAbsolutePath().startsWith(res.getDirectory().getAbsolutePath())) {
                        return module;
                    }
                }
            }
            return null;
        } catch(Throwable t) {
            return null;
        }
    }

    public static File getModuleInfoPlist(Module module) {
        try {
            File projectRoot = getModuleBaseDir(module);
            Config.Builder configBuilder = new Config.Builder();
            configBuilder.home(RoboVmPlugin.getRoboVmHome());
            // Fake a classpath to make Config happy
            configBuilder.addClasspathEntry(new File("."));
            configBuilder.skipLinking(true);
            RoboVmCompileTask.loadConfig(module.getProject(), configBuilder, projectRoot, false);
            Config config = configBuilder.build();
            InfoPList iosInfoPList = config.getIosInfoPList();
            if(iosInfoPList != null) return iosInfoPList.getFile();
            else return null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean isSdkLibrary(String path) {
        String name = new File(path).getName();

        return name.startsWith("robovm-rt") ||
                name.startsWith("robovm-objc") ||
                name.startsWith("robovm-cocoatouch") ||
                name.startsWith("robovm-cacerts");
    }

    public static boolean isBootClasspathLibrary(File path) {
        return path.getName().startsWith("robovm-rt");
    }
}