package org.wangzw.plugin.cppstyle;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Map;
import java.net.URI;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.formatter.CodeFormatter;
import org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.ui.ICEditor;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.undo.DocumentUndoManagerRegistry;
import org.eclipse.text.undo.IDocumentUndoManager;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.console.MessageConsoleStream;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.editors.text.ILocationProvider;

import org.wangzw.plugin.cppstyle.diff_match_patch.Diff;
import org.wangzw.plugin.cppstyle.ui.CppStyleConstants;
import org.wangzw.plugin.cppstyle.ui.CppStyleMessageConsole;


public class ClangFormatFormatter extends CodeFormatter {
	private MessageConsoleStream err = null;
	private Map<String, ?> options;

	public ClangFormatFormatter() {
		super();
		CppStyleMessageConsole console = CppStyle.buildConsole();
		err = console.getErrorStream();
	}

	@Override
	public String createIndentationString(int indentationLevel) {
		return super.createIndentationString(indentationLevel);
	}

	@Override
	public void setOptions(Map<String, ?> options) {
		if (options != null) {
			this.options = options;
		} else {
			this.options = CCorePlugin.getOptions();
		}
	}

	private String getSourceFilePath() {
		IWorkbench wb = PlatformUI.getWorkbench();
		if (wb != null)
		{
			IWorkbenchWindow window = wb.getActiveWorkbenchWindow();
			if (window != null)
			{
				IWorkbenchPage page = window.getActivePage();
				if (page != null) {
					IEditorPart activeEditor = page.getActiveEditor();
					if (activeEditor != null) {
						IEditorInput editorInput = activeEditor.getEditorInput();
						if (editorInput != null) {
							IPath filePath = getSourceFilePathFromEditorInput(editorInput);
							if (filePath != null) {
								return filePath.toOSString();
							}
						}
					}
				}
			}
		}

		ITranslationUnit tu = (ITranslationUnit) options.get(DefaultCodeFormatterConstants.FORMATTER_TRANSLATION_UNIT);

		if (tu == null) {
			IFile file = (IFile) options.get(DefaultCodeFormatterConstants.FORMATTER_CURRENT_FILE);
			if (file != null) {
				tu = (ITranslationUnit) CoreModel.getDefault().create(file);
			}
		}

		if (tu != null) {
			return tu.getResource().getRawLocation().toOSString();
		} else {
			String root = ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString();
			return new File(root, "a.cc").getAbsolutePath();
		}
	}

	private static IPath getSourceFilePathFromEditorInput(IEditorInput editorInput) {
		if (editorInput instanceof IURIEditorInput) {
			URI uri = ((IURIEditorInput) editorInput).getURI();
			if (uri != null) {
				IPath path = URIUtil.toPath(uri);
				if (path != null) {
					  return path;
				}
			}
		}

		if (editorInput instanceof IFileEditorInput) {
			IFile file = ((IFileEditorInput) editorInput).getFile();
			if (file != null) {
				return file.getLocation();
			}
		}

		if (editorInput instanceof ILocationProvider) {
			return ((ILocationProvider) editorInput).getPath(editorInput);
		}

		return null;
	}

	@Override
	public TextEdit format(int kind, String source, int offset, int length, int arg4, String lineSeparator) {
		TextEdit retval = format(source, getSourceFilePath(), new Region(offset, length));
		return retval != null ? retval : new MultiTextEdit();
	}

	public void formatAndApply(ICEditor editor) {
		IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());

		String path = ((IFileEditorInput) editor.getEditorInput()).getFile().getLocation().toOSString();
		TextEdit res = format(doc.get(), path, null);

		if (res == null) {
			return;
		}

		IDocumentUndoManager manager = DocumentUndoManagerRegistry.getDocumentUndoManager(doc);
		manager.beginCompoundChange();

		try {
			res.apply(doc);
		} catch (MalformedTreeException e) {
			CppStyle.log("Failed to apply change", e);
		} catch (BadLocationException e) {
			CppStyle.log("Failed to apply change", e);
		}

		manager.endCompoundChange();

	}

	private TextEdit format(String source, String path, IRegion region) {
		String clangFormatPath = getClangFormatPath();
		if (checkClangFormat(clangFormatPath) == false) {
			return null;
		}

		String confPath = getClangFormatConfigureFile(path);
		if (confPath == null) {
			err.println(
					"Cannot find .clang-format or _clang-format configuration file under any level "
							+ "parent directories of path (" + path + ").");
			err.println("Clang-format will default to Google style.");
		}

		// make clang-format do its own search for the configuration, but fall back to Google.
		String stdArg = "-style=file";
		String fallbackArg = "-fallback-style=Google";

		ArrayList<String> commands = new ArrayList<String>(
				Arrays.asList(clangFormatPath, "-assume-filename=" + path, stdArg, fallbackArg));

		StringBuffer sb = new StringBuffer();
		sb.append(stdArg + " " + fallbackArg + " ");

		if (region != null) {
			commands.add("-offset=" + region.getOffset());
			commands.add("-length=" + region.getLength());

			sb.append("-offset=");
			sb.append(region.getOffset());
			sb.append(" -length=");
			sb.append(region.getLength());
			sb.append(' ');
		}

		ProcessBuilder builder = new ProcessBuilder(commands);

		String root = ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString();
		builder.directory(new File(root));

		try {
			Process process = builder.start();
			OutputStreamWriter output = new OutputStreamWriter(process.getOutputStream());

			output.write(source);
			output.flush();
			output.close();

			InputStreamReader reader = new InputStreamReader(process.getInputStream());
			InputStreamReader error = new InputStreamReader(process.getErrorStream());

			final char[] buffer = new char[1024];
			final StringBuilder stdout = new StringBuilder();
			final StringBuilder errout = new StringBuilder();

			for (;;) {
				int rsz = reader.read(buffer, 0, buffer.length);

				if (rsz < 0) {
					break;
				}

				stdout.append(buffer, 0, rsz);
			}

			for (;;) {
				int rsz = error.read(buffer, 0, buffer.length);

				if (rsz < 0) {
					break;
				}

				errout.append(buffer, 0, rsz);
			}

			String newSource = stdout.toString();

			int code = process.waitFor();
			if (code != 0) {
				err.println("clang-format return error (" + code + ").");
				err.println(errout.toString());
				return null;
			}

			if (errout.length() > 0) {
				err.println(errout.toString());
				return null;
			}

			if (0 == source.compareTo(newSource)) {
				return null;
			}

			diff_match_patch diff = new diff_match_patch();

			LinkedList<Diff> diffs = diff.diff_main(source, newSource);
			diff.diff_cleanupEfficiency(diffs);

			int offset = 0;
			MultiTextEdit edit = new MultiTextEdit();

			for (Diff d : diffs) {
				switch (d.operation) {
				case INSERT:
					InsertEdit e = new InsertEdit(offset, d.text);
					edit.addChild(e);
					break;
				case DELETE:
					DeleteEdit e1 = new DeleteEdit(offset, d.text.length());
					offset += d.text.length();
					edit.addChild(e1);
					break;
				case EQUAL:
					offset += d.text.length();
					break;
				}
			}

			return edit;

		} catch (IOException e) {
			CppStyle.log("Failed to format code", e);
		} catch (InterruptedException e) {
			CppStyle.log("Failed to format code", e);
		}

		return null;
	}

	private String getClangFormatConfigureFile(String path) {
		File file = new File(path);

		while (file != null) {
			File dir = file.getParentFile();
			if (dir != null) {
				File conf = new File(dir, ".clang-format");
				if (conf.exists()) {
					return conf.getAbsolutePath();
				}

				conf = new File(dir, "_clang-format");
				if (conf.exists()) {
					return conf.getAbsolutePath();
				}
			}

			file = dir;
		}

		return null;
	}

	public boolean checkClangFormat(String clangformat) {
		if (clangformat == null) {
			err.println("clang-format is not specified.");
			return false;
		}

		File file = new File(clangformat);

		if (!file.exists()) {
			err.println("clang-format (" + clangformat + ") does not exist.");
			return false;
		}

		if (!file.canExecute()) {
			err.println("clang-format (" + clangformat + ") is not executable.");
			return false;
		}

		return true;
	}

	private boolean enableClangFormatOnSave(IResource resource) {
		boolean enable = CppStyle.getDefault().getPreferenceStore()
				.getBoolean(CppStyleConstants.ENABLE_CLANGFORMAT_ON_SAVE);

		try {
			IProject project = resource.getProject();
			String enableProjectSpecific = project
					.getPersistentProperty(new QualifiedName("", CppStyleConstants.PROJECTS_PECIFIC_PROPERTY));

			if (enableProjectSpecific != null && Boolean.parseBoolean(enableProjectSpecific)) {
				String value = project
						.getPersistentProperty(new QualifiedName("", CppStyleConstants.ENABLE_CLANGFORMAT_PROPERTY));
				if (value != null) {
					return Boolean.parseBoolean(value);
				}

				return false;
			}
		} catch (CoreException e) {
			CppStyle.log(e);
		}

		return enable;
	}

	public boolean runClangFormatOnSave(IResource resource) {
		if (!enableClangFormatOnSave(resource)) {
			return false;
		}

		String clangFormat = getClangFormatPath();

		if (clangFormat == null) {
			err.println("clang-format command must be specified in preferences.");
			return false;
		}

		File file = new File(clangFormat);

		if (!file.exists()) {
			err.println("clang-format (" + clangFormat + ") does not exist.");
			return false;
		}

		if (!file.canExecute()) {
			err.println("clang-format (" + clangFormat + ") is not executable.");
			return false;
		}

		return true;
	}

	public static String getClangFormatPath() {
		return CppStyle.getDefault().getPreferenceStore().getString(CppStyleConstants.CLANG_FORMAT_PATH);
	}

}