/******************************************************************************
* Copyright (C) 2006-2013  Fabio Zadrozny and others
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*     Fabio Zadrozny <fabiofz@gmail.com>    - initial API and implementation
*     Jonah Graham <jonah@kichwacoders.com> - ongoing maintenance
******************************************************************************/
package org.python.pydev.shared_ui.proposals;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.python.pydev.shared_core.log.Log;
import org.python.pydev.shared_core.string.FastStringBuffer;
import org.python.pydev.shared_ui.utils.RunInUiThread;

public abstract class AbstractLinkedModeCompletionProposal extends AbstractCompletionProposalExtension implements
        ICompletionProposalExtension {

    private int firstParameterLen = 0;

    /**
    * The number of positions that we should add to the original position.
    * 
    * Used so that when we enter '.', we add an additional position (because '.' will be added when applying the completion) 
    * or so that we can go back one position when the toggle mode (ctrl) is on and a completion with parameters is applied (and they
    * are removed)
    */
    private int nPositionsAdded = 0;

    /**
    * If true, we'll go to the linked mode after applying a completion.
    */
    private boolean goToLinkedMode = true;

    /**
    * Offset forced to be returned (only valid if >= 0)
    */
    private int newForcedOffset = -1;

    /**
    * Constructor where all the info is passed.
    */
    public AbstractLinkedModeCompletionProposal(String replacementString, int replacementOffset, int replacementLength,
            int cursorPosition, Image image, String displayString, IContextInformation contextInformation,
            String additionalProposalInfo, int priority, int onApplyAction, String args) {
        super(replacementString, replacementOffset, replacementLength, cursorPosition, image, displayString,
                contextInformation, additionalProposalInfo, priority, onApplyAction, args);
    }

    /**
    * Constructor where all the info is passed.
    */
    public AbstractLinkedModeCompletionProposal(String replacementString, int replacementOffset, int replacementLength,
            int cursorPosition, Image image, String displayString, IContextInformation contextInformation,
            String additionalProposalInfo, int priority, int onApplyAction, String args, boolean goToLinkedMode) {
        super(replacementString, replacementOffset, replacementLength, cursorPosition, image, displayString,
                contextInformation, additionalProposalInfo, priority, onApplyAction, args);
        this.goToLinkedMode = goToLinkedMode;
    }

    /*
    * @see ICompletionProposal#getSelection(IDocument)
    */
    @Override
    public Point getSelection(IDocument document) {
        if (newForcedOffset >= 0) {
            return new Point(newForcedOffset, 0);
        }

        if (onApplyAction == ON_APPLY_JUST_SHOW_CTX_INFO) {
            return null;
        }
        if (onApplyAction == ON_APPLY_SHOW_CTX_INFO_AND_ADD_PARAMETETRS) {
            if (fArgs.length() > 0) {
                return new Point(fReplacementOffset + fCursorPosition - 1, firstParameterLen); //the difference is the firstParameterLen here (instead of 0)
            }
            return null;
        }
        if (onApplyAction == ON_APPLY_DEFAULT) {
            return new Point(fReplacementOffset + fCursorPosition + nPositionsAdded, firstParameterLen); //the difference is the firstParameterLen here (instead of 0)
        }
        throw new RuntimeException("Unexpected apply mode:" + onApplyAction);
    }

    public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {

        boolean eat = (stateMask & SWT.MOD1) != 0;
        IDocument doc = viewer.getDocument();

        if (!triggerCharAppliesCurrentCompletion(trigger, doc, offset)) {
            newForcedOffset = offset + 1; //+1 because that's the len of the trigger
            return;
        }

        if (onApplyAction == ON_APPLY_JUST_SHOW_CTX_INFO) {
            return;
        }
        if (onApplyAction == ON_APPLY_SHOW_CTX_INFO_AND_ADD_PARAMETETRS) {
            try {
                String args;
                if (fArgs.length() > 0) {
                    args = fArgs.substring(1, fArgs.length() - 1); //remove the parenthesis
                } else {
                    args = "";
                }
                super.apply(doc);

                if (!goToLinkedMode) {
                    return;
                }

                int iPar = -1;
                int exitPos = offset + args.length() + 1;
                goToLinkedModeFromArgs(viewer, offset, doc, exitPos, iPar, args);

            } catch (BadLocationException e) {
                Log.log(e);
            }
            return;
        }

        if (onApplyAction == ON_APPLY_DEFAULT) {
            try {
                int dif = offset - fReplacementOffset;
                String strToAdd = fReplacementString.substring(dif);
                boolean doReturn = applyOnDoc(offset, eat, doc, dif, trigger);

                if (doReturn || !goToLinkedMode) {
                    return;
                }

                //ok, now, on to the linking part
                int iPar = strToAdd.indexOf('(');
                if (iPar != -1 && strToAdd.charAt(strToAdd.length() - 1) == ')') {
                    String newStr = strToAdd.substring(iPar + 1, strToAdd.length() - 1);
                    goToLinkedModeFromArgs(viewer, offset, doc, offset + strToAdd.length() + nPositionsAdded, iPar,
                            newStr);
                }
            } catch (BadLocationException e) {
                Log.log(e);
            }
            return;
        }

        throw new RuntimeException("Unexpected apply mode:" + onApplyAction);

    }

    /**
    * Applies the changes in the document (useful for testing)
    * 
    * @param offset the offset where the change should be applied
    * @param eat whether we should 'eat' the selection (on toggle)
    * @param doc the document where the changes should be applied 
    * @param dif the difference between the offset and and the replacement offset  
    * 
    * @return whether we should return (and not keep on with the linking mode)
    * @throws BadLocationException
    */
    public boolean applyOnDoc(int offset, boolean eat, IDocument doc, int dif, char trigger)
            throws BadLocationException {
        boolean doReturn = false;

        String rep = fReplacementString;
        int iPar = rep.indexOf('(');

        if (eat) {

            //behavior change: when we have a parenthesis and we're in toggle (eat) mode, let's not add the 
            //parenthesis anymore.
            if (/*fLastIsPar &&*/iPar != -1) {
                rep = rep.substring(0, iPar);
                doc.replace(offset - dif, dif + this.fLen, rep);
                //if the last was a parenthesis, there's nothing to link, so, let's return
                if (!fLastIsPar) {
                    nPositionsAdded = -1;
                }
                doReturn = true;
            } else {
                int sumReplace = 0;
                if (rep.endsWith("=")) {
                    //Special case when applying keyword completions (i.e.: call(param=10)), where the text added is "param=")
                    try {
                        char c = doc.getChar(offset + this.fLen);
                        if (c == '=') {
                            sumReplace++;
                        }
                    } catch (BadLocationException e) {
                        //just ignore it!
                    }
                }
                doc.replace(offset - dif, dif + this.fLen + sumReplace, rep);
            }
        } else {
            if (trigger == '.' || trigger == '(') {
                if (iPar != -1) {
                    //if we had a completion with parameters, we should just remove everything that would appear after
                    //the parenthesis -- and we don't need to raise nTriggerCharsAdded because the cursor position would
                    //already be after the '(. -- which in this case we'll substitute for a '.'
                    rep = rep.substring(0, iPar);
                } else {
                    nPositionsAdded = 1;
                }
                rep = rep + trigger;

                if (trigger == '(') {
                    rep += ')';
                }

                //linking should not happen when applying '.'
                doReturn = true;
            }

            //if the trigger is ')', just let it apply regularly -- so, ')' will only be added if it's already in the completion.
            doc.replace(offset - dif, dif, rep);
        }
        return doReturn;
    }

    private void goToLinkedModeFromArgs(ITextViewer viewer, int offset, IDocument doc, int exitPos, int iPar,
            String newStr) throws BadLocationException {
        if (!goToLinkedMode) {
            return;
        }
        List<Integer> offsetsAndLens = new ArrayList<Integer>();

        FastStringBuffer buffer = new FastStringBuffer();
        for (int i = 0; i < newStr.length(); i++) {
            char c = newStr.charAt(i);

            if (Character.isJavaIdentifierPart(c)) {
                if (buffer.length() == 0) {
                    offsetsAndLens.add(i);
                    buffer.append(c);
                } else {
                    buffer.append(c);
                }
            } else {
                if (buffer.length() > 0) {
                    offsetsAndLens.add(buffer.length());
                    buffer.clear();
                }
            }
        }
        if (buffer.length() > 0) {
            offsetsAndLens.add(buffer.length());
        }
        buffer = null;

        goToLinkedMode(viewer, offset, doc, exitPos, iPar, offsetsAndLens);
    }

    private void goToLinkedMode(ITextViewer viewer, int offset, IDocument doc, int exitPos, int iPar,
            List<Integer> offsetsAndLens) throws BadLocationException {
        if (!goToLinkedMode) {
            return;
        }
        if (offsetsAndLens.size() > 0) {
            LinkedModeModel model = new LinkedModeModel();

            for (int i = 0; i < offsetsAndLens.size(); i++) {
                Integer offs = offsetsAndLens.get(i);
                i++;
                Integer len = offsetsAndLens.get(i);
                if (i == 1) {
                    firstParameterLen = len;
                }
                int location = offset + iPar + offs + 1;
                LinkedPositionGroup group = new LinkedPositionGroup();
                ProposalPosition proposalPosition = new ProposalPosition(doc, location, len, 0,
                        new ICompletionProposal[0]);
                group.addPosition(proposalPosition);
                model.addGroup(group);
            }

            model.forceInstall();

            final LinkedModeUI ui = new EditorLinkedModeUI(model, viewer);
            ui.setDoContextInfo(true); //set it to request the ctx info from the completion processor
            ui.setExitPosition(viewer, exitPos, 0, Integer.MAX_VALUE);
            Runnable r = new Runnable() {
                public void run() {
                    ui.enter();
                }
            };
            RunInUiThread.async(r);

        }
    }

    //-------------------------------------------- ICompletionProposalExtension

    /**
    * We want to apply it on \n or on '.'
    * 
    * When . is entered, the user will finish (and apply) the current completion
    * and request a new one with '.'
    * 
    * If not added, it won't request the new one (and will just stop the current)
    */
    public char[] getTriggerCharacters() {
        return null;
    }

    @Override
    public String toString() {
        return "AbstractLinkedModeCompletionProposal(" + this.getDisplayString() + ")";
    }

    //testing
    void setLen(int i) {
        this.fLen = i;
    }

}
