package jp.sourceforge.pdt_tools.formatter.core.ast;

import java.io.StringReader;
import java.util.Map;

import jp.sourceforge.pdt_tools.formatter.FormatterPlugin;
import jp.sourceforge.pdt_tools.formatter.TokenHolder;
import jp.sourceforge.pdt_tools.formatter.internal.core.formatter.CodeFormatterOptions;
import jp.sourceforge.pdt_tools.formatter.internal.ui.preferences.formatter.PHPSourcePreview;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.internal.core.SourceField;
import org.eclipse.dltk.internal.core.SourceMethod;
import org.eclipse.dltk.internal.core.SourceType;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.formatter.IContentFormatter;
import org.eclipse.jface.text.formatter.IFormattingStrategy;
import org.eclipse.jface.text.formatter.MultiPassContentFormatter;
import org.eclipse.php.internal.core.PHPVersion;
import org.eclipse.php.internal.core.ast.nodes.ASTParser;
import org.eclipse.php.internal.core.ast.nodes.Program;
import org.eclipse.php.internal.core.documentModel.parser.PHPRegionContext;
import org.eclipse.php.internal.core.documentModel.parser.regions.PhpScriptRegion;
import org.eclipse.php.internal.core.documentModel.partitioner.PHPPartitionTypes;
import org.eclipse.php.internal.core.documentModel.provisional.contenttype.ContentTypeIdForPHP;
import org.eclipse.php.internal.core.project.ProjectOptions;
import org.eclipse.php.internal.ui.editor.PHPStructuredEditor;
import org.eclipse.php.internal.ui.editor.PHPStructuredTextViewer;
import org.eclipse.php.ui.format.PHPFormatProcessorProxy;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.wst.html.core.internal.format.HTMLFormatProcessorImpl;
import org.eclipse.wst.html.core.text.IHTMLPartitions;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.parser.ContextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredPartitioning;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.undo.IStructuredTextUndoManager;
import org.eclipse.wst.sse.ui.internal.SSEUIMessages;
import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
import org.eclipse.wst.sse.ui.internal.format.StructuredFormattingStrategy;

public class Formatter implements IContentFormatter {

	private ASTFormatter formatter;
	private Program program;
	private TokenHolder holder;
	private CodeFormatterOptions options;
	private boolean preset;

	private boolean createMarker = true;
	private boolean persistMarker = false;
	private int severity = IMarker.SEVERITY_WARNING;

	private IStructuredDocument structuredDocument;
	private StructuredTextViewer structuredTextViewer;
	private PHPStructuredEditor phpStructuredEditor;
	private static final int FORMAT_DOCUMENT = 1;
	private static final int FORMAT_ACTIVE_ELEMENTS = 2;
	private static final int FORMAT_FILE = 3;
	private static final int FORMAT_PREVIEW = 4;
	private int operation;

	public Formatter() {
		options = null;
		preset = false;
	}

	public Formatter(Map map) {
		options = new CodeFormatterOptions(map);
		preset = true;
	}

	/**
	 * 
	 */
	public void format(IDocument document, IRegion region) {
		try {
			switch (operation = getOperation()) {
			case FORMAT_DOCUMENT:
				formatPHP(document, region);
				break;
			case FORMAT_ACTIVE_ELEMENTS:
				formatActiveElements(document, region);
				break;
			case FORMAT_FILE:
			case FORMAT_PREVIEW:
			default:
				formatDocument(document, region);
				break;
			}
		} catch (BadLocationException e) {
			FormatterPlugin.log(e);
		} catch (ModelException e) {
			FormatterPlugin.log(e);
		}
	}

	public IFormattingStrategy getFormattingStrategy(String contentType) {
		return null;
	}

	/**
	 * Format Active Elements
	 * 
	 * @param document
	 * @param region
	 * @throws BadLocationException
	 * @throws ModelException
	 */
	public void formatActiveElements(IDocument document, IRegion region)
			throws BadLocationException, ModelException {
		int offset = region.getOffset();
		int length = region.getLength();
		boolean php = false;
		boolean html = false;
		int ix = offset;
		do {
			ITypedRegion partition = document.getPartition(ix);
			if (partition.getType().equals(PHPPartitionTypes.PHP_DEFAULT)) {
				php = true;
			} else {
				if (ix + partition.getLength() >= offset + length) {
					String s = document.get(partition.getOffset(),
							partition.getLength());
					if (s.trim().equals("")) {
						break;
					}
				}
				html = true;
			}
			ix += partition.getLength();
		} while (ix < offset + length);

		if (!php && html) {
			formatHTMLElements(document, region);
		} else if (php && !html) {
			formatPHPElements(document, region);
		} else {
			Position position = new Position(offset, length);
			document.addPosition(position);
			formatHTMLElements(document, region);
			ix = position.offset;
			int start = -1;
			int end = 0;
			do {
				ITypedRegion partition = document.getPartition(ix);
				if (partition.getType().equals(PHPPartitionTypes.PHP_DEFAULT)) {
					if (start < 0) {
						start = partition.getOffset();
					}
					end += partition.getLength();
				} else {
					if (start >= 0) {
						Position partpos = new Position(partition.getOffset(),
								partition.getLength());
						document.addPosition(partpos);
						formatPHPElements(document, new Region(start, end));
						ix = partpos.offset + partpos.length;
						document.removePosition(partpos);
						start = -1;
						end = 0;
						continue;
					}
				}
				ix += partition.getLength();
			} while (ix < position.offset + position.length);
			if (start >= 0) {
				formatPHPElements(document, new Region(start, end));
			}
			document.removePosition(position);
		}
	}

	/**
	 * Format Active Elements for PHP<br>
	 * format current function, method, class or php code block
	 * 
	 * @param document
	 * @param region
	 * @throws BadLocationException 
	 * @throws ModelException 
	 */
	public void formatPHPElements(IDocument document, IRegion region)
			throws BadLocationException, ModelException {
		if (phpStructuredEditor == null) {
			phpStructuredEditor = getEditor(document);
			if (phpStructuredEditor == null) {
				return;
			}
		}
		IModelElement modelElement = phpStructuredEditor.getModelElement();
		if (!(modelElement instanceof ISourceModule)) {
			return;
		}
		((ISourceModule) modelElement).reconcile(true, null, null);
		IRegion newRegion = null;
		IRegion startRegion = getElementRegion(document, region.getOffset());
		if (startRegion != null) {
			int ofs = region.getOffset() + region.getLength();
			do {
				if (!Character.isWhitespace(document.getChar(--ofs))) {
					break;
				}
			} while (ofs > 0);
			IRegion stopRegion = getElementRegion(document, ofs);
			if (stopRegion != null) {
				if (startRegion.equals(stopRegion)) {
					newRegion = new Region(startRegion.getOffset(),
							startRegion.getLength());
				} else {
					int offset = Math.min(startRegion.getOffset(),
							stopRegion.getOffset());
					int length = Math.max(
							startRegion.getOffset() + startRegion.getLength(),
							stopRegion.getOffset() + stopRegion.getLength())
							- offset;
					newRegion = new Region(offset, length);
				}
			}
		}
		if (newRegion == null) {
			newRegion = getPhpScriptRegion(document, region.getOffset());
		}
		if (newRegion != null) {
			int lineOffset = document.getLineOffset(document
					.getLineOfOffset(newRegion.getOffset()));
			int headLength = newRegion.getOffset() - lineOffset;
			if (headLength > 0) {
				if (document.get(lineOffset, headLength).trim().length() == 0) {
					newRegion = new Region(lineOffset, newRegion.getLength()
							+ headLength);
				}
			}
			formatDocument(document, newRegion);
		}
	}

	private IRegion getElementRegion(IDocument document, int offset)
			throws BadLocationException, ModelException {
		IModelElement element = ((ISourceModule) phpStructuredEditor
				.getModelElement()).getElementAt(offset);
		if (element != null) {
			if (element instanceof SourceField) {
				element = element.getParent();
			}
			ISourceRange sourceRange = getSourceRange(element);
			if (sourceRange != null) {
				int newOffset = sourceRange.getOffset();
				int newLength = sourceRange.getLength();
				while (offset < newOffset || newOffset + newLength <= offset) {
					element = element.getParent();
					sourceRange = getSourceRange(element);
					if (sourceRange != null) {
						newOffset = sourceRange.getOffset();
						newLength = sourceRange.getLength();
					} else {
						break;
					}
				}
				if (sourceRange != null) {
					// fix inappropriate source range
					if (newOffset + newLength > document.getLength()) {
						newLength = document.getLength() - newOffset;
					}
					for (int i = newLength - 1; i >= 0; i--) {
						char ch = document.getChar(newOffset + i);
						if (ch == '\n' || ch == '\r') {
							newLength--;
						} else {
							break;
						}
					}
					return new Region(newOffset, newLength);
				}
			}
		}
		return null;
	}

	private ISourceRange getSourceRange(IModelElement element)
			throws ModelException {
		if (element instanceof SourceType) {
			return ((SourceType) element).getSourceRange();
		} else if (element instanceof SourceMethod) {
			return ((SourceMethod) element).getSourceRange();
		} else if (element instanceof SourceField) {
			return ((SourceField) element).getSourceRange();
		}
		return null;
	}

	private IRegion getPhpScriptRegion(IDocument document, int offset)
			throws BadLocationException {
		if (!(document instanceof IStructuredDocument)) {
			return null;
		}
		IStructuredDocumentRegion docRegion = ((IStructuredDocument) document)
				.getRegionAtCharacterOffset(offset);
		if (docRegion != null) {
			ITextRegion txtRegion = docRegion
					.getRegionAtCharacterOffset(offset);
			if (txtRegion instanceof PhpScriptRegion) {
				return new Region(docRegion.getStart() + txtRegion.getStart(),
						txtRegion.getLength());
			} else if (txtRegion instanceof ContextRegion) {
				String type = txtRegion.getType();
				if (type.equals(PHPRegionContext.PHP_OPEN)) {
					txtRegion = docRegion.getRegionAtCharacterOffset(docRegion
							.getStart() + txtRegion.getEnd());
				} else if (type.equals(PHPRegionContext.PHP_CLOSE)) {
					txtRegion = docRegion.getRegionAtCharacterOffset(docRegion
							.getStart() + txtRegion.getStart() - 1);
				}
				if (txtRegion instanceof PhpScriptRegion) {
					return new Region(docRegion.getStart()
							+ txtRegion.getStart(), txtRegion.getLength());
				}
			} else if (txtRegion == null) {
				if (offset > 0 && offset == document.getLength()) {
					txtRegion = docRegion
							.getRegionAtCharacterOffset(offset - 1);
					if (txtRegion instanceof PhpScriptRegion) {
						return new Region(docRegion.getStart()
								+ txtRegion.getStart(), txtRegion.getLength());
					}
				}
			}
		}
		return null;
	}

	/**
	 * Format Active Elements for HTML<br>
	 * format current element and sub elements
	 * 
	 * @param document
	 * @param region
	 */
	public void formatHTMLElements(IDocument document, IRegion region) {
		MultiPassContentFormatter htmlFormatter = new MultiPassContentFormatter(
				IStructuredPartitioning.DEFAULT_STRUCTURED_PARTITIONING,
				IHTMLPartitions.HTML_DEFAULT);
		htmlFormatter.setMasterStrategy(new StructuredFormattingStrategy(
				new HTMLFormatProcessorImpl()));
		htmlFormatter.format(document, region);
	}

	/**
	 * Format document within selected range or whole document
	 * 
	 * @param document
	 * @param region (ignored)
	 * @throws BadLocationException 
	 */
	public void formatPHP(IDocument document, IRegion region)
			throws BadLocationException {
		IRegion correctedRegion = getSelection(document);
		if (correctedRegion != null) {
			if (correctedRegion.getLength() == 0) {
				correctedRegion = new Region(0, document.getLength());
			}
			try {
				structuredDocument = (IStructuredDocument) document;
				Point selection = structuredTextViewer.getTextWidget()
						.getSelection();
				beginRecording(SSEUIMessages.Format_Active_Elements_UI_,
						SSEUIMessages.Format_Active_Elements_UI_, selection.x,
						selection.y - selection.x);
				formatDocument(document, correctedRegion);
			} finally {
				Point selection = structuredTextViewer.getTextWidget()
						.getSelection();
				endRecording(selection.x, selection.y - selection.x);
			}
		}
	}

	/**
	 * Format the document
	 * 
	 * @param document
	 * @param region
	 * @throws BadLocationException 
	 */
	public void formatDocument(IDocument document, IRegion region)
			throws BadLocationException {
		IFile file = FormatterPlugin.getDefault().getFile(document);
		IProject project = null;
		if (file != null) {
			project = file.getProject();
		}

		if (!preset) {
			// get options always when invoked
			Map optionsMap = FormatterPlugin.getDefault().getOptions(project);
			options = new CodeFormatterOptions(optionsMap);
		}

		if (file != null && options.format_html_region) {
			boolean doFormatHTML = false;
			int ofs = region.getOffset();
			int end = ofs + region.getLength();
			do {
				ITypedRegion p = document.getPartition(ofs);
				if (!p.getType().equals(PHPPartitionTypes.PHP_DEFAULT)) {
					if (ofs + p.getLength() >= end) {
						String s = document.get(p.getOffset(), p.getLength());
						if (s.trim().equals("")) {
							break;
						}
					}
					doFormatHTML = true;
					break;
				}
				ofs += p.getLength();
			} while (ofs < end);
			if (doFormatHTML) {
				MultiPassContentFormatter htmlFormatter = new MultiPassContentFormatter(
						IStructuredPartitioning.DEFAULT_STRUCTURED_PARTITIONING,
						IHTMLPartitions.HTML_DEFAULT);
				htmlFormatter
						.setMasterStrategy(new StructuredFormattingStrategy(
								new HTMLFormatProcessorImpl()));
				if (operation == FORMAT_FILE
						|| region.getLength() == document.getLength()) {
					htmlFormatter.format(document, region);
					region = new Region(0, document.getLength());
				} else {
					Position position = new Position(region.getOffset(),
							region.getLength());
					document.addPosition(position);
					htmlFormatter.format(document, region);
					region = new Region(position.offset, position.length);
					document.removePosition(position);
				}
			}
		}

		PHPVersion version = ProjectOptions.getDefaultPhpVersion();
		boolean useShortTags = true;
		if (file != null) {
			version = ProjectOptions.getPhpVersion(project);
			useShortTags = ProjectOptions.useShortTags(project);
		}
		program = null;
		try {
			ASTParser parser = ASTParser.newParser(
					new StringReader(document.get()), version, useShortTags);
			program = parser.createAST(new NullProgressMonitor());
		} catch (Exception e) {
			FormatterPlugin.log(e);
		}
		if (program == null) {
			error(file, "Could not get AST");
			return;
		}
		holder = new TokenHolder(document, project);
		if (holder == null) {
			error(file, "Could not get Tokens");
			return;
		}

		formatter = new ASTFormatter(program, holder, options);
		String result = formatter.format();

		if (result == null) {
			error(file, "Could not format (AST error)");
			return;
		}
		if (file == null) { // preview for preferences
			document.set(result);
			return;
		}
		if (document.get().equals(result)) {
			return;
		}
		IDocument formatted = createPHPDocument();
		if (region.getLength() != document.getLength()) {
			formatted.set(document.get());
			replace(formatted, result, region);
		} else {
			formatted.set(result);
		}
		if (!TokenHolder.verify(document, formatted, project)) {
			error(file, "Could not format (verify error)");
			return;
		}

		if (region.getLength() != document.getLength()) {
			replace(document, result, region);
		} else {
			document.set(result);
		}
		if (createMarker) {
			try {
				file.deleteMarkers(FormatterPlugin.MARKER_ID, false,
						IResource.DEPTH_INFINITE);
			} catch (CoreException e) {
			}
		}
	}

	private void error(IFile file, String msg) {
		StringBuffer buf = new StringBuffer(msg);
		if (file != null) {
			buf.append(": ").append(file.getFullPath().toString());
		}
		FormatterPlugin.log(IStatus.ERROR, buf.toString());
		if (file != null && createMarker) {
			FormatterPlugin.getDefault().createMarker(file, severity, msg,
					persistMarker);
		}
	}

	private IDocument createPHPDocument() {
		return StructuredModelManager.getModelManager()
				.createStructuredDocumentFor(
						ContentTypeIdForPHP.ContentTypeID_PHP);
	}

	private void replace(IDocument doc, String str, IRegion region) {
		int ix = 0;
		int iy = 0;
		int start = region.getOffset();
		int stop = start + region.getLength();
		int begin = 0;
		int end = 0;
		boolean inside = false;
		try {
			if (start > doc.getLength() / 2) {
				ix = doc.getLength() - 1;
				iy = str.length() - 1;
				while (ix >= 0 && iy >= 0) {
					char cx = doc.getChar(ix);
					char cy = str.charAt(iy);
					if (cx == cy) {
						if (ix < start) {
							break;
						}
						ix--;
						iy--;
						continue;
					}
					int fg = 0;
					if (Character.isWhitespace(cx)) {
						ix--;
						fg++;
					}
					if (Character.isWhitespace(cy)) {
						iy--;
						fg++;
					}
					if (fg == 0) {
						return;
					}
				}
			}
			while (ix < doc.getLength() && iy < str.length()) {
				if (!inside && ix >= start) {
					begin = iy;
					inside = true;
				}
				if (inside && ix >= stop) {
					end = iy;
					break;
				}
				char cx = doc.getChar(ix);
				char cy = str.charAt(iy);
				if (cx == cy) {
					ix++;
					iy++;
					continue;
				}
				int fg = 0;
				if (Character.isWhitespace(cx)) {
					ix++;
					fg++;
				}
				if (Character.isWhitespace(cy)) {
					iy++;
					fg++;
				}
				if (fg == 0) {
					return;
				}
			}
			if (inside) {
				if (end == 0) {
					end = iy;
				}
				doc.replace(start, region.getLength(),
						str.substring(begin, end));
			}
		} catch (BadLocationException e) {
			FormatterPlugin.log(e);
		}
	}

	private int getOperation() {
		StackTraceElement[] stackTraces = new Exception().getStackTrace();
		for (StackTraceElement stackTrace : stackTraces) {
			if (stackTrace.getClassName().equals(
					StructuredTextViewer.class.getName())) {
				if (stackTrace.getMethodName().equals("doOperation")) {
					return FORMAT_ACTIVE_ELEMENTS;
				}
			} else if (stackTrace.getClassName().equals(
					PHPStructuredTextViewer.class.getName())) {
				if (stackTrace.getMethodName().equals("doOperation")) {
					return FORMAT_DOCUMENT;
				}
			} else if (stackTrace.getClassName().equals(
					PHPFormatProcessorProxy.class.getName())) {
				if (stackTrace.getMethodName().equals("formatFile")) {
					return FORMAT_FILE;
				}
			} else if (stackTrace.getClassName().equals(
					PHPSourcePreview.class.getName())) {
				if (stackTrace.getMethodName().equals("doFormatPreview")) {
					return FORMAT_PREVIEW;
				}
			}
		}
		return 0;
	}

	private IRegion getSelection(IDocument document) {
		IRegion region = null;
		if (structuredTextViewer == null) {
			structuredTextViewer = getViewer(document);
		}
		if (structuredTextViewer != null) {
			Point point = structuredTextViewer.getSelectedRange();
			if (point.y > -1) {
				region = new Region(point.x, point.y);
			}
		}
		return region;
	}

	private StructuredTextViewer getViewer(final IDocument document) {
		PHPStructuredEditor editor = getEditor(document);
		if (editor != null) {
			return editor.getTextViewer();
		}
		return null;
	}

	private PHPStructuredEditor getEditor(final IDocument document) {
		final PHPStructuredEditor[] phpeditor = new PHPStructuredEditor[1];
		PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
			public void run() {
				IWorkbenchWindow window = PlatformUI.getWorkbench()
						.getActiveWorkbenchWindow();
				if (window != null) {
					IWorkbenchPage page = window.getActivePage();
					if (page != null) {
						IEditorPart part = page.getActiveEditor();
						if (part instanceof PHPStructuredEditor) {
							PHPStructuredEditor editor = (PHPStructuredEditor) part;
							if (editor.getDocument().equals(document)) {
								phpeditor[0] = editor;
							}
						}
					}
				}
			}
		});
		return phpeditor[0];
	}

	/*
	 * org.eclipse.wst.sse.ui.internal.StructuredTextViewer.beginRecording(String, String, int, int)
	 */
	private void beginRecording(String label, String description,
			int cursorPosition, int selectionLength) {
		IStructuredTextUndoManager undoManager = structuredDocument
				.getUndoManager();
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=198617
		// undo after paste in document with folds - wrong behavior
		IRegion widgetSelection = new Region(cursorPosition, selectionLength);
		IRegion documentSelection = structuredTextViewer
				.widgetRange2ModelRange(widgetSelection);
		if (documentSelection == null)
			documentSelection = widgetSelection;
		undoManager.beginRecording(this, label, description,
				documentSelection.getOffset(), documentSelection.getLength());
	}

	/*
	 * org.eclipse.wst.sse.ui.internal.StructuredTextViewer.endRecording(int, int)
	 */
	private void endRecording(int cursorPosition, int selectionLength) {
		IStructuredTextUndoManager undoManager = structuredDocument
				.getUndoManager();
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=198617
		// undo after paste in document with folds - wrong behavior
		IRegion widgetSelection = new Region(cursorPosition, selectionLength);
		IRegion documentSelection = structuredTextViewer
				.widgetRange2ModelRange(widgetSelection);
		if (documentSelection == null)
			documentSelection = widgetSelection;
		undoManager.endRecording(this, documentSelection.getOffset(),
				documentSelection.getLength());
	}
}
