/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.xml.schema.completion;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.*;
import javax.swing.Icon;
import javax.xml.XMLConstants;
import org.netbeans.editor.BaseDocument;
import org.netbeans.api.editor.completion.Completion;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.xml.lexer.XMLTokenId;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.netbeans.spi.editor.completion.CompletionTask;
import org.netbeans.modules.xml.axi.AXIComponent;
import org.netbeans.modules.xml.schema.completion.spi.CompletionContext;
import org.netbeans.modules.xml.schema.completion.util.CompletionContextImpl;
import org.netbeans.modules.xml.schema.completion.util.CompletionUtil;
import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
import org.netbeans.swing.plaf.LFCustoms;

/**
 *
 * @author Samaresh (Samaresh.Panda@Sun.Com)
 */
public abstract class CompletionResultItem implements CompletionItem {
    private static final Logger _logger = Logger.getLogger(CompletionResultItem.class.getName());

    private static final Color COLOR = LFCustoms.shiftColor(new Color(64, 64, 255));
    
    public static final String
        ICON_ELEMENT    = "element.png",     //NOI18N
        ICON_ATTRIBUTE  = "attribute.png",   //NOI18N
        ICON_VALUE      = "value.png",       //NOI18N
        ICON_LOCATION   = "/org/netbeans/modules/xml/schema/completion/resources/"; //NOI18N

    protected boolean shift = false;
    protected String typedChars;
    protected String itemText;
    protected javax.swing.Icon icon;
    protected CompletionPaintComponent component;
    protected AXIComponent axiComponent;
    protected int extraPaintGap = CompletionPaintComponent.DEFAULT_ICON_WIDTH;
    protected TokenSequence tokenSequence;

    protected final CompletionContextImpl context;

    /**
     * Creates a new instance of CompletionUtil
     */
    public CompletionResultItem(AXIComponent component, CompletionContext context) {
        this(component, context, null);
    }

    public CompletionResultItem(AXIComponent component, CompletionContext context,
        TokenSequence tokenSequence) {
        this.context = (CompletionContextImpl) context;
        this.axiComponent = component;
        setTokenSequence(tokenSequence);
        if (context != null) {
            this.typedChars = context.getTypedChars();
        }
    }

    Icon getIcon(){
        return icon;
    }

    public AXIComponent getAXIComponent() {
        return axiComponent;
    }

    /**
     * The completion item's name.
     */
    public String getItemText() {
        return itemText;
    }

    /**
     * The text user sees in the CC list. Normally some additional info
     * such as cardinality etc. are added to the item's name.
     * 
     */
    public abstract String getDisplayText();

    /**
     * Replacement text is the one that gets inserted into the document when
     * user selects this item from the CC list.
     */
    public abstract String getReplacementText();

    /**
     * Returns the relative caret position.
     * The caller must call this w.r.t. the offset
     * e.g. component.setCaretPosition(offset + getCaretPosition())
     */
    public abstract int getCaretPosition();

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

    Color getPaintColor() { 
        return LFCustoms.shiftColor(COLOR);
    }

    public int getExtraPaintGap() {
        return extraPaintGap;
    }

    public void setExtraPaintGap(int extraPaintGap) {
        this.extraPaintGap = extraPaintGap;
    }

    public TokenSequence getTokenSequence() {
        return tokenSequence;
    }

    public void setTokenSequence(TokenSequence tokenSequence) {
        this.tokenSequence = tokenSequence;
    }
    
    protected int removeTextLength(JTextComponent component, int offset, int removeLength) {
        return removeLength;
    }
    
    protected int caretOffset() {
        return -1;
    }

    /**
     * Prepares values in the item, by inspecting the Component, token sequence etc.
     * The method is called before the text is changed, and is called under document write lock.
     * 
     * @param component the editing component
     * @param proposedText the proposed text
     * @param offset the offset where the completion occurs
     */
    protected void prepare(JTextComponent component, String proposedText, int offset) {
        
    }
    
    /**
     * Actually replaces a piece of document by passes text.
     * @param component a document source
     * @param text a string to be inserted
     * @param offset the target offset
     * @param len a length that should be removed before inserting text
     */
    protected void replaceText(final JTextComponent component, final String text,
        final int offset, final int len) {
        final BaseDocument doc = (BaseDocument) component.getDocument();
        doc.runAtomic(new Runnable() {
            @Override
            public void run() {
                try {
                    int caretPos = component.getCaretPosition();
                    if ((context != null) && (context.canReplace(text))) {
                        prepare(component, text, offset);
                        int l2 = removeTextLength(component, offset, len);
                        String insertingText = getInsertingText(component, offset, text, l2);
                        if (l2 > 0) doc.remove(offset, l2);
                        doc.insertString(offset, insertingText, null);
                        // fix for issue #186007
                        caretPos = offset + getCaretPosition();
                        caretPos -= text.length() - insertingText.length();
                    } else {
                        caretPos = offset + getCaretPosition(); // change the caret position
                    }
                    int docLength = doc.getLength();
                    if (docLength == 0) {
                        caretPos = 0;
                    } else if (caretPos > doc.getLength()) {
                        caretPos = doc.getLength();
                    }
                    component.setCaretPosition(caretPos);
                    
                    String prefix = CompletionUtil.getPrefixFromTag(text);
                    if (prefix == null) {
                        return;
                    }
                    //insert namespace declaration for the new prefix
                    if ((context != null) && (! context.isSpecialCompletion()) &&
                        (! context.isPrefixBeingUsed(prefix))) {
                        String tns = context.getTargetNamespaceByPrefix(prefix);
                        
                        // CC has made a suggestion, so materialize it:
                        if (tns == null) {
                            tns = context.getSuggestedNamespace().get(prefix);
                        }
                        
                        if (tns != null) {
                            doc.insertString(CompletionUtil.getNamespaceInsertionOffset(doc), " " +
                                    XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix + "=\"" +
                                    tns + "\"", null);
                        }
                    }
                } catch (Exception e) {
                    _logger.log(Level.SEVERE,
                        e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
                }
            }
        });
    }
    
    protected final TokenSequence createTokenSequence(JTextComponent component) {
        if (tokenSequence == null) {
            TokenHierarchy tokenHierarchy = TokenHierarchy.get(component.getDocument());
            this.tokenSequence = tokenHierarchy.tokenSequence();
        }
        return tokenSequence;
    }
    
    private String stripCommonPrefix(String prefix, String replacement, String original) {
        if (replacement.startsWith(prefix) && original.startsWith(prefix)) {
            return replacement.substring(prefix.length());
        } else {
            return replacement;
        }
    }
    
    private void resetTokenSequence() {
        tokenSequence = null;
    }
    
    protected String getInsertingText(JTextComponent component, int textPos, String primaryText, int removeLen) {
        if ((primaryText == null) || (primaryText.length() < 1)) {
            return primaryText;
        }
        createTokenSequence(component);
        if (tokenSequence.move(textPos) == 0) {
            tokenSequence.movePrevious();
        } else {
            tokenSequence.moveNext();
        }
        Token token = tokenSequence.token();
        boolean isTextTag = CompletionUtil.isTextTag(token);

        if (! (isTextTag || CompletionUtil.isEndTagPrefix(token) ||
            CompletionUtil.isTagFirstChar(token))) {
            return primaryText;
        }

        int tokenOffset = tokenSequence.offset();
        if (isTextTag) {
            String tokenText = token.text().toString();
            boolean isCaretAfterTag =
                (tokenText.startsWith(CompletionUtil.END_TAG_PREFIX) &&
                (textPos == tokenOffset + CompletionUtil.END_TAG_PREFIX.length()))
                ||
                (tokenText.startsWith(CompletionUtil.TAG_FIRST_CHAR) &&
                (textPos == tokenOffset + CompletionUtil.TAG_FIRST_CHAR.length()));
            if (! isCaretAfterTag) {
                return primaryText;
            }
        }
        
        String tokenText = token.text().toString();
        if (removeLen > 0) {
            // in the middle of the tag; must return text without starting / end tag
            primaryText = stripCommonPrefix(CompletionUtil.END_TAG_PREFIX, primaryText, tokenText);
            primaryText = stripCommonPrefix(CompletionUtil.TAG_FIRST_CHAR, primaryText, tokenText);
        }        
        if (primaryText.endsWith(CompletionUtil.TAG_LAST_CHAR)) {
            boolean endPresent = false;
            STOP: while (!endPresent && tokenSequence.moveNext()) {
                Token t = tokenSequence.token();
                switch ((XMLTokenId)t.id()) {
                    case WS:
                    case ARGUMENT:
                    case VALUE:
                    case OPERATOR:
                        break;
                    case TAG: {
                        String tt = t.text().toString();
                        if (tt.equals(CompletionUtil.TAG_LAST_CHAR) || tt.equals("/>")) {
                            endPresent = true;
                            break;
                        }
                    }
                    default:
                        break STOP;
                }
            }
            if (endPresent) {
                primaryText = primaryText.substring(0, primaryText.length() -1);
            }
        }

        if ((tokenOffset > -1) && (tokenOffset < textPos)) {
            textPos = tokenOffset;
        }

        boolean isDifferentTextFound = false;
        int i = 0;
        for (; i < primaryText.length(); ++i, ++textPos) {
            try {
                String strDoc  = component.getText(textPos, 1),
                       strText = primaryText.substring(i, i + 1);
                isDifferentTextFound = (! strDoc.equals(strText));
                if (isDifferentTextFound) break;
            } catch(BadLocationException e) {
                _logger.log(Level.WARNING,
                    e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
                isDifferentTextFound = true;
            }
        }
        String text = isDifferentTextFound ? primaryText.substring(Math.max(0, i - removeLen)) : "";
        return text;
    }

    ////////////////////////////////////////////////////////////////////////////////
    ///////////////////methods from CompletionItem interface////////////////////////
    ////////////////////////////////////////////////////////////////////////////////
    @Override
    public CompletionTask createDocumentationTask() {
        return new AsyncCompletionTask(new DocumentationQuery(this));
    }

    @Override
    public CompletionTask createToolTipTask() {
        return new AsyncCompletionTask(new ToolTipQuery(this));
    }

    @Override
    public void defaultAction(JTextComponent component) {
        String selectedText = component.getSelectedText();
        int charsToRemove = selectedText != null ? selectedText.length() :
                            (typedChars == null ? 0 : typedChars.length()),
            substOffset   = selectedText != null ? component.getSelectionStart() :
                            component.getCaretPosition() - charsToRemove;
        if(!shift) Completion.get().hideAll();
        if(getReplacementText().equals(typedChars))
            return;
        replaceText(component, getReplacementText(), substOffset, charsToRemove);
    }

    @Override
    public CharSequence getInsertPrefix() {
        return getItemText();
    }

    public abstract CompletionPaintComponent getPaintComponent();

    @Override
    public int getPreferredWidth(Graphics g, Font defaultFont) {
        CompletionPaintComponent renderComponent = getPaintComponent();
        return renderComponent.getPreferredSize().width;
    //return getPaintComponent().getWidth(getItemText(), defaultFont);
    }

    @Override
    public int getSortPriority() {
        return 0;
    }

    @Override
    public CharSequence getSortText() {
        return getItemText();
    }

    @Override
    public boolean instantSubstitution(JTextComponent component) {
        defaultAction(component);
        return true;
    }

    @Override
    public void processKeyEvent(KeyEvent e) {
        shift = (e.getKeyCode() == KeyEvent.VK_ENTER &&
                 e.getID() == KeyEvent.KEY_PRESSED && e.isShiftDown());
    }

    @Override
    public void render(Graphics g, Font defaultFont,
            Color defaultColor, Color backgroundColor,
            int width, int height, boolean selected) {
        CompletionPaintComponent renderComponent = getPaintComponent();
        renderComponent.setFont(defaultFont);
        renderComponent.setForeground(defaultColor);
        renderComponent.setBackground(backgroundColor);
        renderComponent.setBounds(0, 0, width, height);
        renderComponent.setSelected(selected);
        renderComponent.paintComponent(g);
    }
}