/*
 * Copyright 2000-2013 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 org.jetbrains.plugins.groovy.refactoring.introduce.constant;

import com.intellij.ide.util.*;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.JavaResolveUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.JavaRefactoringSettings;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.introduceField.IntroduceConstantHandler;
import com.intellij.refactoring.ui.JavaVisibilityPanel;
import com.intellij.refactoring.ui.NameSuggestionsField;
import com.intellij.ui.RecentsManager;
import com.intellij.ui.ReferenceEditorComboWithBrowseButton;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import consulo.psi.PsiPackage;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.plugins.groovy.GroovyFileType;
import org.jetbrains.plugins.groovy.actions.GroovyTemplates;
import org.jetbrains.plugins.groovy.actions.GroovyTemplatesFactory;
import org.jetbrains.plugins.groovy.actions.NewGroovyActionBase;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.impl.GroovyNamesUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringBundle;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceContext;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceDialog;
import org.jetbrains.plugins.groovy.refactoring.introduce.StringPartInfo;
import org.jetbrains.plugins.groovy.refactoring.introduce.field.GrFieldNameSuggester;
import org.jetbrains.plugins.groovy.refactoring.introduce.variable.GroovyVariableValidator;
import org.jetbrains.plugins.groovy.refactoring.ui.GrTypeComboBox;

import javax.annotation.Nonnull;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.*;

/**
 * @author Maxim.Medvedev
 */
public class GrIntroduceConstantDialog extends DialogWrapper implements GrIntroduceConstantSettings, GrIntroduceDialog<GrIntroduceConstantSettings>
{

	private static final Logger LOG = Logger.getInstance("#org.jetbrains.plugins.groovy.refactoring.introduce.constant" + ".GrIntroduceConstantDialog");

	private final GrIntroduceContext myContext;
	private JLabel myNameLabel;
	private JCheckBox myReplaceAllOccurrences;
	private JPanel myPanel;
	private GrTypeComboBox myTypeCombo;
	private ReferenceEditorComboWithBrowseButton myTargetClassEditor;
	private NameSuggestionsField myNameField;
	private JavaVisibilityPanel myJavaVisibilityPanel;
	private JPanel myTargetClassPanel;
	private JLabel myTargetClassLabel;
	@javax.annotation.Nullable
	private PsiClass myTargetClass;
	@javax.annotation.Nullable
	private PsiClass myDefaultTargetClass;

	private TargetClassInfo myTargetClassInfo;

	public GrIntroduceConstantDialog(GrIntroduceContext context, @javax.annotation.Nullable PsiClass defaultTargetClass)
	{
		super(context.getProject());
		myContext = context;
		myTargetClass = defaultTargetClass;
		myDefaultTargetClass = defaultTargetClass;

		setTitle(GrIntroduceConstantHandler.REFACTORING_NAME);

		myJavaVisibilityPanel.setVisibility(JavaRefactoringSettings.getInstance().INTRODUCE_CONSTANT_VISIBILITY);

		updateVisibilityPanel();
		updateOkStatus();
		init();
	}

	@javax.annotation.Nullable
	public static PsiClass getParentClass(PsiElement occurrence)
	{
		PsiElement cur = occurrence;
		while(true)
		{
			final PsiClass parentClass = PsiTreeUtil.getParentOfType(cur, PsiClass.class, true);
			if(parentClass == null || parentClass.hasModifierProperty(PsiModifier.STATIC))
			{
				return parentClass;
			}
			cur = parentClass;
		}
	}

	@Override
	public JComponent getPreferredFocusedComponent()
	{
		return myNameField;
	}

	@Override
	protected JComponent createCenterPanel()
	{
		return null;
	}

	@Override
	protected JComponent createNorthPanel()
	{
		initializeName();
		initializeTargetClassEditor();

		if(myContext.getVar() != null)
		{
			myReplaceAllOccurrences.setEnabled(false);
			myReplaceAllOccurrences.setSelected(true);
		}
		else if(myContext.getOccurrences().length < 2)
		{
			myReplaceAllOccurrences.setVisible(false);
		}
		return myPanel;
	}

	private void initializeTargetClassEditor()
	{

		myTargetClassEditor = new ReferenceEditorComboWithBrowseButton(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				TreeClassChooser chooser = TreeClassChooserFactory.getInstance(myContext.getProject()).createWithInnerClassesScopeChooser(RefactoringBundle.message("choose.destination.class"), GlobalSearchScope.projectScope(myContext.getProject()), new ClassFilter()
				{
					public boolean isAccepted(PsiClass aClass)
					{
						return aClass.getParent() instanceof GroovyFile || aClass.hasModifierProperty(PsiModifier.STATIC);
					}
				}, null);
				if(myTargetClass != null)
				{
					chooser.selectDirectory(myTargetClass.getContainingFile().getContainingDirectory());
				}
				chooser.showDialog();
				PsiClass aClass = chooser.getSelected();
				if(aClass != null)
				{
					myTargetClassEditor.setText(aClass.getQualifiedName());
				}

			}
		}, "", myContext.getProject(), true, RECENTS_KEY);
		myTargetClassPanel.setLayout(new BorderLayout());
		myTargetClassPanel.add(myTargetClassLabel, BorderLayout.NORTH);
		myTargetClassPanel.add(myTargetClassEditor, BorderLayout.CENTER);
		Set<String> possibleClassNames = new LinkedHashSet<String>();
		for(final PsiElement occurrence : myContext.getOccurrences())
		{
			final PsiClass parentClass = getParentClass(occurrence);
			if(parentClass != null && parentClass.getQualifiedName() != null)
			{
				possibleClassNames.add(parentClass.getQualifiedName());
			}
		}

		for(String possibleClassName : possibleClassNames)
		{
			myTargetClassEditor.prependItem(possibleClassName);
		}

		if(myDefaultTargetClass != null)
		{
			myTargetClassEditor.prependItem(myDefaultTargetClass.getQualifiedName());
		}

		myTargetClassEditor.getChildComponent().addDocumentListener(new DocumentAdapter()
		{
			public void documentChanged(DocumentEvent e)
			{
				targetClassChanged();
				updateOkStatus();
				// enableEnumDependant(introduceEnumConstant());
			}
		});
	}

	private void initializeName()
	{
		myNameLabel.setLabelFor(myNameField);

		myPanel.registerKeyboardAction(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				myNameField.requestFocus();
			}
		}, KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.ALT_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);

		myNameField.addDataChangedListener(new NameSuggestionsField.DataChanged()
		{
			@Override
			public void dataChanged()
			{
				updateOkStatus();
			}
		});
	}

	@Override
	public String getVisibilityModifier()
	{
		return myJavaVisibilityPanel.getVisibility();
	}

	@javax.annotation.Nullable
	@Override
	public PsiClass getTargetClass()
	{
		return myTargetClassInfo.getTargetClass();
	}

	@Nonnull
	public String getTargetClassName()
	{
		return myTargetClassEditor.getText();
	}

	@Override
	public GrIntroduceConstantSettings getSettings()
	{
		return this;
	}

	@Nonnull
	@Override
	public LinkedHashSet<String> suggestNames()
	{
		return new GrFieldNameSuggester(myContext, new GroovyVariableValidator(myContext), true).suggestNames();
	}

	@javax.annotation.Nullable
	@Override
	public String getName()
	{
		return myNameField.getEnteredName();
	}

	@Override
	public boolean replaceAllOccurrences()
	{
		return myReplaceAllOccurrences.isSelected();
	}

	@Override
	public PsiType getSelectedType()
	{
		return myTypeCombo.getSelectedType();
	}

	@NonNls
	private static final String RECENTS_KEY = "GrIntroduceConstantDialog.RECENTS_KEY";

	private void createUIComponents()
	{
		myJavaVisibilityPanel = new JavaVisibilityPanel(false, true);

		final GrVariable var = myContext.getVar();
		final GrExpression expression = myContext.getExpression();
		final StringPartInfo stringPart = myContext.getStringPart();
		if(expression != null)
		{
			myTypeCombo = GrTypeComboBox.createTypeComboBoxFromExpression(expression);
		}
		else if(stringPart != null)
		{
			myTypeCombo = GrTypeComboBox.createTypeComboBoxFromExpression(stringPart.getLiteral());
		}
		else
		{
			assert var != null;
			myTypeCombo = GrTypeComboBox.createTypeComboBoxWithDefType(var.getDeclaredType(), var);
		}

		List<String> names = new ArrayList<String>();
		if(var != null)
		{
			names.add(var.getName());
		}
		if(expression != null)
		{
			ContainerUtil.addAll(names, suggestNames());
		}

		myNameField = new NameSuggestionsField(ArrayUtil.toStringArray(names), myContext.getProject(), GroovyFileType.GROOVY_FILE_TYPE);

		GrTypeComboBox.registerUpDownHint(myNameField, myTypeCombo);
	}

	private void targetClassChanged()
	{
		final String targetClassName = getTargetClassName();
		myTargetClass = JavaPsiFacade.getInstance(myContext.getProject()).findClass(targetClassName, GlobalSearchScope.projectScope(myContext.getProject()));
		updateVisibilityPanel();
		//    myIntroduceEnumConstantCb.setEnabled(EnumConstantsUtil.isSuitableForEnumConstant(getSelectedType(),
		// myTargetClassEditor));
	}

	private void updateVisibilityPanel()
	{
		if(myTargetClass != null && myTargetClass.isInterface())
		{
			myJavaVisibilityPanel.disableAllButPublic();
		}
		else
		{
			UIUtil.setEnabled(myJavaVisibilityPanel, true, true);
			// exclude all modifiers not visible from all occurrences
			final Set<String> visible = new HashSet<String>();
			visible.add(PsiModifier.PRIVATE);
			visible.add(PsiModifier.PROTECTED);
			visible.add(PsiModifier.PACKAGE_LOCAL);
			visible.add(PsiModifier.PUBLIC);
			for(PsiElement occurrence : myContext.getOccurrences())
			{
				final PsiManager psiManager = PsiManager.getInstance(myContext.getProject());
				for(Iterator<String> iterator = visible.iterator(); iterator.hasNext(); )
				{
					String modifier = iterator.next();

					try
					{
						final String modifierText = PsiModifier.PACKAGE_LOCAL.equals(modifier) ? "" : modifier + " ";
						final PsiField field = JavaPsiFacade.getInstance(psiManager.getProject()).getElementFactory().createFieldFromText(modifierText + "int xxx;", myTargetClass);
						if(!JavaResolveUtil.isAccessible(field, myTargetClass, field.getModifierList(), occurrence, myTargetClass, null))
						{
							iterator.remove();
						}
					}
					catch(IncorrectOperationException e)
					{
						LOG.error(e);
					}
				}
			}
			if(!visible.contains(getVisibilityModifier()))
			{
				if(visible.contains(PsiModifier.PUBLIC))
				{
					myJavaVisibilityPanel.setVisibility(PsiModifier.PUBLIC);
				}
				if(visible.contains(PsiModifier.PACKAGE_LOCAL))
				{
					myJavaVisibilityPanel.setVisibility(PsiModifier.PACKAGE_LOCAL);
				}
				if(visible.contains(PsiModifier.PROTECTED))
				{
					myJavaVisibilityPanel.setVisibility(PsiModifier.PROTECTED);
				}
				if(visible.contains(PsiModifier.PRIVATE))
				{
					myJavaVisibilityPanel.setVisibility(PsiModifier.PRIVATE);
				}
			}
		}
	}

	private void updateOkStatus()
	{
		if(myTargetClassEditor == null)
		{
			return; //dialog is not initialized yet
		}

		String text = getName();
		if(!GroovyNamesUtil.isIdentifier(text))
		{
			setOKActionEnabled(false);
			return;
		}

		final String targetClassName = myTargetClassEditor.getText();
		if(targetClassName.trim().length() == 0 && myDefaultTargetClass == null)
		{
			setOKActionEnabled(false);
			return;
		}
		final String trimmed = targetClassName.trim();
		if(!JavaPsiFacade.getInstance(myContext.getProject()).getNameHelper().isQualifiedName(trimmed))
		{
			setOKActionEnabled(false);
			return;
		}
		setOKActionEnabled(true);
	}

	@Override
	protected void doOKAction()
	{
		final String targetClassName = getTargetClassName();

		if(myDefaultTargetClass == null || !targetClassName.isEmpty() && !Comparing.strEqual(targetClassName, myDefaultTargetClass.getQualifiedName()))
		{
			final Module module = ModuleUtilCore.findModuleForPsiElement(myContext.getPlace());
			JavaPsiFacade facade = JavaPsiFacade.getInstance(myContext.getProject());
			PsiClass newClass = facade.findClass(targetClassName, GlobalSearchScope.projectScope(myContext.getProject()));

			if(newClass == null && Messages.showOkCancelDialog(myContext.getProject(), GroovyRefactoringBundle.message("class.does.not.exist.in.the.module"), IntroduceConstantHandler.REFACTORING_NAME, Messages.getErrorIcon()) != OK_EXIT_CODE)
			{
				return;
			}
			myTargetClassInfo = new TargetClassInfo(targetClassName, myContext.getPlace().getContainingFile().getContainingDirectory(), module, myContext.getProject());
		}
		else
		{
			myTargetClassInfo = new TargetClassInfo(myDefaultTargetClass);
		}


		JavaRefactoringSettings.getInstance().INTRODUCE_CONSTANT_VISIBILITY = getVisibilityModifier();

		RecentsManager.getInstance(myContext.getProject()).registerRecentEntry(RECENTS_KEY, targetClassName);

		super.doOKAction();
	}

	private static class TargetClassInfo
	{
		private PsiClass myTargetClass;

		String myQualifiedName;
		PsiDirectory myBaseDirectory;
		Module myModule;
		Project myProject;

		private TargetClassInfo(PsiClass targetClass)
		{
			myTargetClass = targetClass;
		}

		private TargetClassInfo(String qualifiedName, PsiDirectory baseDirectory, Module module, Project project)
		{
			myQualifiedName = qualifiedName;
			myBaseDirectory = baseDirectory;
			myModule = module;
			myProject = project;
		}

		@javax.annotation.Nullable
		public PsiClass getTargetClass()
		{
			if(myTargetClass == null)
			{
				myTargetClass = getTargetClass(myQualifiedName, myBaseDirectory, myProject, myModule);
			}
			return myTargetClass;
		}

		@javax.annotation.Nullable
		private static PsiClass getTargetClass(String qualifiedName, PsiDirectory baseDirectory, Project project, Module module)
		{
			GlobalSearchScope scope = GlobalSearchScope.projectScope(project);

			PsiClass targetClass = JavaPsiFacade.getInstance(project).findClass(qualifiedName, scope);
			if(targetClass != null)
			{
				return targetClass;
			}

			final String packageName = StringUtil.getPackageName(qualifiedName);
			PsiPackage psiPackage = JavaPsiFacade.getInstance(project).findPackage(packageName);
			final PsiDirectory psiDirectory;
			if(psiPackage != null)
			{
				final PsiDirectory[] directories = psiPackage.getDirectories(GlobalSearchScope.allScope(project));
				psiDirectory = directories.length > 1 ? DirectoryChooserUtil.chooseDirectory(directories, null, project, new HashMap<PsiDirectory, String>()) : directories[0];
			}
			else
			{
				psiDirectory = PackageUtil.findOrCreateDirectoryForPackage(module, packageName, baseDirectory, false);
			}
			if(psiDirectory == null)
			{
				return null;
			}
			final String shortName = StringUtil.getShortName(qualifiedName);
			final String fileName = shortName + NewGroovyActionBase.GROOVY_EXTENSION;

			return WriteAction.compute(() ->
			{
				final GroovyFile file = (GroovyFile) GroovyTemplatesFactory.createFromTemplate(psiDirectory, shortName, fileName, GroovyTemplates.GROOVY_CLASS, true);
				return file.getTypeDefinitions()[0];
			});
		}
	}
}
