/*
* Copyright (c) 2006, 2008 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/


package com.nokia.carbide.internal.api.templatewizard.ui;

import com.nokia.carbide.internal.template.gen.Template.*;
import com.nokia.carbide.template.engine.ITemplate;
import com.nokia.carbide.templatewizard.Messages;
import com.nokia.carbide.templatewizard.TemplateWizardPlugin;
import com.nokia.cpp.internal.api.utils.core.*;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.wizard.IWizard;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.PlatformUI;

import java.text.MessageFormat;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A wizard page constructed from a wizardPage template element.
 * <p>
 * When made visible, the page will immediately update the associated template's value map
 * from the persisted or default values.  Then, changes made in the UI will be added to the map
 * when the page is left.  The {@link #getPageValues()} implementation ensures that values
 * are initialized properly even if a page has not been navigated to (early Finish). 
 */
public class TemplateWizardPage extends org.eclipse.jface.wizard.WizardPage implements IWizardDataPage {

	/** This class is the model for a field, which holds values independent of the UI */
	static class FieldModel {
		private final BaseFieldType type;
		private final Map<String, Object> values;
		private final IWizardDataPage page;
		
		public FieldModel(BaseFieldType type, IWizardDataPage page, Map<String, Object> pageValues) {
			this.type = type;
			this.page = page;
			this.values = pageValues;
		}
		/** Get the field id */
		String getId() {
			return type.getId();
		}
		
		/** Get the base field type */
		BaseFieldType getType() {
			return type;
		}
		
		/**
		 * Initialize the field from the persisted values or the defaults.
		 */
		public void initialize(ITemplate template) {
			Object currentValue = template.getTemplateValues().get(type.getId());
			if (currentValue != null) {
				// use the persisted or current value
				setValue(currentValue.toString());
			} else {
				String defaultValue = null;
				if (type instanceof TextFieldType)
					defaultValue = ((TextFieldType) type).getDefault();
				else if (type instanceof FilenameFieldType)
					defaultValue = ((FilenameFieldType) type).getDefault();
				else if (type instanceof UidFieldType)
					defaultValue = null;	/* no default in XML; initialized directly in control */
					
				if (defaultValue != null) {
					// localize the string
					String value = template.getLocalizedString(defaultValue);
					
					// if the default value is a variable then try to replace it
					if (containsVariable(value)) {
						value = replaceVariables(value);
					}
					setValue(value);
				}
			}
		}

		/** Get the current value, for use in a text */
		public String getValue() {
			Object obj = values.get(type.getId());
			return (obj != null) ? obj.toString() : ""; //$NON-NLS-1$
		}
		
		/** Set the current value */
		public void setValue(String value) {
			values.put(type.getId(), value);
			
			if (type instanceof UidFieldType) {
				values.put(type.getId(), value.toLowerCase());
				// simplest way to support this without creating a language out of templates
				values.put(type.getId() + UIDComposite.WITHOUT_0X_PREFIX, 
						UIDComposite.getWithout0x(value).toLowerCase());
			}

		}
		
		/**
		 * Replace variables in a default value setting by querying the variables
		 * available from previous pages.
		 * @param defaultValue
		 * @return
		 */
		protected String replaceVariables(String defaultValue) {
			VariableSubstitutionEngine engine = new VariableSubstitutionEngine(null, null);
			engine.setVariableToken('(');
			engine.allowRecursion(true);
			IVariableLookupCallback variableLookupCallback = new IVariableLookupCallback() {

				public Object getValue(String var) {
					// check all the pages (including the current one) for values
					IWizardPage wizardPage = page;
					while (wizardPage != null) {
						if (wizardPage instanceof IWizardDataPage) {
							Map<String, Object> otherPageValues = ((IWizardDataPage) wizardPage).getPageValues();
							if (otherPageValues.containsKey(var))
								return otherPageValues.get(var);
						}
						wizardPage = wizardPage.getPreviousPage();
					}
					return null;
				}
				
			};
			defaultValue = engine.substitute(variableLookupCallback, defaultValue);
			return defaultValue;
		}
		
		protected boolean containsVariable(String text) {
			return text != null && VARIABLE_PATTERN.matcher(text).find();
		}
		

	}
	
	
	
	class FieldValidator {
		
		protected BaseFieldType baseFieldType;
		protected Text text;
		
		public FieldValidator(BaseFieldType baseFieldType, Text text) {
			this.baseFieldType = baseFieldType;
			this.text = text;
			text.addModifyListener(new ModifyListener() {
				public void modifyText(ModifyEvent e) {
					validatePage();
				}
			});
		}
		
		
		public String getValue() {
			Check.checkState(text != null);
			return text.getText();
		}
		
		public String getId() {
			Check.checkState(baseFieldType != null);
			return baseFieldType.getId();
		}
		
		/**
		 * @return error string, or null if validates
		 */
		public String validate() {
			Check.checkState(baseFieldType != null);
			if (baseFieldType.isMandatory()) {
				String value = getValue();
				if (value.length() == 0) {
					return MessageFormat.format(Messages.getString("TemplateWizardPage.MandatoryValueError"),  //$NON-NLS-1$
							new Object[] { template.getLocalizedString(baseFieldType.getLabel()) });
				}
			}
			
			return null;
		}
		
		/**
		 * @return warning string, or null if no warning
		 */
		public String warn() {
			return null;
		}
	}
	
	class PatternFieldValidator extends FieldValidator {
		private Pattern pattern;
		private String regex;
		
		public PatternFieldValidator(TextFieldType textFieldType, Text text, String regex) {
			super(textFieldType, text);
			this.regex = regex;
			if (regex != null)
				this.pattern = Pattern.compile(regex, 
						textFieldType.isMultiline() ? Pattern.MULTILINE : 0);
		}
		
		@Override
		public String validate() {
			String error = super.validate();
			if (error == null) {
				String value = getValue();
				if (value.length() > 0 && pattern != null) {
			        Matcher m = pattern.matcher(value);
					if (!m.matches()) {
						return MessageFormat.format(Messages.getString("TemplateWizardPage.PatternMatchError"),  //$NON-NLS-1$
													new Object[] { value, regex });
					}
				}
			}
			
			return error;
		}
	}
	
	class FilenameFieldValidator extends FieldValidator {
		
		public FilenameFieldValidator(FilenameFieldType filenameFieldType, Text text) {
			super(filenameFieldType, text);
		}

		@Override
		public String getValue() {
			return super.getValue().trim();
		}
		
		@Override
		public String validate() {
			String error = super.validate();
			if (error == null) {
				String value = getValue();
				IPath path = new Path(value);
				for (String segment : path.segments()) {
					if (!FileUtils.isValidCarbideProjectPathSegment(segment)) {
						return MessageFormat.format(Messages.getString("TemplateWizardPage.FilenameError"),  //$NON-NLS-1$
								new Object[] { segment });
					}
				}
			}
			
			return error;
		}
	}
	
	class UIDFieldValdator extends FieldValidator {
		
		private UIDComposite composite;

		public UIDFieldValdator(UidFieldType uidFieldType, Text text, UIDComposite composite) {
			super(uidFieldType, text);
			this.composite = composite;
		}
		
		@Override
		public String getValue() {
			String value = super.getValue().trim();
			if (!UIDComposite.isValidHexString(value))
				return value;
			return UIDComposite.makeCanonicalHexString(value);
		}
		
		@Override
		public String validate() {
			String error = super.validate();
			if (error == null) {
				String value = getValue();
				if (!UIDComposite.isValidHexString(value))
					return MessageFormat.format(Messages.getString("TemplateWizardPage.UIDHexError"),  //$NON-NLS-1$
							new Object[] { value });
			}
			
			return error;
		}
		
		@Override
		public String warn() {
			String value = getValue();
			String minText = composite.getMinText();
			String maxText = composite.getMaxText();
			if (value.length() > 0 && !composite.validateAppUIDText()) {
				return MessageFormat.format(Messages.getString("TemplateWizardPage.UIDWarning"),  //$NON-NLS-1$
											new Object[] { value, minText, maxText });
			}
			
			return super.warn();
		}
	}
	
	public static final String NAME_KEY = ".uid"; //$NON-NLS-1$
	private static final int SINGLELINE_STYLE = SWT.BORDER;
	private static final int MULTILINE_STYLE = SWT.WRAP | SWT.MULTI | SWT.V_SCROLL | SWT.BORDER;

	private WizardPageType wizardPageType;
	private ITemplate template;
	private List<FieldValidator> fieldValidators;
	private boolean pageHasBeenShown = false;
	private Map<String, FieldModel> modelMap;
	private Map<String, Text> controlMap;
	private ArrayList<FieldModel> fieldModels;
	private Map<String, Object> pageValues;
	
	private static final Pattern VARIABLE_PATTERN = Pattern.compile("(\\$\\(([^)]+)\\))"); //$NON-NLS-1$

	
	public TemplateWizardPage(IWizard wizard, ITemplate template, WizardPageType wizardPageType) {
		this("TemplateWizardPage"); //$NON-NLS-1$
		this.template = template;
		this.wizardPageType = wizardPageType;
		setTitle(template.getLocalizedString(wizardPageType.getLabel()));
		setDescription(template.getLocalizedString(
				TextUtils.catenateBrokenLines(wizardPageType.getDescription())));
		setWizard(wizard);
		
		pageValues = new HashMap<String, Object>();
		fieldModels = new ArrayList<FieldModel>();
		modelMap = new HashMap<String, FieldModel>();
		
		for (EObject obj : wizardPageType.eContents()) {
    		if (obj instanceof BaseFieldType) {
    			BaseFieldType type = (BaseFieldType) obj;
				FieldModel model = new FieldModel(type, this, pageValues);
				fieldModels.add(model);
    			modelMap.put(type.getId(), model); 
    		}
		}
		
		fieldValidators = new ArrayList<FieldValidator>();
		
		controlMap = new HashMap<String, Text>();
	}

	protected TemplateWizardPage(String pageName) {
		super(pageName);
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.internal.api.templatewizard.ui.IWizardDataPage#getPageValues()
	 */
	public Map<String, Object> getPageValues() {
		// This method is typically called when finishing the wizard.
		// The template's value map already contains values established when
		// leaving other pages, but if pages were skipped entirely, this
		// handles them as well.
		
		if (!pageHasBeenShown) {
			pageHasBeenShown = true;
			initializeFieldValues();
		}
		
		finalizeFieldValues();

		return pageValues;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
	 */
	public void createControl(Composite parent) {
		initializeDialogUnits(parent);

		Composite mainPageComposite = new Composite(parent, SWT.NULL);
		final GridLayout gridLayout = new GridLayout(2, false);
		gridLayout.verticalSpacing = 10;
		mainPageComposite.setLayout(gridLayout);
    	for (FieldModel model : fieldModels) {
    		if (model.getType() instanceof TextFieldType) {
    			TextFieldType textField = (TextFieldType) model.getType();
   				createTextControl(mainPageComposite, textField, model);
    		}
    		else if (model.getType() instanceof UidFieldType) {
    			UidFieldType uidField = (UidFieldType) model.getType();
    			createUIDControl(mainPageComposite, uidField, model);
    		}
    		else if (model.getType() instanceof FilenameFieldType) {
    			FilenameFieldType filenameField = (FilenameFieldType) model.getType();
    			createFileControl(mainPageComposite, filenameField, model);
    		}
    		else
    			throw new IllegalStateException();
		}

    	mainPageComposite.setData(NAME_KEY, wizardPageType.getId());
        setControl(mainPageComposite);
        initializeFieldControls();
        validatePage();

		getControl().setData("WizardPage", this); //$NON-NLS-1$
        
		String helpContextId = TemplateWizardPlugin.ID + "." + wizardPageType.getId(); //$NON-NLS-1$
		PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(), helpContextId);
	}
	
	private void createLabel(Composite container, BaseFieldType baseFieldType) {
		Label label = new Label(container, SWT.NONE);
		label.setText(template.getLocalizedString(baseFieldType.getLabel()));
	}
	
	private void setToolTipText(Control control, BaseFieldType baseFieldType) {
		String description = TextUtils.catenateBrokenLines(baseFieldType.getDescription());
		control.setToolTipText(template.getLocalizedString(description));
	}
	
	private void createTextControl(Composite container, TextFieldType textField, final FieldModel model) {
		createLabel(container, textField);

		int style = textField.isMultiline() ? MULTILINE_STYLE : SINGLELINE_STYLE;
		final Text text = new Text(container, style);
		GridData gridData = new GridData(GridData.FILL, GridData.CENTER, true, false);
		text.setLayoutData(gridData);
		if (textField.isMultiline()) {
			gridData.heightHint = text.getLineHeight() * 4;
		}
		
		setToolTipText(text, textField);
		String id = textField.getId();
		text.setData(NAME_KEY, id);
		String pattern = textField.getPattern();
		fieldValidators.add(new PatternFieldValidator(textField, text, pattern));
		
		text.addModifyListener(new ModifyListener() {

			public void modifyText(ModifyEvent e) {
				model.setValue(text.getText());
			}
			
		});
		controlMap.put(id, text);
	}
	
	private void createUIDControl(Composite container, UidFieldType uidField, final FieldModel model) {
		createLabel(container, uidField);
		long minValue = createLongVal(uidField.getMin());
		long maxValue = createLongVal(uidField.getMax());
		UIDComposite composite = new UIDComposite(container, model, minValue, maxValue);
		composite.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false));
		setToolTipText(composite, uidField);
		String id = uidField.getId();
		final Text text = composite.getTextControl();
		text.setData(NAME_KEY, id);
		fieldValidators.add(new UIDFieldValdator(uidField, text, composite));
		
		text.addModifyListener(new ModifyListener() {

			public void modifyText(ModifyEvent e) {
				model.setValue(text.getText());
			}
			
		});
		
		controlMap.put(id, text);
	}
	
	private static long createLongVal(String valStr) {
		long val = 0;
		Check.checkContract(valStr.startsWith(UIDComposite.HEX_PREFIX));
		try {
			val = Long.parseLong(valStr.substring(UIDComposite.HEX_PREFIX.length()), 16);
		}
		catch (NumberFormatException e) {
			Check.failedArg(e);
		}
		return val;
	}

	private void createFileControl(Composite container, FilenameFieldType filenameField, final FieldModel model) {
		createLabel(container, filenameField);

		final Text text = new Text(container, SINGLELINE_STYLE);
		text.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false));
		
		setToolTipText(text, filenameField);
		String id = filenameField.getId();
		text.setData(NAME_KEY, id);
		fieldValidators.add(new FilenameFieldValidator(filenameField, text));
		
		text.addModifyListener(new ModifyListener() {

			public void modifyText(ModifyEvent e) {
				model.setValue(text.getText());
			}
			
		});
		
		controlMap.put(id, text);
	}

	@Override
	public void setPageComplete(boolean complete) {
		super.setPageComplete(complete);
		if (complete)
			setErrorMessage(null);
	}

	@Override
	public void setWizard(IWizard newWizard) {
		super.setWizard(newWizard);
		setImageDescriptor(ImageDescriptor.createFromImage(newWizard.getDefaultPageImage()));
	}

	public void validatePage() {
		setErrorMessage(null);
		setMessage(null);
		setPageComplete(true);
		
		if (!pageHasBeenShown) {
			// don't validate data before it's been initialized, but don't initialize it here
			// yet, either
			return;
		}
		
		for (Iterator<FieldValidator> iter = fieldValidators.iterator(); iter.hasNext();) {
			FieldValidator checker = iter.next();
			String error = checker.validate();
			if (error != null) {
				setPageComplete(false);
				setErrorMessage(error);
				return;
			}
		}
		for (Iterator<FieldValidator> iter = fieldValidators.iterator(); iter.hasNext();) {
			FieldValidator checker = iter.next();
			String warning = checker.warn();
			if (warning != null) {
				setMessage(warning, IMessageProvider.WARNING);
				return;
			}
		}
	}
	
	@Override
	public void setVisible(boolean visible) {
		super.setVisible(visible);
		
		if (!pageHasBeenShown && visible) {
			// replace and template variables the first time the page is displayed
			pageHasBeenShown = true;
			initializeFieldValues();
		}
	}

	/**
	 * On entering a page, set up the controls with default or persisted values
	 */
	protected void initializeFieldValues() {
		for (FieldModel model : fieldModels) {
			model.initialize(template);
		}
		
		// the page may or may not already have UI
		initializeFieldControls();
	}
	
	/**
	 * When the controls are first created, copy the current page values into them.
	 */
	protected void initializeFieldControls() {
		for (FieldModel model : fieldModels) {
			Text text = controlMap.get(model.getId());
			if (text != null) {
				String value = model.getValue();
				String current = text.getText();
				if (!current.equals(value)) {
					text.setText(value);
				}
			}
		}
	}
	
	/**
	 * On exiting a page, store the data into the template store.
	 */
	protected void finalizeFieldValues() {
		for (FieldModel model : fieldModels) {
			Text text = controlMap.get(model.getId());
			if (text != null) {
				String current = text.getText();
				model.setValue(current);
			}
		}
	}
	
	
}
