/*******************************************************************************
 *  Copyright (c) 2005, 2018 IBM Corporation and others.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.internal.ui.nls;

import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;

import java.util.Properties;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICellModifier;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.ui.IHelpContextIds;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.editor.context.ManifestDocumentSetupParticipant;
import org.eclipse.pde.internal.ui.editor.context.XMLDocumentSetupParticpant;
import org.eclipse.pde.internal.ui.editor.text.ColorManager;
import org.eclipse.pde.internal.ui.editor.text.IColorManager;
import org.eclipse.pde.internal.ui.editor.text.ManifestConfiguration;
import org.eclipse.pde.internal.ui.editor.text.XMLConfiguration;
import org.eclipse.pde.internal.ui.refactoring.PDERefactor;
import org.eclipse.pde.internal.ui.wizards.ListUtil;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ContainerCheckedTreeViewer;

public class ExternalizeStringsWizardPage extends UserInputWizardPage {

	public static final String PAGE_NAME = "ExternalizeStringsWizardPage"; //$NON-NLS-1$

	public static final int EXTERN = 0;
	public static final int VALUE = 1;
	public static final int KEY = 2;
	private static final int SIZE = 3; // column counter
	private static final String[] TABLE_PROPERTIES = new String[SIZE];
	private static final String[] TABLE_COLUMNS = new String[SIZE];

	static {
		TABLE_PROPERTIES[EXTERN] = "extern"; //$NON-NLS-1$
		TABLE_PROPERTIES[VALUE] = "value"; //$NON-NLS-1$
		TABLE_PROPERTIES[KEY] = "key"; //$NON-NLS-1$
		TABLE_COLUMNS[EXTERN] = ""; //$NON-NLS-1$
		TABLE_COLUMNS[VALUE] = PDEUIMessages.ExternalizeStringsWizardPage_value;
		TABLE_COLUMNS[KEY] = PDEUIMessages.ExternalizeStringsWizardPage_subKey;
	}

	private class ModelChangeContentProvider implements ITreeContentProvider {

		@Override
		public Object[] getElements(Object parent) {
			return fModelChangeTable.getAllModelChanges().toArray();
		}

		@Override
		public Object[] getChildren(Object parentElement) {
			if (!(parentElement instanceof ModelChange))
				return new Object[0];
			return ((ModelChange) parentElement).getModelChangeFiles();
		}

		@Override
		public Object getParent(Object element) {
			if (element instanceof ModelChangeFile) {
				return ((ModelChangeFile) element).getModel();
			}
			return null;
		}

		@Override
		public boolean hasChildren(Object element) {
			return element instanceof ModelChange;
		}

	}

	private class ExternalizeStringsCellModifier implements ICellModifier {

		@Override
		public boolean canModify(Object element, String property) {
			return (property != null && (element instanceof ModelChangeElement) && !TABLE_PROPERTIES[VALUE].equals(property) && (isPageComplete() || element.equals(fErrorElement)) && (TABLE_PROPERTIES[KEY].equals(property) && ((ModelChangeElement) element).isExternalized()));

		}

		@Override
		public Object getValue(Object element, String property) {
			if (element instanceof ModelChangeElement changeElement) {
				if (TABLE_PROPERTIES[KEY].equals(property)) {
					return StringHelper.unwindEscapeChars(changeElement.getKey());
				}
			}
			return ""; //$NON-NLS-1$
		}

		@Override
		public void modify(Object element, String property, Object value) {
			if (element instanceof TableItem) {
				Object data = ((TableItem) element).getData();
				if (data instanceof ModelChangeElement changeElement) {
					if (TABLE_PROPERTIES[KEY].equals(property)) {
						String newKey = StringHelper.windEscapeChars((String) value);
						validateKey(newKey, changeElement);
						changeElement.setKey(newKey);
						fPropertiesViewer.update(data, null);
					}
				}
			}
		}
	}

	private ModelChangeTable fModelChangeTable;

	private ContainerCheckedTreeViewer fInputViewer;
	private Button fSelectAll;
	private Button fDeselectAll;
	private Label fProjectLabel;
	private Text fLocalizationText;
	private CheckboxTableViewer fPropertiesViewer;
	private Table fTable;
	private SourceViewer fSourceViewer;

	private ViewerFilter fErrorElementFilter;
	private ModifyListener fModifyListener;

	private Object fCurrSelection;
	private ModelChangeElement fErrorElement;
	private String fPreErrorKey;

	private IDocument fEmptyDoc;
	private IColorManager fColorManager;
	private XMLConfiguration fXMLConfig;
	private XMLDocumentSetupParticpant fXMLSetupParticipant;
	private ManifestDocumentSetupParticipant fManifestSetupParticipant;

	private ManifestConfiguration fManifestConfig;

	protected ExternalizeStringsWizardPage(ModelChangeTable changeTable) {
		super(PAGE_NAME);
		setTitle(PDEUIMessages.ExternalizeStringsWizardPage_pageTitle);
		setDescription(PDEUIMessages.ExternalizeStringsWizardPage_pageDescription);
		fModelChangeTable = changeTable;
		fErrorElementFilter = new ViewerFilter() {
			@Override
			public boolean select(Viewer viewer, Object parentElement, Object element) {
				if (!(element instanceof ModelChangeElement change))
					return false;
				return change.equals(fErrorElement);
			}
		};
		fModifyListener = e -> {
			String localization = fLocalizationText.getText();
			if (StringHelper.isValidLocalization(localization)) {
				setEnabled(fLocalizationText, true);
				setPageComplete(hasCheckedElements());
				setErrorMessage(null);
				if (fCurrSelection instanceof ModelChange) {
					((ModelChange) fCurrSelection).setBundleLocalization(fLocalizationText.getText());
				} else if (fCurrSelection instanceof ModelChangeFile) {
					((ModelChangeFile) fCurrSelection).getModel().setBundleLocalization(fLocalizationText.getText());
				}
			} else {
				setEnabled(fLocalizationText, false);
				fLocalizationText.setEditable(true);
				setPageComplete(false);
				setErrorMessage(PDEUIMessages.ExternalizeStringsWizardPage_badLocalizationError);
			}
		};
		fColorManager = ColorManager.getDefault();
		fXMLConfig = new XMLConfiguration(fColorManager);
		fXMLSetupParticipant = new XMLDocumentSetupParticpant();
		fManifestConfig = new ManifestConfiguration(fColorManager);
		fManifestSetupParticipant = new ManifestDocumentSetupParticipant();
	}

	@Override
	public void dispose() {
		fColorManager.dispose();
		super.dispose();
	}

	@Override
	public void createControl(Composite parent) {

		SashForm superSash = new SashForm(parent, SWT.HORIZONTAL);
		superSash.setFont(parent.getFont());
		superSash.setLayoutData(new GridData(GridData.FILL_BOTH));

		createInputContents(superSash);

		SashForm sash = new SashForm(superSash, SWT.VERTICAL);
		sash.setFont(superSash.getFont());
		sash.setLayoutData(new GridData(GridData.FILL_BOTH));

		createTableViewer(sash);
		createSourceViewer(sash);
		initialize();

		setPageComplete(hasCheckedElements());

		superSash.setWeights(new int[] {4, 7});
		setControl(superSash);
		Dialog.applyDialogFont(superSash);

		PlatformUI.getWorkbench().getHelpSystem().setHelp(superSash, IHelpContextIds.EXTERNALIZE_STRINGS_PAGE);
	}

	private void createInputContents(Composite composite) {
		Composite fileComposite = new Composite(composite, SWT.NONE);
		fileComposite.setLayout(new GridLayout());
		fileComposite.setLayoutData(new GridData(GridData.FILL_BOTH));

		Label label = new Label(fileComposite, SWT.NONE);
		label.setText(PDEUIMessages.ExternalizeStringsWizardPage_resourcelabel);
		fInputViewer = new ContainerCheckedTreeViewer(fileComposite, SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE | SWT.BORDER);
		fInputViewer.setContentProvider(new ModelChangeContentProvider());
		fInputViewer.setLabelProvider(new ModelChangeLabelProvider());
		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.heightHint = 250;
		fInputViewer.getTree().setLayoutData(gd);
		fInputViewer.addSelectionChangedListener(this::handleSelectionChanged);
		fInputViewer.addCheckStateListener(event -> setPageComplete(hasCheckedElements()));
		fInputViewer.setComparator(ListUtil.PLUGIN_COMPARATOR);

		Composite buttonComposite = new Composite(fileComposite, SWT.NONE);
		GridLayout layout = new GridLayout(2, true);
		layout.marginHeight = layout.marginWidth = 0;
		buttonComposite.setLayout(layout);
		buttonComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		fSelectAll = new Button(buttonComposite, SWT.PUSH);
		fSelectAll.setText(PDEUIMessages.ExternalizeStringsWizardPage_selectAllButton);
		fSelectAll.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		fSelectAll.addSelectionListener(widgetSelectedAdapter(e -> {
			fInputViewer.setCheckedElements(fModelChangeTable.getAllModelChanges().toArray());
			setPageComplete(hasCheckedElements());
		}));
		fDeselectAll = new Button(buttonComposite, SWT.PUSH);
		fDeselectAll.setText(PDEUIMessages.ExternalizeStringsWizardPage_deselectAllButton);
		fDeselectAll.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		fDeselectAll.addSelectionListener(widgetSelectedAdapter(e -> {
			fInputViewer.setCheckedElements(new Object[0]);
			setPageComplete(hasCheckedElements());
		}));

		Composite infoComposite = new Composite(fileComposite, SWT.NONE);
		layout = new GridLayout();
		layout.marginHeight = 0;
		infoComposite.setLayout(layout);
		infoComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		Label project = new Label(infoComposite, SWT.NONE);
		project.setText(PDEUIMessages.ExternalizeStringsWizardPage_projectLabel);
		fProjectLabel = new Label(infoComposite, SWT.NONE);
		gd = new GridData(GridData.FILL_HORIZONTAL);
		gd.horizontalIndent = 10;
		fProjectLabel.setLayoutData(gd);
		fProjectLabel.setText(PDEUIMessages.ExternalizeStringsWizardPage_noUnderlyingResource);

		Label properties = new Label(infoComposite, SWT.NONE);
		properties.setText(PDEUIMessages.ExternalizeStringsWizardPage_localizationLabel);
		fLocalizationText = new Text(infoComposite, SWT.BORDER);
		gd = new GridData(GridData.FILL_HORIZONTAL);
		gd.horizontalIndent = 10;
		fLocalizationText.setLayoutData(gd);
		fLocalizationText.setText(PDEUIMessages.ExternalizeStringsWizardPage_noUnderlyingResource);
		fLocalizationText.addModifyListener(fModifyListener);

		fInputViewer.setInput(PDEPlugin.getDefault());
	}

	private void createTableViewer(Composite parent) {
		Composite composite = new Composite(parent, SWT.NONE);
		composite.setFont(parent.getFont());
		composite.setLayoutData(new GridData(GridData.FILL_BOTH));
		composite.setLayout(new GridLayout());

		Label label = new Label(composite, SWT.NONE);
		label.setText(PDEUIMessages.ExternalizeStringsWizardPage_propertiesLabel);
		label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		fPropertiesViewer = CheckboxTableViewer.newCheckList(composite, SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.HIDE_SELECTION | SWT.BORDER);
		fTable = fPropertiesViewer.getTable();
		fTable.setFont(composite.getFont());
		fTable.setLayoutData(new GridData(GridData.FILL_BOTH));
		fTable.setLayout(new GridLayout());
		fTable.setLinesVisible(true);
		fTable.setHeaderVisible(true);

		for (int i = 0; i < TABLE_COLUMNS.length; i++) {
			TableColumn tc = new TableColumn(fTable, SWT.NONE);
			tc.setText(TABLE_COLUMNS[i]);
			tc.setResizable(i != 0);
			tc.setWidth(i == 0 ? 20 : 200);
		}

		fPropertiesViewer.setUseHashlookup(true);
		fPropertiesViewer.setCellEditors(createCellEditors());
		fPropertiesViewer.setColumnProperties(TABLE_PROPERTIES);
		fPropertiesViewer.setCellModifier(new ExternalizeStringsCellModifier());
		fPropertiesViewer.setContentProvider((IStructuredContentProvider) inputElement -> {
			if (fInputViewer.getSelection() instanceof IStructuredSelection) {
				Object selection = fInputViewer.getStructuredSelection().getFirstElement();
				if (selection instanceof ModelChangeFile cf) {
					return (cf).getModel().getChangesInFile(cf.getFile()).toArray();
				}
			}
			return new Object[0];
		});
		fPropertiesViewer.setLabelProvider(new ExternalizeStringsLabelProvider());
		fPropertiesViewer.addSelectionChangedListener(event -> handlePropertySelection());
		fPropertiesViewer.addCheckStateListener(event -> {
			Object element = event.getElement();
			if (element instanceof ModelChangeElement) {
				((ModelChangeElement) element).setExternalized(event.getChecked());
				fPropertiesViewer.update(element, null);
			}
		});
		fPropertiesViewer.setInput(new Object());
	}

	private void createSourceViewer(Composite parent) {
		Composite composite = new Composite(parent, SWT.NONE);
		composite.setLayoutData(new GridData(GridData.FILL_BOTH));
		composite.setLayout(new GridLayout());

		Label label = new Label(composite, SWT.NONE);
		label.setText(PDEUIMessages.ExternalizeStringsWizardPage_sourceLabel);
		label.setLayoutData(new GridData());

		fSourceViewer = new SourceViewer(composite, null, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER);
		fSourceViewer.setEditable(false);
		fSourceViewer.getTextWidget().setFont(JFaceResources.getTextFont());
		fSourceViewer.getControl().setLayoutData(new GridData(GridData.FILL_BOTH));

		fEmptyDoc = new Document();
		fSourceViewer.setDocument(fEmptyDoc);
	}

	// must set selection after source viewer is created, otherwise you get an NPE.
	private void initialize() {
		Object[] preSelect = fModelChangeTable.getPreSelected();
		fInputViewer.setSelection(new StructuredSelection(preSelect));
		fInputViewer.setCheckedElements(fModelChangeTable.getPreSelected());
	}

	private void handleSelectionChanged(SelectionChangedEvent event) {
		if (!(event.getSelection() instanceof IStructuredSelection))
			return;
		Object selection = (event.getStructuredSelection().getFirstElement());
		if (selection == null) {
			fCurrSelection = null;
			fSourceViewer.setDocument(fEmptyDoc);
		} else if (selection.equals(fCurrSelection)) {
			return;
		} else if (selection instanceof ModelChangeFile) {
			fCurrSelection = selection;
			IFile file = ((ModelChangeFile) fCurrSelection).getFile();
			NullProgressMonitor monitor = new NullProgressMonitor();
			ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
			try {
				try {
					manager.connect(file.getFullPath(), LocationKind.IFILE, monitor);
					updateSourceViewer(manager, file);
				} catch (MalformedTreeException e) {
				} finally {
					manager.disconnect(file.getFullPath(), LocationKind.IFILE, monitor);
				}
			} catch (CoreException e) {
			}
		} else if (selection instanceof ModelChange) {
			fCurrSelection = selection;
			fSourceViewer.setDocument(fEmptyDoc);
			updatePropertiesLabel(((ModelChange) fCurrSelection).getParentModel());
		}
		refreshPropertiesViewer(false);
	}

	private void refreshPropertiesViewer(boolean updateLabels) {
		fPropertiesViewer.refresh(updateLabels);
		TableItem[] items = fTable.getItems();
		for (int i = 0; i < items.length; i++) {
			if (!(items[i].getData() instanceof ModelChangeElement))
				continue;
			ModelChangeElement element = (ModelChangeElement) items[i].getData();
			fPropertiesViewer.setChecked(element, element.isExternalized());
		}
	}

	private void updateSourceViewer(ITextFileBufferManager manager, IFile sourceFile) {
		IDocument document = manager.getTextFileBuffer(sourceFile.getFullPath(), LocationKind.IFILE).getDocument();
		TreeItem item = fInputViewer.getTree().getSelection()[0];
		IPluginModelBase model = ((ModelChange) item.getParentItem().getData()).getParentModel();

		if (fSourceViewer.getDocument() != null)
			fSourceViewer.unconfigure();
		if (sourceFile.getFileExtension().equalsIgnoreCase("xml")) { //$NON-NLS-1$
			fSourceViewer.configure(fXMLConfig);
			fXMLSetupParticipant.setup(document);
		} else {
			fSourceViewer.configure(fManifestConfig);
			fManifestSetupParticipant.setup(document);
		}

		fSourceViewer.setDocument(document);
		updatePropertiesLabel(model);
	}

	private void updatePropertiesLabel(IPluginModelBase model) {
		ModelChange modelChange = fModelChangeTable.getModelChange(model);
		fProjectLabel.setText(model.getUnderlyingResource().getProject().getName());
		fLocalizationText.setEditable(!modelChange.localizationSet());
		fLocalizationText.setText(modelChange.getBundleLocalization());
	}

	private void handlePropertySelection() {
		Object selection = fPropertiesViewer.getStructuredSelection().getFirstElement();
		if (selection instanceof ModelChangeElement element && fSourceViewer.getDocument() != null) {
			int offset = element.getOffset();
			int length = element.getLength();
			fSourceViewer.setSelectedRange(offset, length);
			fSourceViewer.revealRange(offset, length);
		}
	}

	private CellEditor[] createCellEditors() {
		final CellEditor editors[] = new CellEditor[SIZE];
		editors[EXTERN] = null;
		editors[VALUE] = null;
		editors[KEY] = new TextCellEditor(fTable);
		return editors;
	}

	private void validateKey(String key, ModelChangeElement element) {
		ModelChange modelChange = ((ModelChangeFile) fCurrSelection).getModel();
		Properties properties = modelChange.getProperties();
		String error = null;
		String oldKey = (fPreErrorKey != null) ? fPreErrorKey : element.getKey();
		if (key.equals(fPreErrorKey)) {
//			error = null;
		} else if (key.trim().length() < 1) {
			error = getErrorMessage(PDEUIMessages.ExternalizeStringsWizardPage_keyEmptyError, oldKey);
		} else if (key.charAt(0) == '#' || key.charAt(0) == '!' || key.charAt(0) == '%') {
			error = getErrorMessage(PDEUIMessages.ExternalizeStringsWizardPage_keyCommentError, oldKey);
		} else if (key.indexOf(':') != -1 || key.indexOf('=') != -1 || key.indexOf(' ') != -1) {
			error = getErrorMessage(PDEUIMessages.ExternalizeStringsWizardPage_keyError, oldKey);
		} else if ((!key.equals(oldKey) || fPreErrorKey != null) && properties.containsKey(key)) {
			error = getErrorMessage(PDEUIMessages.ExternalizeStringsWizardPage_keyDuplicateError, oldKey);
		}

		setErrorMessage(error);
		setPageComplete(error == null && hasCheckedElements());
		if (error == null) {
			fErrorElement = null;
			fPreErrorKey = null;
			setEnabled(fPropertiesViewer.getControl(), true);
			fPropertiesViewer.removeFilter(fErrorElementFilter);
			refreshPropertiesViewer(true);
			properties.setProperty(key, element.getValue());
		} else if (fPreErrorKey == null) {
			fErrorElement = element;
			fPreErrorKey = oldKey;
			setEnabled(fPropertiesViewer.getControl(), false);
			fPropertiesViewer.addFilter(fErrorElementFilter);
		}
	}

	private String getErrorMessage(String error, String suggestion) {
		StringBuilder sb = new StringBuilder(error);
		if (suggestion != null) {
			sb.append(PDEUIMessages.ExternalizeStringsWizardPage_keySuggested);
			sb.append(suggestion);
		}
		return sb.toString();
	}

	public Object[] getChangeFiles() {
		return fInputViewer.getCheckedElements();
	}

	private boolean hasCheckedElements() {
		return fInputViewer.getCheckedElements().length > 0;
	}

	private void setEnabled(Control exception, boolean enabled) {
		if (!exception.equals(fInputViewer.getControl()))
			fInputViewer.getControl().setEnabled(enabled);
		if (!exception.equals(fPropertiesViewer.getControl()))
			fPropertiesViewer.getControl().setEnabled(enabled);
		if (!exception.equals(fLocalizationText))
			fLocalizationText.setEnabled(enabled);
		if (!exception.equals(fSelectAll))
			fSelectAll.setEnabled(enabled);
		if (!exception.equals(fDeselectAll))
			fDeselectAll.setEnabled(enabled);
	}

	@Override
	public void setPageComplete(boolean complete) {
		super.setPageComplete(complete);
		// if the page is ready to be completed set the selection on the processor so it knows
		// what work needs to be done
		if (complete)
			((ExternalizeStringsProcessor) ((PDERefactor) getRefactoring()).getProcessor()).setChangeFiles(fInputViewer.getCheckedElements());
	}
}
