/***********************************************************************************************************************
 *
 * Copyright (C) 2010 by the Stratosphere project (http://stratosphere.eu)
 *
 * 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 eu.stratosphere.nephele.visualization.swt;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
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.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TreeItem;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public final class SWTFailurePatternsEditor implements JobFailurePatternTreeListener {

	private static final int WIDTH = 800;

	private static final int HEIGHT = 400;

	private final Shell shell;

	private final SWTJobFailurePatternTree jobTree;

	private final SWTFailureEventTable failureEventTable;

	private final Set<String> jobSuggestions;

	private final Map<String, JobFailurePattern> loadedPatterns;

	SWTFailurePatternsEditor(final Shell parent, final Set<String> jobSuggestions, final Set<String> nameSuggestions,
			final Map<String, JobFailurePattern> loadedPatterns) {

		this.jobSuggestions = jobSuggestions;
		this.loadedPatterns = loadedPatterns;

		// Set size
		this.shell = new Shell(parent);
		this.shell.setSize(WIDTH, HEIGHT);
		this.shell.setText("Manage Failure Patterns");
		GridLayout gl = new GridLayout(1, false);
		gl.horizontalSpacing = 0;
		gl.verticalSpacing = 0;
		gl.marginRight = 0;
		gl.marginLeft = 0;
		gl.marginBottom = 0;
		gl.marginTop = 0;
		gl.marginHeight = 0;
		gl.marginWidth = 0;
		this.shell.setLayout(gl);

		final Composite mainComposite = new Composite(this.shell, SWT.NONE);
		mainComposite.setLayout(new GridLayout(1, false));
		GridData gridData = new GridData();
		gridData.horizontalAlignment = GridData.FILL;
		gridData.verticalAlignment = GridData.FILL;
		gridData.grabExcessHorizontalSpace = true;
		gridData.grabExcessVerticalSpace = true;
		mainComposite.setLayoutData(gridData);

		final SashForm horizontalSash = new SashForm(mainComposite, SWT.HORIZONTAL);
		horizontalSash.setLayoutData(new GridData(GridData.FILL_BOTH));

		final Group jobGroup = new Group(horizontalSash, SWT.NONE);
		jobGroup.setText("Job Failure Patterns");
		jobGroup.setLayout(new FillLayout());

		this.jobTree = new SWTJobFailurePatternTree(jobGroup, SWT.SINGLE | SWT.BORDER, this);

		this.failureEventTable = new SWTFailureEventTable(horizontalSash, SWT.BORDER | SWT.SINGLE, nameSuggestions);
		horizontalSash.setWeights(new int[] { 2, 8 });

		final Composite buttonComposite = new Composite(this.shell, SWT.NONE);
		buttonComposite.setLayout(new GridLayout(2, false));
		gridData = new GridData();
		gridData.horizontalAlignment = GridData.FILL;
		buttonComposite.setLayoutData(gridData);

		final Label fillLabel = new Label(buttonComposite, SWT.NONE);
		gridData = new GridData();
		gridData.horizontalAlignment = GridData.FILL;
		gridData.grabExcessHorizontalSpace = true;
		gridData.grabExcessVerticalSpace = false;
		fillLabel.setLayoutData(gridData);

		final Button closeButton = new Button(buttonComposite, SWT.PUSH);
		closeButton.setText("Close");
		gridData = new GridData();
		gridData.horizontalAlignment = SWT.RIGHT;
		closeButton.setLayoutData(gridData);
		closeButton.addSelectionListener(new SelectionAdapter() {

			@Override
			public void widgetSelected(SelectionEvent arg0) {

				shell.dispose();
			}
		});

		final Iterator<JobFailurePattern> it = this.loadedPatterns.values().iterator();
		while (it.hasNext()) {
			this.jobTree.addFailurePatternToTree(it.next());
		}

		// Initialize the tables
		if (this.jobTree.getItemCount() > 0) {

			final TreeItem ti = this.jobTree.getItem(0);
			this.jobTree.setSelection(ti);
			this.failureEventTable.showFailurePattern((JobFailurePattern) ti.getData());

		} else {
			this.failureEventTable.showFailurePattern(null);
		}

	}

	public Map<String, JobFailurePattern> show() {

		this.shell.open();

		final Display display = this.shell.getDisplay();

		while (!this.shell.isDisposed()) {
			if (!display.readAndDispatch()) {
				display.sleep();
			}
		}

		return this.loadedPatterns;
	}

	@Override
	public void addFailurePattern() {

		final Set<String> takenNames = this.loadedPatterns.keySet();

		final SWTNewFailurePatternDialog dialog = new SWTNewFailurePatternDialog(this.shell, this.jobSuggestions,
			takenNames);

		final String patternName = dialog.showDialog();
		if (patternName == null) {
			return;
		}

		final JobFailurePattern jobFailurePattern = new JobFailurePattern(patternName);

		// Add to loaded patterns
		this.loadedPatterns.put(jobFailurePattern.getName(), jobFailurePattern);

		this.jobTree.addFailurePatternToTree(jobFailurePattern);
		this.failureEventTable.showFailurePattern(jobFailurePattern);
	}

	@Override
	public void removeFailurePattern(final TreeItem selectedItem) {

		final JobFailurePattern failurePattern = (JobFailurePattern) selectedItem.getData();
		if (failurePattern == null) {
			return;
		}

		final MessageBox messageBox = new MessageBox(this.shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO);
		messageBox.setText("Confirm Removal");
		messageBox
			.setMessage("Do you really want to remove the job failure pattern '" + failurePattern.getName() + "'?");

		if (messageBox.open() != SWT.YES) {
			return;
		}

		selectedItem.dispose();

		this.loadedPatterns.remove(failurePattern.getName());

		if (this.jobTree.getItemCount() == 0) {
			jobFailurePatternSelected(null);
		} else {
			final TreeItem ti = this.jobTree.getItem(0);
			this.jobTree.setSelection(ti);
			jobFailurePatternSelected(ti);
		}
	}

	@Override
	public void saveFailurePattern(final TreeItem selectedItem) {

		final JobFailurePattern failurePattern = (JobFailurePattern) selectedItem.getData();
		if (failurePattern == null) {
			return;
		}

		final FileDialog fileDialog = new FileDialog(this.shell, SWT.SAVE);
		fileDialog.setText("Save Failure Pattern");
		final String[] filterExts = { "*.xml", "*.*" };
		fileDialog.setFilterExtensions(filterExts);

		final String selectedFile = fileDialog.open();
		if (selectedFile == null) {
			return;
		}

		final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();

		try {
			final DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();

			final Document doc = builder.newDocument();

			// Construct the DOM tree
			final Element root = doc.createElement("pattern");
			doc.appendChild(root);

			final Element name = doc.createElement("name");
			root.appendChild(name);
			name.appendChild(doc.createTextNode(failurePattern.getName()));

			final Element failures = doc.createElement("failures");
			root.appendChild(failures);

			final Iterator<AbstractFailureEvent> it = failurePattern.iterator();
			while (it.hasNext()) {

				final AbstractFailureEvent event = it.next();

				final Element failure = doc.createElement("failure");
				failure.setAttribute("type", (event instanceof VertexFailureEvent) ? "task" : "instance");

				final Element failureName = doc.createElement("name");
				failureName.appendChild(doc.createTextNode(event.getName()));
				failure.appendChild(failureName);

				final Element interval = doc.createElement("interval");
				interval.appendChild(doc.createTextNode(Integer.toString(event.getInterval())));
				failure.appendChild(interval);

				failures.appendChild(failure);
			}

			// Write the DOM tree to the chosen file
			final DOMSource domSource = new DOMSource(doc);
			final TransformerFactory transformerFactory = TransformerFactory.newInstance();

			final FileOutputStream fos = new FileOutputStream(selectedFile);
			final Transformer transformer = transformerFactory.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
			transformer.transform(domSource, new StreamResult(fos));

		} catch (Exception e) {

			final MessageBox messageBox = new MessageBox(this.shell, SWT.ICON_ERROR);
			messageBox.setText("Cannot Save Failure Pattern");
			messageBox.setMessage(e.getMessage());
			messageBox.open();
		}

	}

	@Override
	public void jobFailurePatternSelected(final TreeItem selectedItem) {

		if (selectedItem == null) {
			this.failureEventTable.showFailurePattern(null);
		} else {
			this.failureEventTable.showFailurePattern((JobFailurePattern) selectedItem.getData());
		}
	}

	@Override
	public void loadFailurePattern() {

		final FileDialog fileDialog = new FileDialog(this.shell, SWT.OPEN);
		fileDialog.setText("Load Failure Pattern");
		final String[] filterExts = { "*.xml", "*.*" };
		fileDialog.setFilterExtensions(filterExts);

		final String selectedFile = fileDialog.open();
		if (selectedFile == null) {
			return;
		}

		final JobFailurePattern failurePattern = loadFailurePatternFromFile(selectedFile);

		if (this.loadedPatterns.containsKey(failurePattern.getName())) {

			final MessageBox messageBox = new MessageBox(this.shell, SWT.ICON_ERROR);
			messageBox.setText("Cannot Load Failure Pattern");
			messageBox.setMessage("There is already a failure pattern loaded with the name '"
				+ failurePattern.getName() + "'. Please remove it first.");
			messageBox.open();

			return;
		}

		this.loadedPatterns.put(failurePattern.getName(), failurePattern);

		this.jobTree.addFailurePatternToTree(failurePattern);
		this.failureEventTable.showFailurePattern(failurePattern);
	}

	private JobFailurePattern loadFailurePatternFromFile(final String filename) {

		final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
		// Ignore comments in the XML file
		docBuilderFactory.setIgnoringComments(true);
		docBuilderFactory.setNamespaceAware(true);

		JobFailurePattern jobFailurePattern = null;
		InputStream inputStream = null;

		try {

			inputStream = new FileInputStream(filename);

			final DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
			Document doc = null;
			Element root = null;

			doc = builder.parse(inputStream);

			if (doc == null) {
				throw new Exception("Document is null");
			}

			root = doc.getDocumentElement();
			if (root == null) {
				throw new Exception("Root element is null");
			}

			if (!"pattern".equals(root.getNodeName())) {
				throw new Exception("Encountered unknown element " + root.getNodeName());
			}

			final NodeList patternChildren = root.getChildNodes();
			for (int i = 0; i < patternChildren.getLength(); ++i) {

				final Node patternChild = patternChildren.item(i);

				if (patternChild instanceof org.w3c.dom.Text) {
					continue;
				}

				if (patternChild instanceof Element) {

					final Element patternElement = (Element) patternChild;
					if ("name".equals(patternElement.getNodeName())) {
						final String name = extractValueFromElement(patternElement);
						if (jobFailurePattern != null) {
							throw new Exception("Element name detected more than once in the file");
						}

						jobFailurePattern = new JobFailurePattern(name);
						continue;
					}

					if ("failures".equals(patternElement.getNodeName())) {

						if (jobFailurePattern == null) {
							throw new Exception("Expected pattern name to be stored before the failure events");
						}

						final NodeList failuresChildren = patternElement.getChildNodes();
						for (int j = 0; j < failuresChildren.getLength(); ++j) {

							final Node failuresChild = failuresChildren.item(j);

							if (failuresChild instanceof org.w3c.dom.Text) {
								continue;
							}

							if (!(failuresChild instanceof Element)) {
								throw new Exception("Expected type element as child of element 'failures'");
							}

							final Element failuresElement = (Element) failuresChild;

							if (!"failure".equals(failuresElement.getNodeName())) {
								throw new Exception("Expected element 'failure' as child of element 'failures'");
							}

							final String type = failuresElement.getAttribute("type");
							if (type == null) {
								throw new Exception("Element 'failure' lacks the attribute 'type'");
							}

							final boolean taskFailure = ("task".equals(type));
							String name = null;
							String interval = null;

							final NodeList failureChildren = failuresElement.getChildNodes();
							for (int k = 0; k < failureChildren.getLength(); ++k) {

								final Node failureChild = failureChildren.item(k);

								if (failureChild instanceof org.w3c.dom.Text) {
									continue;
								}

								if (!(failureChild instanceof Element)) {
									throw new Exception("Expected type element as child of element 'failure'");
								}

								final Element failureElement = (Element) failureChild;
								if ("name".equals(failureElement.getNodeName())) {
									name = extractValueFromElement(failureElement);
								}

								if ("interval".equals(failureElement.getNodeName())) {
									interval = extractValueFromElement(failureElement);
								}
							}

							if (name == null) {
								throw new Exception("Could not find name for failure event " + j);
							}

							if (interval == null) {
								throw new Exception("Could not find interval for failure event " + j);
							}

							int iv = 0;
							try {
								iv = Integer.parseInt(interval);

							} catch (NumberFormatException e) {
								throw new Exception("Interval " + interval + " for failure event " + j
									+ " is not an integer number");
							}

							if (iv <= 0) {
								throw new Exception("Interval for failure event " + j
									+ " must be greather than zero, but is " + iv);
							}

							AbstractFailureEvent failureEvent = null;
							if (taskFailure) {
								failureEvent = new VertexFailureEvent(name, iv);
							} else {
								failureEvent = new InstanceFailureEvent(name, iv);
							}

							jobFailurePattern.addEvent(failureEvent);
						}

						continue;
					}

					throw new Exception("Uncountered unecpted element " + patternElement.getNodeName());

				} else {
					throw new Exception("Encountered unexpected child of type " + patternChild.getClass());
				}
			}

		} catch (Exception e) {

			final MessageBox messageBox = new MessageBox(this.shell, SWT.ICON_ERROR);
			messageBox.setText("Cannot Load Failure Pattern");
			messageBox.setMessage(e.getMessage());
			messageBox.open();
			return null;
		} finally {
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (Exception e) {
				}
			}
		}

		return jobFailurePattern;
	}

	private String extractValueFromElement(final Element element) throws Exception {

		final NodeList children = element.getChildNodes();
		if (children.getLength() != 1) {
			throw new Exception("Element " + element.getNodeName() + " has an unexpected number of children");
		}

		final Node child = children.item(0);

		if (!(child instanceof org.w3c.dom.Text)) {
			throw new Exception("Expected child of element " + element.getNodeName() + " to be of type text");
		}

		org.w3c.dom.Text childText = (org.w3c.dom.Text) child;

		return childText.getTextContent();
	}
}
