/*
 * 05/26/2012
 *
 * TemplateCompletion.java - A completion used to insert boilerplate code
 * snippets that have arbitrary sections the user will want to change, such as
 * for-loops.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package com.fr.design.gui.autocomplete;

import com.fr.design.gui.autocomplete.TemplatePiece.Param;
import com.fr.design.gui.autocomplete.TemplatePiece.ParamCopy;
import com.fr.design.gui.autocomplete.TemplatePiece.Text;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.RSyntaxUtilities;

import javax.swing.text.*;
import java.util.ArrayList;
import java.util.List;


/**
 * A completion made up of a template with arbitrary parameters that the user
 * can tab through and fill in.  This completion type is useful for inserting
 * common boilerplate code, such as for-loops.<p>
 * <p/>
 * The format of a template is similar to those in Eclipse.  The following
 * example would be the format for a for-loop template:
 * <p/>
 * <pre>
 * for (int ${i} = 0; ${i} &lt; ${array}.length; ${i}++) {
 *    ${cursor}
 * }
 * </pre>
 * <p/>
 * In the above example, the first <code>${i}</code> is a parameter for the
 * user to type into; all the other <code>${i}</code> instances are
 * automatically changed to what the user types in the first one.  The parameter
 * named <code>${cursor}</code> is the "ending position" of the template.  It's
 * where the caret moves after it cycles through all other parameters.  If the
 * user types into it, template mode terminates.  If more than one
 * <code>${cursor}</code> parameter is specified, behavior is undefined.<p>
 * <p/>
 * Two dollar signs in a row ("<code>$$</code>") will be evaluated as a single
 * dollar sign.  Otherwise, the template parsing is pretty straightforward and
 * fault-tolerant.<p>
 * <p/>
 * Leading whitespace is automatically added to lines if the template spans
 * more than one line, and if used with a text component using a
 * <code>PlainDocument</code>, tabs will be converted to spaces if requested.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class TemplateCompletion extends AbstractCompletion
        implements ParameterizedCompletion {

    private List<TemplatePiece> pieces;

    private String inputText;

    private String definitionString;

    private String shortDescription;

    private String summary;

    /**
     * The template's parameters.
     */
    private List<Parameter> params;


    public TemplateCompletion(CompletionProvider provider,
                              String inputText, String definitionString, String template) {
        this(provider, inputText, definitionString, template, null, null);
    }


    public TemplateCompletion(CompletionProvider provider,
                              String inputText, String definitionString, String template,
                              String shortDescription, String summary) {
        super(provider);
        this.inputText = inputText;
        this.definitionString = definitionString;
        this.shortDescription = shortDescription;
        this.summary = summary;
        pieces = new ArrayList<TemplatePiece>(3);
        params = new ArrayList<Parameter>(3);
        parse(template);
    }


    private void addTemplatePiece(TemplatePiece piece) {
        pieces.add(piece);
        if (piece instanceof Param && !"cursor".equals(piece.getText())) {
            final String type = null; // TODO
            Parameter param = new Parameter(type, piece.getText());
            params.add(param);
        }
    }


    @Override
    public String getInputText() {
        return inputText;
    }


    private String getPieceText(int index, String leadingWS) {
        TemplatePiece piece = pieces.get(index);
        String text = piece.getText();
        if (text.indexOf('\n') > -1) {
            text = text.replaceAll("\n", "\n" + leadingWS);
        }
        return text;
    }


    /**
     * Returns <code>null</code>; template completions insert all of their
     * text via <code>getInsertionInfo()</code>.
     *
     * @return <code>null</code> always.
     */
    public String getReplacementText() {
        return null;
    }


    public String getSummary() {
        return summary;
    }


    public String getDefinitionString() {
        return definitionString;
    }

    public String getShortDescription() {
        return shortDescription;
    }


    /**
     * {@inheritDoc}
     */
    public boolean getShowParameterToolTip() {
        return false;
    }


    public ParameterizedCompletionInsertionInfo getInsertionInfo(
            JTextComponent tc, boolean replaceTabsWithSpaces) {

        ParameterizedCompletionInsertionInfo info =
                new ParameterizedCompletionInsertionInfo();

        StringBuilder sb = new StringBuilder();
        int dot = tc.getCaretPosition();

        // Get the range in which the caret can move before we hide
        // this tool tip.
        int minPos = dot;
        Position maxPos = null;
        int defaultEndOffs = -1;
        try {
            maxPos = tc.getDocument().createPosition(dot);
        } catch (BadLocationException ble) {
             // Never happens
        }
        info.setCaretRange(minPos, maxPos);
        int selStart = dot; // Default value
        int selEnd = selStart;

        Document doc = tc.getDocument();
        String leadingWS = null;
        try {
            leadingWS = RSyntaxUtilities.getLeadingWhitespace(doc, dot);
        } catch (BadLocationException ble) { // Never happens

            leadingWS = "";
        }

        // Create the text to insert (keep it one completion for
        // performance and simplicity of undo/redo).
        int start = dot;
        for (int i = 0; i < pieces.size(); i++) {
            TemplatePiece piece = pieces.get(i);
            String text = getPieceText(i, leadingWS);
            if (piece instanceof Text) {
                if (replaceTabsWithSpaces) {
                    start = possiblyReplaceTabsWithSpaces(sb, text, tc, start);
                } else {
                    sb.append(text);
                    start += text.length();
                }
            } else if (piece instanceof Param && "cursor".equals(text)) {
                defaultEndOffs = start;
            } else {
                int end = start + text.length();
                sb.append(text);
                if (piece instanceof Param) {
                    info.addReplacementLocation(start, end);
                    if (selStart == dot) {
                        selStart = start;
                        selEnd = selStart + text.length();
                    }
                } else if (piece instanceof ParamCopy) {
                    info.addReplacementCopy(piece.getText(), start, end);
                }
                start = end;
            }
        }

        // Highlight the first parameter.  If no params were specified, move
        // the caret to the ${cursor} location, if specified
        if (selStart == minPos && selStart == selEnd && getParamCount() == 0) {
            if (defaultEndOffs > -1) { // ${cursor} specified
                selStart = selEnd = defaultEndOffs;
            }
        }
        info.setInitialSelection(selStart, selEnd);

        if (defaultEndOffs > -1) {
            // Keep this location "after" all others when tabbing
            info.addReplacementLocation(defaultEndOffs, defaultEndOffs);
        }
        info.setDefaultEndOffs(defaultEndOffs);
        info.setTextToInsert(sb.toString());

        return info;

    }


    /**
     * {@inheritDoc}
     */
    public Parameter getParam(int index) {
        return params.get(index);
    }


    /**
     * {@inheritDoc}
     */
    public int getParamCount() {
        return params == null ? 0 : params.size();
    }


    /**
     * Returns whether a parameter is already defined with a specific name.
     *
     * @param name The name.
     * @return Whether a parameter is defined with that name.
     */
    private boolean isParamDefined(String name) {
        for (int i = 0; i < getParamCount(); i++) {
            Parameter param = getParam(i);
            if (name.equals(param.getName())) {
                return true;
            }
        }
        return false;
    }


    /**
     * Parses a template string into logical pieces used by this class.
     *
     * @param template The template to parse.
     */
    private void parse(String template) {

        int offs = 0;
        int lastOffs = 0;

        while ((offs = template.indexOf('$', lastOffs)) > -1 && offs < template.length() - 1) {

            char next = template.charAt(offs + 1);
            switch (next) {
                case '$': // "$$" => escaped single dollar sign
                    addTemplatePiece(new TemplatePiece.Text(
                            template.substring(lastOffs, offs + 1)));
                    lastOffs = offs += 2;
                    break;
                case '{': // "${...}" => variable
                    int closingCurly = template.indexOf('}', offs + 2);
                    if (closingCurly > -1) {
                        addTemplatePiece(new TemplatePiece.Text(
                                template.substring(lastOffs, offs)));
                        String varName = template.substring(offs + 2, closingCurly);
                        if (!"cursor".equals(varName) && isParamDefined(varName)) {
                            addTemplatePiece(new TemplatePiece.ParamCopy(varName));
                        } else {
                            addTemplatePiece(new TemplatePiece.Param(varName));
                        }
                        lastOffs = offs = closingCurly + 1;
                    }
                    break;
            }

        }

        if (lastOffs < template.length()) {
            String text = template.substring(lastOffs);
            addTemplatePiece(new TemplatePiece.Text(text));
        }

    }


    private int possiblyReplaceTabsWithSpaces(StringBuilder sb, String text,
                                              JTextComponent tc, int start) {

        int tab = text.indexOf('\t');
        if (tab > -1) {

            int startLen = sb.length();

            int size = 4;
            Document doc = tc.getDocument();
            if (doc != null) {
                Integer i = (Integer) doc.getProperty(PlainDocument.tabSizeAttribute);
                if (i != null) {
                    size = i.intValue();
                }
            }
            String tabStr = "";
            for (int i = 0; i < size; i++) {
                tabStr += " ";
            }

            int lastOffs = 0;
            do {
                sb.append(text.substring(lastOffs, tab));
                sb.append(tabStr);
                lastOffs = tab + 1;
            } while ((tab = text.indexOf('\t', lastOffs)) > -1);
            sb.append(text.substring(lastOffs));

            start += sb.length() - startLen;

        } else {
            sb.append(text);
            start += text.length();
        }

        return start;

    }


    @Override
    public String toString() {
        return getDefinitionString();
    }


}