/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed 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 com.intellij.platform.templates;

import com.intellij.codeStyle.CodeStyleFacade;
import com.intellij.diagnostic.LogMessageEx;
import com.intellij.execution.RunManager;
import com.intellij.execution.configurations.ModuleBasedConfiguration;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.fileTemplates.FileTemplateUtil;
import com.intellij.ide.util.projectWizard.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.module.*;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.RefreshQueue;
import com.intellij.platform.templates.github.ZipUtil;
import com.intellij.util.Consumer;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.apache.velocity.exception.VelocityException;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.serialization.PathMacroUtil;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipInputStream;

/**
* @author Dmitry Avdeev
*         Date: 10/19/12
*/
public class TemplateModuleBuilder extends ModuleBuilder {

  private final ModuleType myType;
  private List<WizardInputField> myAdditionalFields;
  private ArchivedProjectTemplate myTemplate;
  private boolean myProjectMode;

  public TemplateModuleBuilder(ArchivedProjectTemplate template, ModuleType moduleType, List<WizardInputField> additionalFields) {
    myTemplate = template;
    myType = moduleType;
    myAdditionalFields = additionalFields;
  }

  @Override
  public void setupRootModel(ModifiableRootModel modifiableRootModel) throws ConfigurationException {

  }

  @Override
  public ModuleWizardStep[] createWizardSteps(@NotNull WizardContext wizardContext, @NotNull ModulesProvider modulesProvider) {
    ModuleBuilder builder = myType.createModuleBuilder();
    return builder.createWizardSteps(wizardContext, modulesProvider);
  }

  @Override
  protected List<WizardInputField> getAdditionalFields() {
    return myAdditionalFields;
  }

  @Override
  public Module commitModule(@NotNull final Project project, ModifiableModuleModel model) {
    if (myProjectMode) {
      final Module[] modules = ModuleManager.getInstance(project).getModules();
      if (modules.length > 0) {
        final Module module = modules[0];
        ApplicationManager.getApplication().runWriteAction(() -> {
          try {
            setupModule(module);
          }
          catch (ConfigurationException e) {
            LOG.error(e);
          }
        });

        StartupManager.getInstance(project).registerPostStartupActivity(() -> ApplicationManager.getApplication().runWriteAction(() -> {
          try {
            ModifiableModuleModel modifiableModuleModel = ModuleManager.getInstance(project).getModifiableModel();
            modifiableModuleModel.renameModule(module, module.getProject().getName());
            modifiableModuleModel.commit();
            fixModuleName(module);
          }
          catch (ModuleWithNameAlreadyExists exists) {
            // do nothing
          }
        }));
        return module;
      }
      return null;
    }
    else {
      return super.commitModule(project, model);
    }
  }

  @Nullable
  @Override
  public String getBuilderId() {
    return myTemplate.getName();
  }

  @Override
  public ModuleType getModuleType() {
    return myType;
  }

  @Override
  public Icon getNodeIcon() {
    return myTemplate.getIcon();
  }

  @Override
  public boolean isTemplateBased() {
    return true;
  }

  @NotNull
  @Override
  public Module createModule(@NotNull ModifiableModuleModel moduleModel)
    throws InvalidDataException, IOException, ModuleWithNameAlreadyExists, JDOMException, ConfigurationException {
    final String path = getContentEntryPath();
    final ExistingModuleLoader loader = ExistingModuleLoader.setUpLoader(getModuleFilePath());
    unzip(loader.getName(), path, true, null, true);
    Module module = loader.createModule(moduleModel);
    if (myProjectMode) {
      moduleModel.renameModule(module, module.getProject().getName());
    }
    fixModuleName(module);
    return module;
  }

  private void fixModuleName(Module module) {
    List<RunConfiguration> configurations = RunManager.getInstance(module.getProject()).getAllConfigurationsList();
    for (RunConfiguration configuration : configurations) {
      if (configuration instanceof ModuleBasedConfiguration) {
        ((ModuleBasedConfiguration)configuration).getConfigurationModule().setModule(module);
      }
    }
    ModifiableRootModel model = ModuleRootManager.getInstance(module).getModifiableModel();
    for (WizardInputField field : myAdditionalFields) {
      ProjectTemplateParameterFactory factory = WizardInputField.getFactoryById(field.getId());
      factory.applyResult(field.getValue(), model);
    }
    model.commit();
  }

  private WizardInputField getBasePackageField() {
    for (WizardInputField field : getAdditionalFields()) {
      if (ProjectTemplateParameterFactory.IJ_BASE_PACKAGE.equals(field.getId())) {
        return field;
      }
    }
    return null;
  }

  private void unzip(final @Nullable String projectName,
                     String path,
                     final boolean moduleMode,
                     @Nullable ProgressIndicator pI,
                     boolean reportFailuresWithDialog) {
    final WizardInputField basePackage = getBasePackageField();
    try {
      final File dir = new File(path);
      class ExceptionConsumer implements Consumer<VelocityException> {
        private String myPath;
        private String myText;
        private SmartList<Trinity<String, String, VelocityException>> myFailures = new SmartList<>();

        @Override
        public void consume(VelocityException e) {
          myFailures.add(Trinity.create(myPath, myText, e));
        }

        private void setCurrentFile(String path, String text) {
          myPath = path;
          myText = text;
        }

        private void reportFailures() {
          if (myFailures.isEmpty()) {
            return;
          }

          if(reportFailuresWithDialog) {
            String dialogMessage;
            if (myFailures.size() == 1) {
              dialogMessage = "Failed to decode file \'" + myFailures.get(0).getFirst() + "\'";
            }
            else {
              StringBuilder dialogMessageBuilder = new StringBuilder();
              dialogMessageBuilder.append("Failed to decode files: \n");
              for (Trinity<String, String, VelocityException> failure : myFailures) {
                dialogMessageBuilder.append(failure.getFirst()).append("\n");
              }
              dialogMessage = dialogMessageBuilder.toString();
            }
            Messages.showErrorDialog(dialogMessage, "Decoding Template");
          }

          StringBuilder reportBuilder = new StringBuilder();
          for (Trinity<String, String, VelocityException> failure : myFailures) {
            reportBuilder.append("File: ").append(failure.getFirst()).append("\n");
            reportBuilder.append("Exception:\n").append(ExceptionUtil.getThrowableText(failure.getThird())).append("\n");
            reportBuilder.append("File content:\n\'").append(failure.getSecond()).append("\'\n");
            reportBuilder.append("\n===========================================\n");
          }

          LOG.error(LogMessageEx.createEvent("Cannot decode files in template", "",
                                             new Attachment("Files in template", reportBuilder.toString())));
        }
      }
      ExceptionConsumer consumer = new ExceptionConsumer();

      List<File> filesToRefresh = new ArrayList<>();
      myTemplate.processStream(new ArchivedProjectTemplate.StreamProcessor<Void>() {
        @Override
        public Void consume(@NotNull ZipInputStream stream) throws IOException {
          ZipUtil.unzip(ProgressManager.getInstance().getProgressIndicator(), dir, stream, path1 -> {
            if (moduleMode && path1.contains(Project.DIRECTORY_STORE_FOLDER)) {
              return null;
            }
            if (basePackage != null) {
              return path1.replace(getPathFragment(basePackage.getDefaultValue()), getPathFragment(basePackage.getValue()));
            }
            return path1;
          }, new ZipUtil.ContentProcessor() {
            @Override
            public byte[] processContent(byte[] content, File file) throws IOException {
              if(pI != null){
                pI.checkCanceled();
              }
              FileType fileType = FileTypeManager.getInstance().getFileTypeByExtension(FileUtilRt.getExtension(file.getName()));
              String text = new String(content, CharsetToolkit.UTF8_CHARSET);
              consumer.setCurrentFile(file.getName(), text);
              return fileType.isBinary() ? content : processTemplates(projectName, text, file, consumer);
            }
          }, true);

          myTemplate.handleUnzippedDirectories(dir, filesToRefresh);

          return null;
        }
      });

      if(pI != null) {
        pI.setText("Refreshing...");
      }

      String iml = ContainerUtil.find(dir.list(), s -> s.endsWith(".iml"));
      if (moduleMode) {
        File from = new File(path, iml);
        File to = new File(getModuleFilePath());
        if (!from.renameTo(to)) {
          throw new IOException("Can't rename " + from + " to " + to);
        }
      }

      RefreshQueue refreshQueue = RefreshQueue.getInstance();
      LOG.assertTrue(!filesToRefresh.isEmpty());
      for (File file : filesToRefresh) {
        VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
        if (virtualFile == null) {
          throw new IOException("Can't find " + file);
        }
        refreshQueue.refresh(false, true, null, virtualFile);
      }

      consumer.reportFailures();
    }
    catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  private static String getPathFragment(String value) {
    return "/" + value.replace('.', '/') + "/";
  }

  @SuppressWarnings("UseOfPropertiesAsHashtable")
  @Nullable
  private byte[] processTemplates(@Nullable String projectName, String content, File file, Consumer<VelocityException> exceptionConsumer)
    throws IOException {
    String patchedContent = content;
    if (!(myTemplate instanceof LocalArchivedTemplate) || ((LocalArchivedTemplate)myTemplate).isEscaped()) {
      for (WizardInputField field : myAdditionalFields) {
        if (!field.acceptFile(file)) {
          return null;
        }
      }
      Properties properties = FileTemplateManager.getDefaultInstance().getDefaultProperties();
      for (WizardInputField field : myAdditionalFields) {
        properties.putAll(field.getValues());
      }
      if (projectName != null) {
        properties.put(ProjectTemplateParameterFactory.IJ_PROJECT_NAME, projectName);
      }
      String merged = FileTemplateUtil.mergeTemplate(properties, content, true, exceptionConsumer);
      patchedContent = merged.replace("\\$", "$").replace("\\#", "#");
    }
    else {
      int i = content.indexOf(SaveProjectAsTemplateAction.FILE_HEADER_TEMPLATE_PLACEHOLDER);
      if (i != -1) {
        final FileTemplate template =
          FileTemplateManager.getDefaultInstance().getDefaultTemplate(SaveProjectAsTemplateAction.getFileHeaderTemplateName());
        Properties properties = FileTemplateManager.getDefaultInstance().getDefaultProperties();
        String templateText = template.getText(properties);
        patchedContent = patchedContent.substring(0, i) +
                         templateText +
                         patchedContent.substring(i + SaveProjectAsTemplateAction.FILE_HEADER_TEMPLATE_PLACEHOLDER.length());
      }
    }
    return StringUtilRt.convertLineSeparators(patchedContent, CodeStyleFacade.getInstance().getLineSeparator()).
      getBytes(CharsetToolkit.UTF8_CHARSET);
  }

  @Nullable
  @Override
  public Project createProject(String name, final String path) {
    final File location = new File(FileUtil.toSystemDependentName(path));
    LOG.assertTrue(location.exists());

    final VirtualFile baseDir = ApplicationManager.getApplication().runWriteAction(
      (Computable<VirtualFile>)() -> LocalFileSystem.getInstance().refreshAndFindFileByIoFile(location));
    if (baseDir == null) {
      LOG.error("Couldn't find path '" + path + "' in VFS");
      return null;
    }

    VirtualFile[] children = baseDir.getChildren();
    boolean isSomehowOverwriting = children.length > 1 ||
                                   (children.length == 1 && !PathMacroUtil.DIRECTORY_STORE_NAME.equals(children[0].getName()));

    Task.WithResult<Project, RuntimeException> task = new Task.WithResult<Project, RuntimeException>(null, "Applying Template", true) {
      @Override
      public Project compute(@NotNull ProgressIndicator indicator) {
        try {
          myProjectMode = true;
          unzip(name, path, false, indicator, false);
          return ProjectManagerEx.getInstanceEx().convertAndLoadProject(path);
        }
        catch (IOException e) {
          LOG.error(e);
          return null;
        }
        finally {
          cleanup();
          if(indicator.isCanceled()){
            if (!isSomehowOverwriting) {
              ApplicationManager.getApplication().invokeLater(() -> {
                try {
                  WriteAction.run(() -> baseDir.delete(TemplateProjectDirectoryGenerator.class));
                }
                catch (IOException e) {
                  LOG.error(e);
                }
              });
            }
          }
        }
      }
    };
    return ProgressManager.getInstance().run(task);
  }

  private final static Logger LOG = Logger.getInstance(TemplateModuleBuilder.class);
}
