/**
 * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Eclipse Public License (EPL).
 * Please see the license.txt included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package org.python.pydev.editor.actions;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.TextViewer;
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.viewers.ISelection;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.python.pydev.core.IIndentPrefs;
import org.python.pydev.core.docutils.ParsingUtils;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.core.docutils.SyntaxErrorException;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.autoedit.DefaultIndentPrefs;
import org.python.pydev.editor.autoedit.PyAutoIndentStrategy;
import org.python.pydev.shared_core.string.FastStringBuffer;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.utils.DocCmd;
import org.python.pydev.shared_ui.editor.ITextViewerExtensionAutoEditions;

/**
 * Something similar org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor.BracketInserter (but not too similar).
 *
 * @author Fabio Zadrozny
 */
public class PyPeerLinker {

    private IIndentPrefs prefs;

    public void setIndentPrefs(IIndentPrefs prefs) {
        this.prefs = prefs;
    }

    private int linkOffset;
    private int linkExitPos;
    private int linkLen;

    /**
     * Creates a handler that will properly treat backspaces considering python code.
     */
    public static VerifyKeyListener createVerifyKeyListener(final TextViewer viewer) {
        return new VerifyKeyListener() {

            private final PyPeerLinker pyPeerLinker = new PyPeerLinker();

            @Override
            public void verifyKey(VerifyEvent event) {
                if (!event.doit) {
                    return;
                }
                switch (event.character) {
                    case '\'':
                    case '\"':
                    case '[':
                    case '{':
                    case '(':
                        break;
                    default:
                        return;
                }
                if (viewer != null && viewer.isEditable()) {
                    boolean blockSelection = false;
                    try {
                        blockSelection = viewer.getTextWidget().getBlockSelection();
                    } catch (Throwable e) {
                        //that's OK (only available in eclipse 3.5)
                    }
                    if (!blockSelection) {
                        if (viewer instanceof ITextViewerExtensionAutoEditions) {
                            ITextViewerExtensionAutoEditions autoEditions = (ITextViewerExtensionAutoEditions) viewer;
                            if (!autoEditions.getAutoEditionsEnabled()) {
                                return;
                            }
                        }

                        ISelection selection = viewer.getSelection();
                        if (selection instanceof ITextSelection) {
                            IAdaptable adaptable;
                            if (viewer instanceof IAdaptable) {
                                adaptable = (IAdaptable) viewer;
                            } else {
                                adaptable = new IAdaptable() {

                                    @Override
                                    public <T> T getAdapter(Class<T> adapter) {
                                        return null;
                                    }
                                };
                            }

                            //Don't bother in getting the indent prefs from the editor: the default indent prefs are
                            //always global for the settings we want.
                            pyPeerLinker.setIndentPrefs(new DefaultIndentPrefs(adaptable));
                            PySelection ps = new PySelection(viewer.getDocument(), (ITextSelection) selection);

                            if (pyPeerLinker.perform(ps, event.character, viewer)) {
                                event.doit = false;
                            }
                        }
                    }
                }
            }
        };
    }

    /**
     * @param ps
     */
    protected boolean perform(PySelection ps, final char c, TextViewer viewer) {
        linkOffset = -1;
        linkExitPos = -1;
        linkLen = 0;

        boolean literal = true;
        switch (c) {
            case '\'':
            case '\"':
                break;
            case '[':
            case '{':
            case '(':
                literal = false;
                break;
            default:
                return false;
        }

        if (literal) {
            if (!prefs.getAutoLiterals()) {
                return false;
            }
        } else {
            if (!prefs.getAutoParentesis()) {
                return false;
            }
        }

        try {
            IDocument doc = ps.getDoc();
            String contentType = ParsingUtils.getContentType(ps.getDoc(), ps.getAbsoluteCursorOffset());
            boolean isDefaultContext = contentType.equals(ParsingUtils.PY_DEFAULT);
            if (!isDefaultContext) {
                //not handled: leave it up to the auto-indent (if we're in link mode already it may delete the selected text and add a ', which is what we want).
                return false;
            }
            DocCmd docCmd = new DocCmd(ps.getAbsoluteCursorOffset(), ps.getSelLength(), "" + c);
            if (literal) {
                if (!handleLiteral(doc, docCmd, ps, isDefaultContext, prefs)) {
                    return false; //not handled
                }
            } else {
                if (!handleBrackets(ps, c, doc, docCmd, viewer)) {
                    return false; //not handled
                }
            }
            if (linkOffset == -1 || linkExitPos == -1) {
                return true; //it was handled (without the link)
            }

            if (prefs.getAutoLink()) {
                LinkedPositionGroup group = new LinkedPositionGroup();
                group.addPosition(new LinkedPosition(doc, linkOffset, linkLen, LinkedPositionGroup.NO_STOP));

                LinkedModeModel model = new LinkedModeModel();
                model.addGroup(group);
                model.forceInstall();

                if (viewer == null) {
                    return true; //don't actually do the link.
                }

                LinkedModeUI ui = new EditorLinkedModeUI(model, viewer);
                ui.setSimpleMode(true);
                IExitPolicy policy = new IExitPolicy() {

                    @Override
                    public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
                        //Yes, no special exit, if ' is entered again, let's do the needed treatment again instead of going
                        //to the end (only <return> goes to the end).
                        //if (event.character == c) {
                        //    return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
                        //}
                        return null;
                    }
                };
                ui.setExitPolicy(policy);
                ui.setExitPosition(viewer, linkExitPos, 0, Integer.MAX_VALUE);
                ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
                ui.enter();
                IRegion newSelection = ui.getSelectedRegion();
                viewer.setSelectedRange(newSelection.getOffset(), newSelection.getLength());
            } else {
                viewer.setSelectedRange(linkOffset, linkLen);
            }

        } catch (Exception e) {
            Log.log(e);
        }
        return true;
    }

    private boolean handleBrackets(PySelection ps, final char c, IDocument doc, DocCmd docCmd, TextViewer viewer)
            throws BadLocationException {
        if (c == '(') {

            PyAutoIndentStrategy.handleParens(doc, docCmd, prefs);

            docCmd.doExecute(doc);

            //Note that this is done with knowledge on how the handleParens deals with the doc command (not meant as a
            //general thing to apply a doc command).
            if (docCmd.shiftsCaret) {
                //Regular stuff: just shift it and don't link
                if (viewer != null) {
                    viewer.setSelectedRange(docCmd.offset + docCmd.text.length(), 0);
                }
            } else {
                linkOffset = docCmd.caretOffset;
                linkLen = 0;
                linkExitPos = docCmd.offset + docCmd.text.length();
            }

        } else { //  [ or {
            char peer = StringUtils.getPeer(c);
            if (PyAutoIndentStrategy.shouldClose(ps, c, peer)) {
                int offset = ps.getAbsoluteCursorOffset();
                doc.replace(offset, ps.getSelLength(), StringUtils.getWithClosedPeer(c));
                linkOffset = offset + 1;
                linkLen = 0;
                linkExitPos = linkOffset + linkLen + 1;
            } else {
                //No link, just add the char and set the new selected range (if possible)
                docCmd.doExecute(doc);
                if (viewer != null) {
                    viewer.setSelectedRange(docCmd.offset + docCmd.text.length(), 0);
                }
            }
        }

        //Yes, in this situation, all cases are handled.
        return true;
    }

    /**
     * Called right after a ' or "
     *
     * @return false if we should leave the handling to the auto-indent and true if it handled things properly here.
     */
    private boolean handleLiteral(IDocument document, DocumentCommand command, PySelection ps,
            boolean isDefaultContext, IIndentPrefs prefs) throws BadLocationException {
        int offset = ps.getAbsoluteCursorOffset();

        if (command.length > 0) {
            String selectedText = ps.getSelectedText();
            if (selectedText.indexOf('\r') != -1 || selectedText.indexOf('\n') != -1) {
                //we have a new line
                FastStringBuffer buf = new FastStringBuffer(selectedText.length() + 10);
                buf.appendN(command.text, 3);
                buf.append(selectedText);
                buf.appendN(command.text, 3);
                document.replace(offset, ps.getSelLength(), buf.toString());
                linkOffset = offset + 3;
                linkLen = selectedText.length();
                linkExitPos = linkOffset + linkLen + 3;
            } else {
                document.replace(offset, ps.getSelLength(), command.text + selectedText + command.text);
                linkOffset = offset + 1;
                linkLen = selectedText.length();
                linkExitPos = linkOffset + linkLen + 1;
            }
            return true;
        }
        char literalChar = command.text.charAt(0);

        try {
            char nextChar = ps.getCharAfterCurrentOffset();
            if (Character.isJavaIdentifierPart(nextChar)) {
                //we're just before a word (don't try to do anything in this case)
                //e.g. |var (| is cursor position)
                return false;
            }
        } catch (BadLocationException e) {
        }

        String cursorLineContents = ps.getCursorLineContents();
        if (cursorLineContents.indexOf(literalChar) == -1) {
            if (!isDefaultContext) {
                //only add additional chars if on default context.
                return false;
            }
            document.replace(offset, ps.getSelLength(), command.text + command.text);
            linkOffset = offset + 1;
            linkLen = 0;
            linkExitPos = linkOffset + linkLen + 1;
            return true;
        }

        boolean balanced = isLiteralBalanced(cursorLineContents);

        Tuple<String, String> beforeAndAfterMatchingChars = ps.getBeforeAndAfterMatchingChars(literalChar);

        int matchesBefore = beforeAndAfterMatchingChars.o1.length();
        int matchesAfter = beforeAndAfterMatchingChars.o2.length();

        boolean hasMatchesBefore = matchesBefore != 0;
        boolean hasMatchesAfter = matchesAfter != 0;

        if (!hasMatchesBefore && !hasMatchesAfter) {
            //if it's not balanced, this char would be the closing char.
            if (balanced) {
                if (!isDefaultContext) {
                    //only add additional chars if on default context.
                    return false;
                }
                document.replace(offset, ps.getSelLength(), command.text + command.text);
                linkOffset = offset + 1;
                linkLen = 0;
                linkExitPos = linkOffset + linkLen + 1;
                return true;
            }
        } else {
            //we're right after or before a " or '
            return false;
        }
        return false;
    }

    /**
     * @return true if the passed string has balanced ' and "
     */
    private boolean isLiteralBalanced(String cursorLineContents) {
        ParsingUtils parsingUtils = ParsingUtils.create(cursorLineContents, true);

        int offset = 0;
        int end = cursorLineContents.length();
        boolean balanced = true;
        while (offset < end) {
            char curr = cursorLineContents.charAt(offset++);
            if (curr == '"' || curr == '\'') {
                int eaten;
                try {
                    eaten = parsingUtils.eatLiterals(null, offset - 1) + 1;
                } catch (SyntaxErrorException e) {
                    balanced = false;
                    break;
                }
                if (eaten > offset) {
                    offset = eaten;
                }
            }
        }
        return balanced;
    }

    /**
     * In default namespace (used for testing)
     */

    int getLinkLen() {
        return linkLen;
    }

    int getLinkExitPos() {
        return linkExitPos;
    }

    int getLinkOffset() {
        return linkOffset;
    }

}