package ts.eclipse.jface.text.contentassist;

import java.util.List;
import java.util.stream.Collectors;

import org.eclipse.jface.internal.text.html.BrowserInformationControl;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ContextInformation;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.InclusivePositionUpdater;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

import ts.TypeScriptException;
import ts.client.ITypeScriptServiceClient;
import ts.client.codefixes.CodeAction;
import ts.client.completions.CompletionEntry;
import ts.client.completions.CompletionEntryDetails;
import ts.client.completions.ICompletionEntryMatcher;
import ts.client.completions.SymbolDisplayPart;
import ts.eclipse.jface.images.TypeScriptImagesRegistry;
import ts.eclipse.jface.text.DocumentUtils;
import ts.eclipse.jface.text.HoverControlCreator;
import ts.eclipse.jface.text.PresenterControlCreator;
import ts.utils.StringUtils;
import ts.utils.TypeScriptHelper;

public class TypeScriptCompletionProposal extends CompletionEntry
		implements ICompletionProposal, ICompletionProposalExtension, ICompletionProposalExtension2,
		ICompletionProposalExtension3, ICompletionProposalExtension6 {

	private static final String SPACE = " ";
	private static final String RPAREN = ")";
	private static final String LPAREN = "(";
	private static final String COMMA = ",";

	private int cursorPosition;
	private int replacementOffset;
	private int replacementlength;
	private boolean contextInformationComputed;
	private IContextInformation contextInformation;
	private boolean fToggleEating;
	private StyledString fDisplayString;

	private IRegion fSelectedRegion; // initialized by apply()
	private IPositionUpdater fUpdater;

	private String fReplacementString;

	private Arguments arguments;
	private ITextViewer fTextViewer;

	private int position;
	private String prefix;
	private final ITextViewer originalTextViewer;

	private HoverControlCreator tsControlCreator;

	public TypeScriptCompletionProposal(ICompletionEntryMatcher matcher, String fileName, int line, int offset,
			ITypeScriptServiceClient client, int position, String prefix, ITextViewer textViewer) {
		super(matcher, fileName, line, offset, client);
		this.position = position;
		this.prefix = prefix;
		this.originalTextViewer = textViewer;
	}

	private void initIfNeeded() {
		if (fReplacementString != null) {
			return;
		}
		String name = super.getName();
		fReplacementString = name;
		this.cursorPosition = name.length();
		this.replacementOffset = position - prefix.length();
		setReplacementLength(prefix.length());
		this.contextInformationComputed = false;
		this.fDisplayString = new StyledString(getName());
	}

	public void setReplacementLength(int replacementlength) {
		this.replacementlength = replacementlength;
	}

	@Override
	public void apply(IDocument document) {
		initIfNeeded();
		CompletionProposal proposal = new CompletionProposal(getReplacementString(), getReplacementOffset(),
				getReplacementLength(), getCursorPosition(), getImage(), getDisplayString(), getContextInformation(),
				getAdditionalProposalInfo());
		proposal.apply(document);
	}

	@Override
	public void apply(IDocument document, char trigger, int offset) {
		initIfNeeded();
		// compute replacement string
		String replacement = computeReplacementString(document, offset);
		setReplacementString(replacement);

		updateReplacementLengthForString(document, offset, replacement);

		// apply the replacement.
		CompletionProposal proposal = new CompletionProposal(getReplacementString(), getReplacementOffset(),
				getReplacementLength(), getCursorPosition(), getImage(), getDisplayString(), getContextInformation(),
				getAdditionalProposalInfo());
		// we currently don't do anything special for which character
		// selected the proposal, and where the cursor offset is
		// but we might in the future...
		proposal.apply(document);
		
		int oldlength = document.getLength();
		int newlength = oldlength;
		try {
			List<CompletionEntryDetails> entryDetails = super.getEntryDetails();
			if (entryDetails != null && entryDetails.size() > 0) {
				List<CodeAction> codeActions = entryDetails.get(0).getCodeActions();
				if (codeActions != null) {
					try {
						DocumentUtils.applyEdits(document, codeActions.get(0).getChanges().stream()
								.flatMap(f -> f.getTextChanges().stream()).collect(Collectors.toList()));
						
						newlength = document.getLength();
					} catch (Exception e) {						
						e.printStackTrace();
					}
				}
			}
		} catch (TypeScriptException e) {
		}
		
		int baseOffset = getReplacementOffset() + newlength - oldlength;

		if (arguments != null && !arguments.isEmpty() && getTextViewer() != null) {
			try {// adjust offset of the whole arguments
				arguments.setBaseOffset(baseOffset);
				// guess parameters if "guess-types" tern plugin is checked.
				// guessParameters(offset);
				// Create group.
				Arg arg = null;
				LinkedModeModel model = new LinkedModeModel();
				for (int i = 0; i != arguments.size(); i++) {
					arg = arguments.get(i);
					LinkedPositionGroup group = new LinkedPositionGroup();
					if (arg.getProposals() == null) {
						group.addPosition(new LinkedPosition(document, arg.getOffset(), arg.getLength(),
								LinkedPositionGroup.NO_STOP));
					} else {
						ensurePositionCategoryInstalled(document, model);
						document.addPosition(getCategory(), arg);
						group.addPosition(new ProposalPosition(document, arg.getOffset(), arg.getLength(),
								LinkedPositionGroup.NO_STOP, arg.getProposals()));
					}
					model.addGroup(group);
				}

				model.forceInstall();
				/*
				 * JavaEditor editor = getJavaEditor(); if (editor != null) {
				 * model.addLinkingListener(new EditorHighlightingSynchronizer( editor)); }
				 */

				LinkedModeUI ui = new EditorLinkedModeUI(model, getTextViewer());
				ui.setExitPosition(getTextViewer(), baseOffset + replacement.length(), 0, Integer.MAX_VALUE);
				ui.setExitPolicy(new ExitPolicy(')', document));
				ui.setDoContextInfo(true);
				ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT);
				ui.enter();

				fSelectedRegion = ui.getSelectedRegion();

			} catch (BadLocationException e) {
				ensurePositionCategoryRemoved(document);
				// JavaScriptPlugin.log(e);
				// openErrorDialog(e);
			} catch (BadPositionCategoryException e) {
				ensurePositionCategoryRemoved(document);
				// JavaScriptPlugin.log(e);
				// openErrorDialog(e);
			}

		} else {
			int newOffset = baseOffset + replacement.length();
			fSelectedRegion = new Region(newOffset, 0);
		}
	}

	private String computeReplacementString(IDocument document, int offset) {

		try {
			if (super.isFunction()) {
				List<CompletionEntryDetails> entryDetails = super.getEntryDetails();
				if (entryDetails != null && entryDetails.size() > 0) {
					// It's a function
					// compute replacement string

					String indentation = getIndentation(document, offset);
					arguments = new Arguments();

					StringBuilder replacement = new StringBuilder(super.getName());
					replacement.append(LPAREN);
					setCursorPosition(replacement.length());
					computeReplacementString(entryDetails.get(0).getDisplayParts(), replacement, arguments, indentation,
							1, true);
					replacement.append(RPAREN);
					return replacement.toString();
				}
			}

		} catch (TypeScriptException e) {
		}
		return getReplacementString();
	}

	/**
	 * Compute replacement string for the given function.
	 * 
	 * @param parameters
	 * @param replacement
	 * @param arguments
	 * @param indentation
	 * @param nbIndentations
	 * @param initialFunction
	 */
	private void computeReplacementString(List<SymbolDisplayPart> parameters, StringBuilder replacement,
			Arguments arguments, String indentation, int nbIndentations, boolean initialFunction) {
		int count = parameters.size();
		SymbolDisplayPart part = null;
		String paramName = null;
		boolean hasParam = false;
		for (int i = 0; i != count; i++) {
			part = parameters.get(i);
			if (!part.isParameterName()) {
				continue;
			}
			if (hasParam) {
				// if (prefs.beforeComma)
				// buffer.append(SPACE);
				replacement.append(COMMA);
				// if (prefs.afterComma)
				replacement.append(SPACE);
			}

			int offset = replacement.length();
			paramName = part.getText();
			// to select focus for parameter
			replacement.append(paramName);
			arguments.addArg(offset, paramName.length());
			hasParam = true;
		}
	}

	/**
	 * Returns the indentation characters from the given line.
	 * 
	 * @param document
	 * @param offset
	 * @return the indentation characters from the given line.
	 */
	private String getIndentation(IDocument document, int offset) {
		try {
			IRegion lineRegion = document.getLineInformationOfOffset(offset);
			String lineText = document.get(lineRegion.getOffset(), lineRegion.getLength());
			StringBuilder indentation = new StringBuilder();
			char[] chars = lineText.toCharArray();
			char c;
			for (int i = 0; i < chars.length; i++) {
				c = chars[i];
				if (c == ' ' || c == '\t') {
					indentation.append(c);
				} else {
					break;
				}
			}
			return indentation.toString();

		} catch (BadLocationException e1) {
		}
		return "";
	}

	/**
	 * Compute new replacement length for string replacement.
	 * 
	 * @param document
	 * @param offset
	 * @param replacement
	 */
	protected void updateReplacementLengthForString(IDocument document, int offset, String replacement) {
		boolean isString = replacement.startsWith("\"") || replacement.startsWith("'");
		if (isString) {
			int length = document.getLength();
			int pos = offset;
			char c;
			while (pos < length) {
				try {
					c = document.getChar(pos);
					switch (c) {
					case '\r':
					case '\n':
					case '\t':
					case ' ':
						return;
					case '"':
					case '\'':
						setReplacementLength(getReplacementLength() + pos - offset + 1);
						return;
					}
					++pos;
				} catch (BadLocationException e) {
					e.printStackTrace();
				}
			}
		}
	}

	@Override
	public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
		initIfNeeded();
		IDocument document = viewer.getDocument();
		if (fTextViewer == null) {
			fTextViewer = viewer;
		}
		// don't eat if not in preferences, XOR with modifier key 1 (Ctrl)
		// but: if there is a selection, replace it!
		Point selection = viewer.getSelectedRange();
		fToggleEating = (stateMask & SWT.MOD1) != 0;
		int newLength = selection.x + selection.y - getReplacementOffset();
		if ((insertCompletion() ^ fToggleEating) && newLength >= 0) {
			setReplacementLength(newLength);
		}
		apply(document, trigger, offset);
		fToggleEating = false;
	}

	private boolean insertCompletion() {
		return true;
	}

	protected String getReplacementString() {
		return fReplacementString;
	}

	/**
	 * @param replacementString
	 *            The fReplacementString to set.
	 */
	public void setReplacementString(String replacementString) {
		fReplacementString = replacementString;
	}

	protected int getReplacementLength() {
		return replacementlength;
	}

	protected int getReplacementOffset() {
		return replacementOffset;
	}

	protected int getCursorPosition() {
		return cursorPosition;
	}

	public void setCursorPosition(int cursorPosition) {
		this.cursorPosition = cursorPosition;
	}

	@Override
	public String getAdditionalProposalInfo() {
		initIfNeeded();
		try {
			List<CompletionEntryDetails> details = super.getEntryDetails();
			if (details == null || details.size() < 1) {
				return null;
			}
			return toHtml(details);
		} catch (TypeScriptException e) {
			e.printStackTrace();
		}
		return "";
	}

	protected String toHtml(List<CompletionEntryDetails> details) {
		CompletionEntryDetails firstDetails = details.get(0);
		String html = TypeScriptHelper.text(firstDetails.getDocumentation(), true);
		if (StringUtils.isEmpty(html)) {
			html = TypeScriptHelper.text(firstDetails.getDisplayParts(), true);
		}
		return html;
	}

	@Override
	public IContextInformation getContextInformation() {
		if (contextInformationComputed) {
			return contextInformation;
		}
		this.contextInformation = createContextInformation();
		contextInformationComputed = true;
		return contextInformation;
	}

	private IContextInformation createContextInformation() {
		try {
			String information = null;
			List<CompletionEntryDetails> entryDetails = super.getEntryDetails();
			if (entryDetails == null || entryDetails.size() < 1) {
				return null;
			}
			if (isFunction()) {
				information = TypeScriptHelper.extractFunctionParameters(entryDetails.get(0).getDisplayParts());
			}
			return information != null ? new ContextInformation("", information) : null;
		} catch (TypeScriptException e) {
		}
		return null;
	}

	@Override
	public int getContextInformationPosition() {
		initIfNeeded();
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=110355
		// return getCursorPosition();
		if (getContextInformation() == null)
			return getReplacementOffset() - 1;
		return getReplacementOffset() + getCursorPosition();
	}

	@Override
	public String getDisplayString() {
		initIfNeeded();
		if (fDisplayString != null) {
			return fDisplayString.getString();
		}
		return ""; //$NON-NLS-1$
	}

	@Override
	public Image getImage() {
		return TypeScriptImagesRegistry.getImage(this);
	}

	@Override
	public Point getSelection(IDocument document) {
		initIfNeeded();
		// CompletionProposal proposal = new
		// CompletionProposal(getReplacementString(), getReplacementOffset(),
		// getReplacementLength(), getCursorPosition(), getImage(),
		// getDisplayString(), getContextInformation(),
		// getAdditionalProposalInfo());
		// return proposal.getSelection(document);
		if (fSelectedRegion == null) {
			return new Point(getReplacementOffset(), 0);
		}
		return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
	}

	public ITextViewer getTextViewer() {
		return fTextViewer;
	}

	@Override
	public char[] getTriggerCharacters() {
		return null;
	}

	@Override
	public IInformationControlCreator getInformationControlCreator() {
		Shell shell = getActiveWorkbenchShell();
		if (shell == null || !BrowserInformationControl.isAvailable(shell))
			return null;
		if (tsControlCreator == null) {
			PresenterControlCreator presenterControlCreator = new PresenterControlCreator();
			tsControlCreator = new HoverControlCreator(presenterControlCreator, true);
		}
		return tsControlCreator;
	}

	protected Shell getActiveWorkbenchShell() {
		return null;
	}

	@Override
	public int getPrefixCompletionStart(IDocument document, int completionOffset) {
		initIfNeeded();
		return replacementOffset;
	}

	@Override
	public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
		return null;
	}

	@Override
	public void selected(ITextViewer viewer, boolean smartToggle) {

	}

	@Override
	public void unselected(ITextViewer viewer) {

	}

	@Override
	public boolean validate(IDocument document, int offset, DocumentEvent event) {
		initIfNeeded();
		if (offset < replacementOffset) {
			return false;
		}

		int replacementOffset = getReplacementOffset();
		String word = getReplacementString();
		int wordLength = word == null ? 0 : word.length();
		if (offset > replacementOffset + wordLength) {
			return false;
		}

		try {
			int length = offset - replacementOffset;
			String start = document.get(replacementOffset, length);
			return super.updatePrefix(start);
			// if (word == null) {
			// return false;
			// }
			// int[] bestSequence = getMatcher().bestSubsequence(word, start);
			// if (bestSequence != null && bestSequence.length > 0) {
			// super.updatePrefix(start);
			// return true;
			// }
		} catch (BadLocationException x) {
		}

		return false;

		/*
		 * 
		 * 
		 * boolean validated = isMatchWord(document, offset, getReplacementString()); if
		 * (validated) { StyledString styledDisplayString = new StyledString();
		 * styledDisplayString.append(getName()); String pattern = if (pattern != null
		 * && pattern.length() > 0) { String displayString =
		 * styledDisplayString.getString(); int[] bestSequence =
		 * getMatcher().bestSubsequence(displayString, pattern); int highlightAdjustment
		 * = 0; for (int index : bestSequence) { styledDisplayString.setStyle(index +
		 * highlightAdjustment, 1, null); } } }
		 * 
		 * // if (fUpdateLengthOnValidate && event != null) { // replacementLength +=
		 * event.fText.length() - event.fLength; // adjust // the // replacement //
		 * length // by // the // event's // text // replacement // } return validated;
		 */
	}

	protected boolean isMatchWord(IDocument document, int offset, String word) {
		int replacementOffset = getReplacementOffset();
		int wordLength = word == null ? 0 : word.length();
		if (offset > replacementOffset + wordLength)
			return false;

		try {
			int length = offset - replacementOffset;
			String start = document.get(replacementOffset, length);
			if (word == null) {
				return false;
			}
			int[] bestSequence = getMatcher().bestSubsequence(word, start);
			return bestSequence != null && bestSequence.length > 0;
		} catch (BadLocationException x) {
		}

		return false;
	}

	@Override
	public boolean isValidFor(IDocument document, int offset) {
		return false; // validate(document, offset, null);
	}

	@Override
	public StyledString getStyledDisplayString() {
		initIfNeeded();
		return fDisplayString;
	}

	/**
	 * Computes the token at the given <code>offset</code> in <code>document</code>
	 * to emphasize the ranges matching this token in proposal's display string.
	 * 
	 * @param document
	 *            the document where content assist is invoked
	 * @param offset
	 *            the offset in the document at current caret location
	 * @return the token at the given <code>offset</code> in <code>document</code>
	 *         to be used for emphasizing matching ranges in proposal's display
	 *         string
	 * @since 3.12
	 */
	protected String getPatternToEmphasizeMatch(IDocument document, int offset) {
		int start = getPrefixCompletionStart(document, offset);
		int patternLength = offset - start;
		String pattern = null;
		try {
			pattern = document.get(start, patternLength);
		} catch (BadLocationException e) {
			// return null
		}
		return pattern;
	}

	private void ensurePositionCategoryInstalled(final IDocument document, LinkedModeModel model) {
		if (!document.containsPositionCategory(getCategory())) {
			document.addPositionCategory(getCategory());
			fUpdater = new InclusivePositionUpdater(getCategory());
			document.addPositionUpdater(fUpdater);

			model.addLinkingListener(new ILinkedModeListener() {

				/*
				 * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.
				 * eclipse.jface.text.link.LinkedModeModel, int)
				 */
				public void left(LinkedModeModel environment, int flags) {
					ensurePositionCategoryRemoved(document);
				}

				public void suspend(LinkedModeModel environment) {
				}

				public void resume(LinkedModeModel environment, int flags) {
				}
			});
		}
	}

	private void ensurePositionCategoryRemoved(IDocument document) {
		if (document.containsPositionCategory(getCategory())) {
			try {
				document.removePositionCategory(getCategory());
			} catch (BadPositionCategoryException e) {
				// ignore
			}
			document.removePositionUpdater(fUpdater);
		}
	}

	protected static final class ExitPolicy implements IExitPolicy {

		final char fExitCharacter;
		private final IDocument fDocument;

		public ExitPolicy(char exitCharacter, IDocument document) {
			fExitCharacter = exitCharacter;
			fDocument = document;
		}

		public ExitFlags doExit(LinkedModeModel environment, VerifyEvent event, int offset, int length) {

			if (event.character == fExitCharacter) {
				if (environment.anyPositionContains(offset))
					return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
				else
					return new ExitFlags(ILinkedModeListener.UPDATE_CARET, true);
			}

			switch (event.character) {
			case ';':
				return new ExitFlags(ILinkedModeListener.NONE, true);
			case SWT.CR:
				// when entering an anonymous class as a parameter, we don't
				// want
				// to jump after the parenthesis when return is pressed
				if (offset > 0) {
					try {
						if (fDocument.getChar(offset - 1) == '{')
							return new ExitFlags(ILinkedModeListener.EXIT_ALL, true);
					} catch (BadLocationException e) {
					}
				}
				return null;
			default:
				return null;
			}
		}

	}

	private String getCategory() {
		return "TypeScriptCompletionProposal_" + toString(); //$NON-NLS-1$
	}

	public ITextViewer getOriginalTextViewer() {
		return originalTextViewer;
	}
}
