package ee.edio.garmin;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.intellij.compiler.CompilerWorkspaceConfiguration;
import com.intellij.execution.RunManager;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.ide.util.projectWizard.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.DumbAwareRunnable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkTypeId;
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.ExternalChangeAction;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.DocumentUtil;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.GenericAttributeValue;
import ee.edio.garmin.configuration.TargetDeviceModuleExtension;
import ee.edio.garmin.dom.manifest.Manifest;
import ee.edio.garmin.dom.manifest.Products;
import ee.edio.garmin.dom.sdk.projectinfo.NewProjectFileMap;
import ee.edio.garmin.dom.sdk.projectinfo.ProjectInfo;
import ee.edio.garmin.module.MonkeyModuleWizardStep;
import ee.edio.garmin.runconfig.MonkeyConfigurationType;
import ee.edio.garmin.runconfig.MonkeyModuleBasedConfiguration;
import ee.edio.garmin.runconfig.TargetDevice;
import ee.edio.garmin.sdk.MonkeySdkType;
import ee.edio.garmin.util.ExternalTemplateUtil;
import org.apache.commons.lang.WordUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.java.JavaResourceRootType;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import static ee.edio.garmin.MonkeyUtil.loadDomElement;
public class MonkeyModuleBuilder extends JavaModuleBuilder implements SourcePathsBuilder, ModuleBuilderListener {
private static final Logger LOG = Logger.getInstance("#ee.edio.garmin.MonkeyModuleBuilder");
public static final String MANIFEST_XML = "manifest.xml";
public static final String PROJECT_INFO_XML = "projectInfo.xml";
public static final String FILE_TYPE_SOURCE = "source";
public static final TargetDevice DEFAULT_TARGET_DEVICE = TargetDevice.SQUARE_WATCH;
private final String appType;
public MonkeyModuleBuilder(String appType) {
this.appType = appType;
}
@Override
public void setupRootModel(ModifiableRootModel rootModel) throws ConfigurationException {
addListener(this);
final Module module = rootModel.getModule();
final MonkeySdkType sdkType = MonkeySdkType.getInstance();
// this code from here....
Sdk sdk = findAndSetSdk(module, sdkType);
final List<Pair<String, String>> sourcePaths = constructSourcePaths();
setSourcePaths(sourcePaths);
super.setupRootModel(rootModel);
final String contentEntryPath = getContentEntryPath();
final VirtualFile moduleContentRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(contentEntryPath.replace('\\', '/'));
final ContentEntry[] contentEntries = rootModel.getContentEntries();
if (contentEntries != null && contentEntries.length == 1) {
final ContentEntry contentEntry = contentEntries[0];
final String path = getContentEntryPath() + File.separator + "resources";
new File(path).mkdirs();
final VirtualFile sourceRoot = LocalFileSystem.getInstance()
.refreshAndFindFileByPath(FileUtil.toSystemIndependentName(path));
// TODO: there can be many resource folders, e.g based on language or device that come from SDK's example project definitions
contentEntry.addSourceFolder(sourceRoot, JavaResourceRootType.RESOURCE, JavaResourceRootType.RESOURCE.createDefaultProperties());
}
// ... to here is just awful. TODO: Get rid of extending JavaModuleBuilder
final TargetDeviceModuleExtension targetDeviceModuleExtension = rootModel.getModuleExtension(TargetDeviceModuleExtension.class);
targetDeviceModuleExtension.setTargetDevice(DEFAULT_TARGET_DEVICE);
final Project project = rootModel.getProject();
VirtualFile[] files = rootModel.getContentRoots();
if (files.length > 0) {
final VirtualFile contentRoot = files[0];
StartupManager.getInstance(project).runWhenProjectIsInitialized(new DumbAwareRunnable() {
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
DocumentUtil.writeInRunUndoTransparentAction(new Runnable() {
@Override
public void run() {
createProject(project, contentRoot, module);
}
});
}
});
}
});
}
}
private List<Pair<String, String>> constructSourcePaths() {
final List<Pair<String, String>> paths = new ArrayList<Pair<String, String>>();
@NonNls final String path = getContentEntryPath() + File.separator + "source";
new File(path).mkdirs();
paths.add(Pair.create(path, ""));
return paths;
}
@Override
protected boolean isAvailable() {
return false;
}
private void createProject(Project project, VirtualFile contentRoot, Module module) {
final MonkeySdkType sdkType = MonkeySdkType.getInstance();
Sdk sdk = findAndSetSdk(module, sdkType);
VirtualFile sdkBinDir = sdkType.getBinDir(sdk);
createResourcesAndLibs(module, contentRoot, sdkBinDir);
fillTemplates(module, contentRoot);
setupRunConfiguration(module);
}
private Sdk findAndSetSdk(Module module, MonkeySdkType sdkType) {
Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
if (sdk == null) {
sdk = ProjectRootManager.getInstance(module.getProject()).getProjectSdk();
}
if (sdk == null) {
Comparator<Sdk> preferredSdkComparator = new Comparator<Sdk>() {
@Override
public int compare(Sdk o1, Sdk o2) {
if (o1.getSdkType() instanceof MonkeySdkType) {
return 1;
} else if (o2.getSdkType() instanceof MonkeySdkType) {
return -1;
}
return 0;
}
};
SdkConfigurationUtil.configureDirectoryProjectSdk(module.getProject(), preferredSdkComparator, sdkType);
sdk = ProjectRootManager.getInstance(module.getProject()).getProjectSdk();
}
return sdk;
}
private void createResourcesAndLibs(final Module module, final VirtualFile rootDir, VirtualFile sdkBinDir) {
final Project project = module.getProject();
ProjectInfo sdkProjectInfo = getSdkProjectInfo(project, sdkBinDir);
List<NewProjectFileMap> newProjectFileMaps = sdkProjectInfo.getNewProjectFilesMaps().getNewProjectFileMaps();
final NewProjectFileMap newProjectFileMap = Iterables.find(newProjectFileMaps, new Predicate<NewProjectFileMap>() {
@Override
public boolean apply(@Nullable NewProjectFileMap newProjectFileMap) {
return newProjectFileMap != null && appType.equals(newProjectFileMap.getAppType().getStringValue());
}
});
final GenericAttributeValue<String> baseDir = newProjectFileMap.getBaseDir(); // e.g templates/watch-app/simple
String appName = module.getName(); // used in resources.xml to set freeform app name
String sanitizedName = WordUtils.capitalize(appName);
Map<String, String> substitutions = new HashMap<>();
substitutions.put("appName", appName);
substitutions.put("appClassName", sanitizedName + "App");
substitutions.put("viewClassName", sanitizedName + "View");
substitutions.put("delegateClassName", sanitizedName + "Delegate");
substitutions.put("menuDelegateClassName", sanitizedName + "MenuDelegate");
VelocityContext context = new VelocityContext();
for (Map.Entry<String, String> substitution : substitutions.entrySet()) {
context.put(substitution.getKey(), substitution.getValue());
}
try {
for (ee.edio.garmin.dom.sdk.projectinfo.File file : newProjectFileMap.getFiles()) {
final String relativeFilePath = file.getValue(); // e.g resources/images/launcher_icon.png
String toRelativeFilePath = relativeFilePath;
// source files are prefixed with app name
if (FILE_TYPE_SOURCE.equals(file.getType().getStringValue())) {
final Path relativeFilePathPath = Paths.get(relativeFilePath);
final Path fileName = relativeFilePathPath.getFileName(); // e.g App.mc
final Path parent = relativeFilePathPath.getParent(); // e.g source
toRelativeFilePath = parent + "/" + sanitizedName + fileName;
}
final VirtualFile templateFile = sdkBinDir.findFileByRelativePath(baseDir + "/" + relativeFilePath);
// this handles creating child directories as well
VirtualFile newFile = VfsUtil.copyFileRelative(project, templateFile, rootDir, toRelativeFilePath);
if (!newFile.getFileType().isBinary()) {
String content = getParsedFileContent(context, newFile);
VfsUtil.saveText(newFile, content);
}
}
} catch (IOException e) {
LOG.error(e);
}
}
private String getParsedFileContent(Context context, VirtualFile file) throws FileNotFoundException {
FileReader fileReader = new FileReader(VfsUtil.virtualToIoFile(file));
final VelocityEngine velocityEngine = ExternalTemplateUtil.getEngine();
StringWriter writer = new StringWriter();
velocityEngine.evaluate(context, writer, "Monkey", fileReader);
return writer.toString();
}
private void fillTemplates(final Module module, final VirtualFile contentRoot) {
final Project project = module.getProject();
CommandProcessor.getInstance().executeCommand(project, new ExternalChangeAction() {
@Override
public void run() {
Runnable action = new Runnable() {
@Override
public void run() {
final Manifest manifest = getManifest(project, contentRoot);
if (manifest != null) {
StartupManager.getInstance(project).runWhenProjectIsInitialized(new Runnable() {
@Override
public void run() {
FileDocumentManager.getInstance().saveAllDocuments();
}
});
configureManifest(manifest, module, appType);
}
}
};
ApplicationManager.getApplication().runWriteAction(action);
}
}, "Create project", null);
}
private static void configureManifest(Manifest manifest, Module module, String appType) {
if (appType == null) {
throw new RuntimeException("app type is null");
}
final PsiFile manifestFile = getValidatedPsiFile(manifest);
if (manifestFile == null) {
return;
}
String applicationId = MonkeyUtil.generateProjectId();
String entryClassName = WordUtils.capitalize(module.getName()) + "App";
manifest.getApplication().getId().setValue(applicationId);
manifest.getApplication().getType().setValue(appType);
manifest.getApplication().getName().setValue("AppName");
// entry is a class which extends Toybox.Application.AppBase
manifest.getApplication().getEntry().setValue(entryClassName);
manifest.getApplication().getLauncherIcon().setValue("LauncherIcon");
Products products = manifest.getApplication().getProducts();
XmlTag productsRootTag = products.getXmlTag();
XmlTag productTag = productsRootTag.createChildTag("product", productsRootTag.getNamespace(), "", false);
productTag = productsRootTag.addSubTag(productTag, true);
productTag.setAttribute("id", null, DEFAULT_TARGET_DEVICE.getId());
productTag.collapseIfEmpty();
CodeStyleManager.getInstance(manifestFile.getProject()).reformat(manifestFile);
}
private static PsiFile getValidatedPsiFile(DomElement domElement) {
final XmlTag rootTag = domElement.getXmlTag();
if (rootTag == null) {
return null;
}
final PsiFile psiFile = rootTag.getContainingFile();
if (psiFile == null) {
return null;
}
final VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile == null ||
!ReadonlyStatusHandler.ensureFilesWritable(psiFile.getProject(), virtualFile)) {
return null;
}
return psiFile;
}
private static ProjectInfo getSdkProjectInfo(Project project, VirtualFile sdkBinDir) {
final VirtualFile projectInfoFile = sdkBinDir.findChild(PROJECT_INFO_XML);
return projectInfoFile != null ? loadDomElement(project, projectInfoFile, ProjectInfo.class) : null;
}
private static Manifest getManifest(Project project, VirtualFile contentRoot) {
VirtualFile manifestFile = contentRoot.findChild(MANIFEST_XML);
return manifestFile != null ? loadDomElement(project, manifestFile, Manifest.class) : null;
}
private void setupRunConfiguration(Module module) {
final Project project = module.getProject();
final RunManager runManager = RunManager.getInstance(project);
final ConfigurationFactory configurationFactory = MonkeyConfigurationType.getInstance().getFactory();
final RunnerAndConfigurationSettings settings = runManager.createRunConfiguration(module.getName(), configurationFactory);
final MonkeyModuleBasedConfiguration configuration = (MonkeyModuleBasedConfiguration) settings.getConfiguration();
configuration.setModule(module);
configuration.setTargetDeviceId(DEFAULT_TARGET_DEVICE.getId());
runManager.addConfiguration(settings, false);
runManager.setSelectedConfiguration(settings);
}
@NotNull
@Override
public ModuleType getModuleType() {
return MonkeyModuleType.getInstance();
}
@Override
public boolean isSuitableSdkType(SdkTypeId sdkType) {
return sdkType == MonkeySdkType.getInstance();
}
@Override
public void moduleCreated(@NotNull Module module) {
CompilerWorkspaceConfiguration.getInstance(module.getProject()).CLEAR_OUTPUT_DIRECTORY = false;
}
@Override
public ModuleWizardStep[] createWizardSteps(@NotNull WizardContext context, @NotNull ModulesProvider modulesProvider) {
return new ModuleWizardStep[]{new MonkeyModuleWizardStep(this, context)};
}
}