/*
 * Copyright 2018 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
package io.flutter.editor;

import com.intellij.AppTopics;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileDocumentManagerListener;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.util.PsiErrorElementUtil;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import com.jetbrains.lang.dart.DartLanguage;
import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService;
import com.jetbrains.lang.dart.assists.AssistUtils;
import io.flutter.FlutterUtils;
import io.flutter.dart.DartPlugin;
import io.flutter.settings.FlutterSettings;
import org.dartlang.analysis.server.protocol.SourceEdit;
import org.dartlang.analysis.server.protocol.SourceFileEdit;
import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;

/**
 * A manager class to run actions on save (formatting, organize imports, ...).
 */
public class FlutterSaveActionsManager {
  private static final Logger LOG = Logger.getInstance(FlutterSaveActionsManager.class);

  /**
   * Initialize the save actions manager for the given project.
   */
  public static void init(@NotNull Project project) {
    // Call getInstance() will init FlutterSaveActionsManager for the given project by calling the private constructor below.
    getInstance(project);
  }

  public static FlutterSaveActionsManager getInstance(@NotNull Project project) {
    return ServiceManager.getService(project, FlutterSaveActionsManager.class);
  }

  private final @NotNull Project myProject;

  private FlutterSaveActionsManager(@NotNull Project project) {
    this.myProject = project;

    final MessageBus bus = project.getMessageBus();
    final MessageBusConnection connection = bus.connect();
    connection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, new FileDocumentManagerListener() {
      @Override
      public void beforeDocumentSaving(@NotNull Document document) {
        // Don't try and format read only docs.
        if (!document.isWritable()) {
          return;
        }

        handleBeforeDocumentSaving(document);
      }
    });
  }

  private void handleBeforeDocumentSaving(@NotNull Document document) {
    final FlutterSettings settings = FlutterSettings.getInstance();
    if (!settings.isFormatCodeOnSave()) {
      return;
    }

    if (!myProject.isInitialized() || myProject.isDisposed()) {
      return;
    }

    final VirtualFile file = FileDocumentManager.getInstance().getFile(document);
    if (file == null) {
      return;
    }

    if (!FlutterUtils.isDartFile(file)) {
      return;
    }

    final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
    if (psiFile == null || !psiFile.isValid()) {
      return;
    }

    final Module module = ModuleUtil.findModuleForFile(file, myProject);
    if (module == null) {
      return;
    }

    if (!DartPlugin.isDartSdkEnabled(module)) {
      return;
    }

    // check for errors
    if (PsiErrorElementUtil.hasErrors(myProject, psiFile.getVirtualFile())) {
      return;
    }

    if (DartAnalysisServerService.getInstance(myProject).serverReadyForRequest()) {
      if (settings.isOrganizeImportsOnSave()) {
        performOrganizeThenFormat(document, file);
      }
      else {
        performFormat(document, file, false);
      }
    }
  }

  private void performOrganizeThenFormat(@NotNull Document document, @NotNull VirtualFile file) {
    final String filePath = file.getPath();
    final SourceFileEdit fileEdit = DartAnalysisServerService.getInstance(myProject).edit_organizeDirectives(filePath);

    if (myProject.isDisposed()) {
      return;
    }

    if (fileEdit != null) {
      ApplicationManager.getApplication().invokeLater(() -> new WriteCommandAction.Simple(myProject) {
        @Override
        protected void run() {
          if (myProject.isDisposed()) {
            return;
          }

          AssistUtils.applySourceEdits(myProject, file, document, fileEdit.getEdits(), Collections.emptySet());

          // Committing a document here is required in order to guarantee that DartPostFormatProcessor.processText() is called afterwards.
          PsiDocumentManager.getInstance(myProject).commitDocument(document);

          // Run this in an invoke later so that we don't exeucte the initial part of performFormat in a write action.
          //noinspection CodeBlock2Expr
          ApplicationManager.getApplication().invokeLater(() -> {
            performFormat(document, file, true);
          });
        }
      }.execute());
    }
  }

  private void performFormat(@NotNull Document document, @NotNull VirtualFile file, boolean reSave) {
    final int lineLength = getRightMargin(myProject);
    final DartAnalysisServerService das = DartAnalysisServerService.getInstance(myProject);

    das.updateFilesContent();

    final DartAnalysisServerService.FormatResult formatResult = das.edit_format(file, 0, 0, lineLength);

    if (formatResult == null) {
      if (reSave) {
        FileDocumentManager.getInstance().saveDocument(document);
      }
      return;
    }

    if (myProject.isDisposed()) {
      return;
    }

    ApplicationManager.getApplication().invokeLater(() -> new WriteCommandAction.Simple(myProject) {
      @Override
      protected void run() {
        if (myProject.isDisposed()) {
          return;
        }

        boolean didFormat = false;

        final List<SourceEdit> edits = formatResult.getEdits();
        if (edits != null && edits.size() == 1) {
          final String replacement = StringUtil.convertLineSeparators(edits.get(0).getReplacement());
          document.replaceString(0, document.getTextLength(), replacement);
          PsiDocumentManager.getInstance(myProject).commitDocument(document);

          didFormat = true;
        }

        // Don't perform the save in a write action - it could invoke EDT work.
        if (reSave || didFormat) {
          //noinspection CodeBlock2Expr
          ApplicationManager.getApplication().invokeLater(() -> {
            FileDocumentManager.getInstance().saveDocument(document);
          });
        }
      }
    }.execute());
  }

  private static int getRightMargin(@NotNull Project project) {
    return CodeStyleSettingsManager.getSettings(project).getCommonSettings(DartLanguage.INSTANCE).RIGHT_MARGIN;
  }
}