/*******************************************************************************
 * Copyright (c) 2008, 2018 Phil Muldoon <pkmuldoon@picobot.org>.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Phil Muldoon <pkmuldoon@picobot.org> - initial API and implementation.
 *******************************************************************************/
package org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Pattern;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPartitioningException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextHover;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
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.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.proposals.STPFunctionCompletionProposal;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.proposals.STPProbeCompletionProposal;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.proposals.STPProbevarCompletionProposal;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.ManpageCacher;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.TapsetItemType;
import org.eclipse.linuxtools.systemtap.structures.TreeNode;

public class STPCompletionProcessor implements IContentAssistProcessor, ITextHover {

    private final IContextInformation[] NO_CONTEXTS = new IContextInformation[0];
    private final char[] PROPOSAL_ACTIVATION_CHARS = new char[] { '.' };
    private ICompletionProposal[] NO_COMPLETIONS = new ICompletionProposal[0];

    private static final String GLOBAL_KEYWORD = "global "; //$NON-NLS-1$
    private static final String PROBE_KEYWORD = "probe "; //$NON-NLS-1$
    private static final String FUNCTION_KEYWORD = "function "; //$NON-NLS-1$

    private static final String[][] GLOBAL_KEYWORDS = {
            { GLOBAL_KEYWORD, Messages.STPCompletionProcessor_global },
            { PROBE_KEYWORD, Messages.STPCompletionProcessor_probe },
            { FUNCTION_KEYWORD, Messages.STPCompletionProcessor_function } };

    private STPMetadataSingleton stpMetadataSingleton;

    private static class Token implements IRegion {
        String tokenString;
        int offset;

        public Token(String string, int n) {
            this.tokenString = string;
            this.offset = n;
        }

        @Override
        public int getLength() {
            return this.tokenString.length();
        }

        @Override
        public int getOffset() {
            return this.offset;
        }
    }

    public STPCompletionProcessor() {
        this.stpMetadataSingleton = STPMetadataSingleton.getInstance();
    }

    @Override
    public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer,
            int offset) {
        return computeCompletionProposals(viewer.getDocument(), offset);
    }

    public ICompletionProposal[] computeCompletionProposals(IDocument document, int offset) {

        ITypedRegion partition = null;
        boolean useGlobal = false;

        try {
            partition =
                    ((IDocumentExtension3) document).getPartition(STPProbeScanner.STP_PROBE_PARTITIONING, offset, false);
            if (partition.getOffset() == offset) {
                if (partition.getType() != IDocument.DEFAULT_CONTENT_TYPE && partition.getType() != STPProbeScanner.STP_PROBE) {
                    if (offset > 0) {
                        ITypedRegion prevPartition =
                                ((IDocumentExtension3) document).getPartition(STPProbeScanner.STP_PROBE_PARTITIONING, offset - 1, false);
                        useGlobal = prevPartition.getType() == IDocument.DEFAULT_CONTENT_TYPE;
                    } else {
                        useGlobal = true;
                    }
                }
            }
        } catch (BadLocationException|BadPartitioningException e) {
            return NO_COMPLETIONS;
        }

        String prefix = ""; //$NON-NLS-1$
        String prePrefix = ""; //$NON-NLS-1$

        // Get completion hint from document
        try {
            prefix = getPrefix(document, offset);
            Token previousToken = getPrecedingToken(document, offset - prefix.length() - 1);

            while (previousToken.tokenString.equals("=") || //$NON-NLS-1$
                    previousToken.tokenString.equals(",") ) { //$NON-NLS-1$
                previousToken = getPrecedingToken(document, previousToken.offset - 1);
                previousToken = getPrecedingToken(document, previousToken.offset - 1);
            }

            prePrefix = previousToken.tokenString;

        } catch (BadLocationException e) {
            return NO_COMPLETIONS;
        }

        if (prePrefix.startsWith("probe")) { //$NON-NLS-1$
            return getProbeCompletionList(prefix, offset);
        }

        // If inside a probe return probe variable completions and functions
        // which can be called.
        if (partition.getType() == STPProbeScanner.STP_PROBE) {
            ICompletionProposal[] variableCompletions = getProbeVariableCompletions(document, offset, prefix);
            ICompletionProposal[] functionCompletions = getFunctionCompletions(offset, prefix);

            ArrayList<ICompletionProposal> completions = new ArrayList<>(
                    variableCompletions.length + functionCompletions.length);
            completions.addAll(Arrays.asList(variableCompletions));
            completions.addAll(Arrays.asList(functionCompletions));

            return completions.toArray(new ICompletionProposal[0]);
        } else if (partition.getType() == IDocument.DEFAULT_CONTENT_TYPE || useGlobal) {
            // In the global scope return global keyword completion.
            return getGlobalKeywordCompletion(prefix, offset);
        }

        return NO_COMPLETIONS;
    }

    private ICompletionProposal[] getFunctionCompletions(int offset,
            String prefix) {
        TreeNode[] completionData = stpMetadataSingleton.getFunctionCompletions(prefix);
        ICompletionProposal[] result = new ICompletionProposal[completionData.length];
        for (int i = 0; i < completionData.length; i++) {
            result[i] = new STPFunctionCompletionProposal(
                    completionData[i],
                    prefix.length(),
                    offset);
        }

        return result;
    }

    private ICompletionProposal[] getProbeVariableCompletions(IDocument document, int offset, String prefix) {
        try {
            String probeName = getProbe(document, offset);
            TreeNode[] completionData = stpMetadataSingleton
                    .getProbeVariableCompletions(probeName, prefix);
            ICompletionProposal[] result = new ICompletionProposal[completionData.length];

            for (int i = 0; i < completionData.length; i++) {
                result[i] = new STPProbevarCompletionProposal(
                        completionData[i],
                        prefix.length(),
                        offset,
                        probeName);
            }
            return result;
        } catch (BadLocationException|BadPartitioningException e) {
            return NO_COMPLETIONS;
        }
    }

    /**
     * Returns the full name of the probe surrounding the given
     * offset. This function assumes that the given offset is inside
     * of a {@link STPPartitionScanner#STP_PROBE} section.
     * @param document
     * @param offset
     * @return the probe name
     * @throws BadLocationException
     * @throws BadPartitioningException
     */
    private String getProbe(IDocument document, int offset) throws BadLocationException, BadPartitioningException {
        String probePoint = null;

        ITypedRegion partition
          = ((IDocumentExtension3)document).getPartition(STPProbeScanner.STP_PROBE_PARTITIONING,
                  offset, false);
        String probe = document.get(partition.getOffset(), partition.getLength());

        // make sure that we are inside a probe
        if (probe.startsWith(PROBE_KEYWORD)) {
            probePoint = probe.substring(PROBE_KEYWORD.length(), probe.indexOf('{'));
            probePoint = probePoint.trim();
        }

        return probePoint;
    }

    private ICompletionProposal[] getProbeCompletionList(String prefix, int offset) {
        prefix = canonicalizePrefix(prefix);
        TreeNode[] completionData = stpMetadataSingleton.getProbeCompletions(prefix);

        ICompletionProposal[] result = new ICompletionProposal[completionData.length];
        for (int i = 0; i < completionData.length; i++) {
            result[i] = new STPProbeCompletionProposal(
                            completionData[i],
                            prefix.length(),
                            offset);
        }
        return result;

    }

    /**
     * Returns a standardized version of the given prefix so that completion matching
     * can be performed.
     * For example for process("/some/long/path") this returns process(string);
     * @param prefix
     * @return
     */
    private String canonicalizePrefix(String prefix) {

        if (prefix.isEmpty()) {
            return ""; //$NON-NLS-1$
        }
        prefix = prefix.replaceAll("(?s)\\(\\s*\".*\"\\s*\\)", "(string)"); //$NON-NLS-1$ //$NON-NLS-2$
        prefix = prefix.replaceAll("(?s)\\(\\s*\\d*\\s*\\)", "(number)"); //$NON-NLS-1$ //$NON-NLS-2$
        return prefix;
    }

    private ICompletionProposal[] getGlobalKeywordCompletion(String prefix, int offset) {

        ArrayList<ICompletionProposal> completions = new ArrayList<>();
        int prefixLength = prefix.length();
        for (String[] keyword : GLOBAL_KEYWORDS) {
            if (keyword[0].startsWith(prefix)) {
                CompletionProposal proposal = new CompletionProposal(
                        keyword[0].substring(prefixLength),
                        offset,
                        0,
                        keyword[0].length() - prefixLength,
                        null,
                        keyword[0],
                        new ContextInformation("contextDisplayString", "informationDisplayString"), //$NON-NLS-1$ //$NON-NLS-2$
                        keyword[1]);
                completions.add(proposal);
            }
        }
        return completions.toArray(new ICompletionProposal[0]);
    }

    /**
     * Returns the token preceding the current completion position.
     * @param doc The document for the which the completion is requested.
     * @param prefix The prefix for which the user has requested completion.
     * @param offset Current offset in the document
     * @return The preceding token.
     * @throws BadLocationException
     */
    private Token getPrecedingToken(IDocument doc, int offset) throws BadLocationException {
        // Skip trailing space
        int n = offset;
        while (n >= 0 && Character.isSpaceChar(doc.getChar(n))) {
            n--;
        }

        char c = doc.getChar(n);
        if (isTokenDelimiter(c)) {
            return new Token(Character.toString(c), n);
        }

        int end = n;
        while (n >= 0 && !isTokenDelimiter(doc.getChar(n))) {
            n--;
        }

        return new Token(doc.get(n+1, end-n), n+1);
    }

    private Token getCurrentToken(IDocument doc, int offset) throws BadLocationException {
        char c = doc.getChar(offset);

        if (isDelimiter(c)) {
            return new Token(Character.toString(c), offset);
        }

        int start = offset;
        while (start >= 0 && !isDelimiter(doc.getChar(start))) {
            start--;
        }

        int end = offset;
        while (end < doc.getLength() && !isDelimiter(doc.getChar(end))) {
            end++;
        }

        start++;
        return new Token(doc.get(start, end-start), start);
    }

    private boolean isFunctionRegion(IDocument doc, IRegion region) throws BadLocationException {
        int start = region.getLength() + region.getOffset();
        while (isTokenDelimiter(doc.getChar(start))) {
            start++;
        }
        return doc.getChar(start) == '(';
    }

    /**
     *
     * Return the word the user wants to submit for completion proposals.
     *
     * @param doc - document to insert completion.
     * @param offset - offset of where completion hint was first generated.
     * @return - word to generate completion proposals.
     *
     * @throws BadLocationException
     */
    private String getPrefix(IDocument doc, int offset)
            throws BadLocationException {

        for (int n = offset - 1; n >= 0; n--) {
            char c = doc.getChar(n);
            if (isTokenDelimiter(c)) {
                return doc.get(n + 1, offset - n - 1);
            }
        }
        return ""; //$NON-NLS-1$
    }

    private boolean isTokenDelimiter(char c) {
        if (Character.isWhitespace(c)) {
            return true;
        }

        switch (c) {
        case '\n':
        case '\0':
        case ',':
        case '{':
        case '}':
        case ']':
        case '[':
            return true;
        }
        return false;
    }

    private boolean isDelimiter(char c) {

        if (isTokenDelimiter(c)) {
            return true;
        }

        switch (c) {
            case '.':
            case '$':
                return false;
        }
        return Pattern.matches("\\W", Character.toString(c)); //$NON-NLS-1$
    }

    @Override
    public IContextInformation[] computeContextInformation(ITextViewer viewer,
            int offset) {
        return NO_CONTEXTS;
    }

    @Override
    public char[] getCompletionProposalAutoActivationCharacters() {
        return PROPOSAL_ACTIVATION_CHARS;
    }

    @Override
    public char[] getContextInformationAutoActivationCharacters() {
        return PROPOSAL_ACTIVATION_CHARS;
    }

    @Override
    public IContextInformationValidator getContextInformationValidator() {
        return null;
    }

    @Override
    public String getErrorMessage() {
        // TODO: When does this trigger?
        return "Error."; //$NON-NLS-1$
    }

    @Override
    public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) {
        String documentation = null;
        try {
            String keyword = textViewer.getDocument().get(hoverRegion.getOffset(), hoverRegion.getLength());
            int offset = hoverRegion.getOffset();
            IDocument document = textViewer.getDocument();

            if (getPrecedingToken(document, offset - 1).tokenString.equals(PROBE_KEYWORD.trim())) {
                documentation = ManpageCacher.getDocumentation(TapsetItemType.PROBE, keyword);
            } else {
                ITypedRegion partition =
                        ((IDocumentExtension3)document).getPartition(STPProbeScanner.STP_PROBE_PARTITIONING,
                                offset, false);
                if (partition.getType() == STPProbeScanner.STP_PROBE) {
                    if (isFunctionRegion(document, hoverRegion)) {
                        documentation = ManpageCacher.getDocumentation(TapsetItemType.FUNCTION, keyword);
                    } else {
                        String probe = getProbe(document, offset);
                        if (stpMetadataSingleton.isVariableInProbe(probe, keyword)) {
                            documentation = ManpageCacher.getDocumentation(TapsetItemType.PROBEVAR, probe, keyword);
                        }
                    }
                }
            }

        } catch (BadLocationException|BadPartitioningException e) {
            // Bad hover location/scenario; just ignore it.
        }

        return documentation;
    }

    @Override
    public IRegion getHoverRegion(ITextViewer textViewer, int offset) {
        try {
            return getCurrentToken(textViewer.getDocument(), offset);
        } catch (BadLocationException e) {
            // Bad hover location; just ignore it.
        }

        return null;
    }
}