/*
Copyright 2008-2010 Gephi
Authors : Mathieu Bastian <mathieu.bastian@gephi.org>
Website : http://www.gephi.org

This file is part of Gephi.

DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.

Copyright 2011 Gephi Consortium. All rights reserved.

The contents of this file are subject to the terms of either the GNU
General Public License Version 3 only ("GPL") or the Common
Development and Distribution License("CDDL") (collectively, the
"License"). You may not use this file except in compliance with the
License. You can obtain a copy of the License at
http://gephi.org/about/legal/license-notice/
or /cddl-1.0.txt and /gpl-3.0.txt. See the License for the
specific language governing permissions and limitations under the
License.  When distributing the software, include this License Header
Notice in each file and include the License files at
/cddl-1.0.txt and /gpl-3.0.txt. If applicable, add the following below the
License Header, with the fields enclosed by brackets [] replaced by
your own identifying information:
"Portions Copyrighted [year] [name of copyright owner]"

If you wish your version of this file to be governed by only the CDDL
or only the GPL Version 3, indicate your decision by adding
"[Contributor] elects to include this software in this distribution
under the [CDDL or GPL Version 3] license." If you do not indicate a
single choice of license, a recipient has the option to distribute
your version of this file under either the CDDL, the GPL Version 3 or
to extend the choice of license to its licensees as provided above.
However, if you add GPL Version 3 code and therefore, elected the GPL
Version 3 license, then the option applies only if the new code is
made subject to such option by the copyright holder.

Contributor(s):

Portions Copyrighted 2011 Gephi Consortium.
*/
package org.gephi.ui.components;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.Hashtable;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLWriter;


//Copied from org.netbeans.lib.profiler.ui.components
//Autors Ian Formanek & Jiri Sedlacek
public class JHTMLEditorPane extends JEditorPane implements HyperlinkListener, MouseListener {
    //~ Inner Classes ------------------------------------------------------------------------------------------------------------

    /** Private Writer that extracts correctly formatted string from HTMLDocument */
    private class ExtendedHTMLWriter extends HTMLWriter {
        //~ Constructors ---------------------------------------------------------------------------------------------------------

        public ExtendedHTMLWriter(Writer w, HTMLDocument doc, int pos, int len) {
            super(w, doc, pos, len);
            setLineLength(Integer.MAX_VALUE);
        }

        //~ Methods --------------------------------------------------------------------------------------------------------------
        protected boolean isSupportedBreakFlowTag(AttributeSet attr) {
            Object o = attr.getAttribute(StyleConstants.NameAttribute);

            if (o instanceof HTML.Tag) {
                HTML.Tag tag = (HTML.Tag) o;

                if ((tag == HTML.Tag.HTML) || (tag == HTML.Tag.HEAD) || (tag == HTML.Tag.BODY) || (tag == HTML.Tag.HR)) {
                    return false;
                }

                return (tag).breaksFlow();
            }

            return false;
        }

        @Override
        protected void emptyTag(Element elem) throws BadLocationException, IOException {
            if (isSupportedBreakFlowTag(elem.getAttributes())) {
                writeLineSeparator();
            }

            if (matchNameAttribute(elem.getAttributes(), HTML.Tag.CONTENT)) {
                text(elem);
            }
        }

        @Override
        protected void endTag(Element elem) throws IOException {
            if (isSupportedBreakFlowTag(elem.getAttributes())) {
                writeLineSeparator();
            }
        }

        @Override
        protected void startTag(Element elem) throws IOException, BadLocationException {
        }
    }

    // --- Private classes for copy/paste support --------------------------------
    //
    // NOTE: only vertical formatting is correctly copy/pasted,
    //       horizontal formatting (ul, li) is ignored.
    /** Private TransferHandler that copies correctly formatted string from HTMLDocument to system clipboard */
    private class HTMLTextAreaTransferHandler extends TransferHandler {
        //~ Methods --------------------------------------------------------------------------------------------------------------

        @Override
        public void exportToClipboard(JComponent comp, Clipboard clip, int action) {
            try {
                int selStart = getSelectionStart();
                int selLength = getSelectionEnd() - selStart;

                StringWriter plainTextWriter = new StringWriter();

                try {
                    new ExtendedHTMLWriter(plainTextWriter, (HTMLDocument) getDocument(), selStart, selLength).write();
                } catch (Exception e) {
                }

                String plainText = NcrToUnicode.decode(plainTextWriter.toString());
                clip.setContents(new StringSelection(plainText), null);

                if (action == TransferHandler.MOVE) {
                    getDocument().remove(selStart, selLength);
                }
            } catch (BadLocationException ble) {
            }
        }
    }

    /** Class for decoding strings from NCR to Unicode */
    private static class NcrToUnicode {
        //~ Static fields/initializers -------------------------------------------------------------------------------------------

        private static Map<String, String> entities;

        //~ Methods --------------------------------------------------------------------------------------------------------------
        public static String decode(String str) {
            StringBuffer ostr = new StringBuffer();
            int i1 = 0;
            int i2 = 0;

            while (i2 < str.length()) {
                i1 = str.indexOf("&", i2); //NOI18N

                if (i1 == -1) {
                    ostr.append(str.substring(i2, str.length()));

                    break;
                }

                ostr.append(str.substring(i2, i1));
                i2 = str.indexOf(";", i1); //NOI18N

                if (i2 == -1) {
                    ostr.append(str.substring(i1, str.length()));

                    break;
                }

                String tok = str.substring(i1 + 1, i2);

                if (tok.charAt(0) == '#') { //NOI18N

                    if (tok.equals("#160")) { //NOI18N
                        ostr.append(getEntities().get("nbsp")); //NOI18N // Fixes Issue 92818, "&nbsp;" is resolved as "&#160;" before decoding, so redirecting back to "&nbsp;"
                    } else {
                        tok = tok.substring(1);

                        try {
                            int radix = 10;

                            if (tok.trim().charAt(0) == 'x') { //NOI18N
                                radix = 16;
                                tok = tok.substring(1, tok.length());
                            }

                            ostr.append((char) Integer.parseInt(tok, radix));
                        } catch (NumberFormatException exp) {
                            ostr.append('?'); //NOI18N
                        }
                    }
                } else {
                    tok = getEntities().get(tok);

                    if (tok != null) {
                        ostr.append(tok);
                    } else {
                        ostr.append('?'); //NOI18N
                    }
                }

                i2++;
            }

            return ostr.toString();
        }

        private static synchronized Map<String, String> getEntities() {
            if (entities == null) {
                entities = new Hashtable();
                //Quotation mark
                entities.put("quot", "\""); //NOI18N
                //Ampersand

                entities.put("amp", "\u0026"); //NOI18N
                //Less than

                entities.put("lt", "\u003C"); //NOI18N
                //Greater than

                entities.put("gt", "\u003E"); //NOI18N
                //Nonbreaking space

                entities.put("nbsp", "\u0020"); //NOI18N // Fixes Issue 92818, "\u00A0" (&nbsp; equivalent) is resolved as incorrect character, thus mapping to standard space
                //Inverted exclamation point

                entities.put("iexcl", "\u00A1"); //NOI18N
                //Cent sign

                entities.put("cent", "\u00A2"); //NOI18N
                //Pound sign

                entities.put("pound", "\u00A3"); //NOI18N
                //General currency sign

                entities.put("curren", "\u00A4"); //NOI18N
                //Yen sign

                entities.put("yen", "\u00A5"); //NOI18N
                //Broken vertical bar

                entities.put("brvbar", "\u00A6"); //NOI18N
                //Section sign

                entities.put("sect", "\u00A7"); //NOI18N
                //Umlaut

                entities.put("uml", "\u00A8"); //NOI18N
                //Copyright

                entities.put("copy", "\u00A9"); //NOI18N
                //Feminine ordinal

                entities.put("ordf", "\u00AA"); //NOI18N
                //Left angle quote

                entities.put("laquo", "\u00AB"); //NOI18N
                //Not sign

                entities.put("not", "\u00AC"); //NOI18N
                //Soft hyphen

                entities.put("shy", "\u00AD"); //NOI18N
                //Registered trademark

                entities.put("reg", "\u00AE"); //NOI18N
                //Macron accent

                entities.put("macr", "\u00AF"); //NOI18N
                //Degree sign

                entities.put("deg", "\u00B0"); //NOI18N
                //Plus or minus

                entities.put("plusmn", "\u00B1"); //NOI18N
                //Superscript 2

                entities.put("sup2", "\u00B2"); //NOI18N
                //Superscript 3

                entities.put("sup3", "\u00B3"); //NOI18N
                //Acute accent

                entities.put("acute", "\u00B4"); //NOI18N
                //Micro sign (Greek mu)

                entities.put("micro", "\u00B5"); //NOI18N
                //Paragraph sign

                entities.put("para", "\u00B6"); //NOI18N
                //Middle dot

                entities.put("middot", "\u00B7"); //NOI18N
                //Cedilla

                entities.put("cedil", "\u00B8"); //NOI18N
                //Superscript 1

                entities.put("sup1", "\u00B9"); //NOI18N
                //Masculine ordinal

                entities.put("ordm", "\u00BA"); //NOI18N
                //Right angle quote

                entities.put("raquo", "\u00BB"); //NOI18N
                //Fraction one-fourth

                entities.put("frac14", "\u00BC"); //NOI18N
                //Fraction one-half

                entities.put("frac12", "\u00BD"); //NOI18N
                //Fraction three-fourths

                entities.put("frac34", "\u00BE"); //NOI18N
                //Inverted question mark

                entities.put("iquest", "\u00BF"); //NOI18N
                //Capital A, grave accent

                entities.put("Agrave", "\u00C0"); //NOI18N
                //Capital A, acute accent

                entities.put("Aacute", "\u00C1"); //NOI18N
                //Capital A, circumflex accent

                entities.put("Acirc", "\u00C2"); //NOI18N
                //Capital A, tilde

                entities.put("Atilde", "\u00C3"); //NOI18N
                //Capital A, umlaut

                entities.put("Auml", "\u00C4"); //NOI18N
                //Capital A, ring

                entities.put("Aring", "\u00C5"); //NOI18N
                //Capital AE ligature

                entities.put("AElig", "\u00C6"); //NOI18N
                //Capital C, cedilla

                entities.put("Ccedil", "\u00C7"); //NOI18N
                //Capital E, grave accent

                entities.put("Egrave", "\u00C8"); //NOI18N
                //Capital E, acute accent

                entities.put("Eacute", "\u00C9"); //NOI18N
                //Capital E, circumflex accent

                entities.put("Ecirc", "\u00CA"); //NOI18N
                //Capital E, umlaut

                entities.put("Euml", "\u00CB"); //NOI18N
                //Capital I, grave accent

                entities.put("Igrave", "\u00CC"); //NOI18N
                //Capital I, acute accent

                entities.put("Iacute", "\u00CD"); //NOI18N
                //Capital I, circumflex accent

                entities.put("Icirc", "\u00CE"); //NOI18N
                //Capital I, umlaut

                entities.put("Iuml", "\u00CF"); //NOI18N
                //Capital eth, Icelandic

                entities.put("ETH", "\u00D0"); //NOI18N
                //Capital N, tilde

                entities.put("Ntilde", "\u00D1"); //NOI18N
                //Capital O, grave accent

                entities.put("Ograve", "\u00D2"); //NOI18N
                //Capital O, acute accent

                entities.put("Oacute", "\u00D3"); //NOI18N
                //Capital O, circumflex accent

                entities.put("Ocirc", "\u00D4"); //NOI18N
                //Capital O, tilde

                entities.put("Otilde", "\u00D5"); //NOI18N
                //Capital O, umlaut

                entities.put("Ouml", "\u00D6"); //NOI18N
                //Multiply sign

                entities.put("times", "\u00D7"); //NOI18N
                //Capital O, slash

                entities.put("Oslash", "\u00D8"); //NOI18N
                //Capital U, grave accent

                entities.put("Ugrave", "\u00D9"); //NOI18N
                //Capital U, acute accent

                entities.put("Uacute", "\u00DA"); //NOI18N
                //Capital U, circumflex accent

                entities.put("Ucirc", "\u00DB"); //NOI18N
                //Capital U, umlaut

                entities.put("Uuml", "\u00DC"); //NOI18N
                //Capital Y, acute accent

                entities.put("Yacute", "\u00DD"); //NOI18N
                //Capital thorn, Icelandic

                entities.put("THORN", "\u00DE"); //NOI18N
                //Small sz ligature, German

                entities.put("szlig", "\u00DF"); //NOI18N
                //Small a, grave accent

                entities.put("agrave", "\u00E0"); //NOI18N
                //Small a, acute accent

                entities.put("aacute", "\u00E1"); //NOI18N
                //Small a, circumflex accent

                entities.put("acirc", "\u00E2"); //NOI18N
                //Small a, tilde

                entities.put("atilde", "\u00E3"); //NOI18N
                //Small a, umlaut

                entities.put("auml", "\u00E4"); //NOI18N
                //Small a, ring

                entities.put("aring", "\u00E5"); //NOI18N
                //Small ae ligature

                entities.put("aelig", "\u00E6"); //NOI18N
                //Small c, cedilla

                entities.put("ccedil", "\u00E7"); //NOI18N
                //Small e, grave accent

                entities.put("egrave", "\u00E8"); //NOI18N
                //Small e, acute accent

                entities.put("eacute", "\u00E9"); //NOI18N
                //Small e, circumflex accent

                entities.put("ecirc", "\u00EA"); //NOI18N
                //Small e, umlaut

                entities.put("euml", "\u00EB"); //NOI18N
                //Small i, grave accent

                entities.put("igrave", "\u00EC"); //NOI18N
                //Small i, acute accent

                entities.put("iacute", "\u00ED"); //NOI18N
                //Small i, circumflex accent

                entities.put("icirc", "\u00EE"); //NOI18N
                //Small i, umlaut

                entities.put("iuml", "\u00EF"); //NOI18N
                //Small eth, Icelandic

                entities.put("eth", "\u00F0"); //NOI18N
                //Small n, tilde

                entities.put("ntilde", "\u00F1"); //NOI18N
                //Small o, grave accent

                entities.put("ograve", "\u00F2"); //NOI18N
                //Small o, acute accent

                entities.put("oacute", "\u00F3"); //NOI18N
                //Small o, circumflex accent

                entities.put("ocirc", "\u00F4"); //NOI18N
                //Small o, tilde

                entities.put("otilde", "\u00F5"); //NOI18N
                //Small o, umlaut

                entities.put("ouml", "\u00F6"); //NOI18N
                //Division sign

                entities.put("divide", "\u00F7"); //NOI18N
                //Small o, slash

                entities.put("oslash", "\u00F8"); //NOI18N
                //Small u, grave accent

                entities.put("ugrave", "\u00F9"); //NOI18N
                //Small u, acute accent

                entities.put("uacute", "\u00FA"); //NOI18N
                //Small u, circumflex accent

                entities.put("ucirc", "\u00FB"); //NOI18N
                //Small u, umlaut

                entities.put("uuml", "\u00FC"); //NOI18N
                //Small y, acute accent

                entities.put("yacute", "\u00FD"); //NOI18N
                //Small thorn, Icelandic

                entities.put("thorn", "\u00FE"); //NOI18N
                //Small y, umlaut

                entities.put("yuml", "\u00FF"); //NOI18N
            }

            return entities;
        }
    }

    //~ Static fields/initializers -----------------------------------------------------------------------------------------------

    // -----
    // I18N String constants
    private static final ResourceBundle messages = ResourceBundle.getBundle("org.gephi.ui.components.Bundle"); // NOI18N
    private static final String CUT_STRING = messages.getString("HTMLTextArea_CutString"); // NOI18N
    private static final String COPY_STRING = messages.getString("HTMLTextArea_CopyString"); // NOI18N
    private static final String PASTE_STRING = messages.getString("HTMLTextArea_PasteString"); // NOI18N
    private static final String DELETE_STRING = messages.getString("HTMLTextArea_DeleteString"); // NOI18N
    private static final String SELECT_ALL_STRING = messages.getString("HTMLTextArea_SelectAllString"); // NOI18N
    // -----

    //~ Instance fields ----------------------------------------------------------------------------------------------------------
    private ActionListener popupListener;
    private JMenuItem itemCopy;
    private JMenuItem itemCut;
    private JMenuItem itemDelete;
    private JMenuItem itemPaste;
    private JMenuItem itemSelectAll;

    // --- Popup menu support ----------------------------------------------------
    private JPopupMenu popupMenu;
    private String originalText;
    private boolean showPopup = true;

    //~ Constructors -------------------------------------------------------------------------------------------------------------
    public JHTMLEditorPane() {
        super();
        setEditorKit(new HTMLEditorKit());
        setEditable(false);
        setOpaque(true);
        setAutoscrolls(true);
        addHyperlinkListener(this);
        setTransferHandler(new HTMLTextAreaTransferHandler());
        setFont(UIManager.getFont("Label.font")); //NOI18N
        addMouseListener(this);
    }

    public JHTMLEditorPane(String text) {
        this();
        setText(text);
    }

    //~ Methods ------------------------------------------------------------------------------------------------------------------
    @Override
    public void setForeground(Color color) {
        super.setForeground(color);
        setText(originalText);
    }

    public void setShowPopup(boolean showPopup) {
        this.showPopup = showPopup;
    }

    public boolean getShowPopup() {
        return showPopup;
    }

    @Override
    public void setText(String value) {
        if (value == null) {
            return;
        }

        originalText = value;

        Font font = getFont();
        Color textColor = getForeground();
        value = value.replaceAll("\\n\\r|\\r\\n|\\n|\\r", "<br>"); //NOI18N
        value = value.replaceAll("<code>", "<code style=\"font-size: " + font.getSize() + "pt;\">"); //NOI18N

        String colorText = "rgb(" + textColor.getRed() + "," + textColor.getGreen() + "," + textColor.getBlue() + ")"; //NOI18N
        super.setText("<html><body text=\"" + colorText + "\" style=\"font-size: " + font.getSize() + "pt; font-family: " + font.getName() + ";\">" + value + "</body></html>"); //NOI18N
    }

    public void deleteSelection() {
        try {
            getDocument().remove(getSelectionStart(), getSelectionEnd() - getSelectionStart());
        } catch (Exception ex) {
        }

    }

    @Override
    public void hyperlinkUpdate(HyperlinkEvent e) {
        if (!isEnabled()) {
            return;
        }

        if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
            showURL(e.getURL());
        } else if (e.getEventType() == HyperlinkEvent.EventType.ENTERED) {
            setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        } else if (e.getEventType() == HyperlinkEvent.EventType.EXITED) {
            setCursor(Cursor.getDefaultCursor());
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getModifiers() == InputEvent.BUTTON3_MASK) {
            if (isEnabled() && isFocusable() && showPopup) {
                JPopupMenu popup = getPopupMenu();

                if (popup != null) {
                    updatePopupMenu();

                    if (!hasFocus()) {
                        requestFocus(); // required for Select All functionality
                    }

                    popup.show(this, e.getX(), e.getY());
                }
            }
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

    @Override
    public void mousePressed(MouseEvent e) {
    }

    @Override
    public void mouseReleased(MouseEvent e) {
    }

    @Override
    public void paste() {
        try {
            replaceSelection(Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this).getTransferData(DataFlavor.stringFlavor).toString());
        } catch (Exception ex) {
        }
    }

    protected JPopupMenu getPopupMenu() {
        if (popupMenu == null) {
            popupMenu = createPopupMenu();
        }

        return popupMenu;
    }

    protected JPopupMenu createPopupMenu() {
        JPopupMenu popup = new JPopupMenu();

        popupListener = createPopupListener();

        itemCut = new JMenuItem(CUT_STRING);
        itemCopy = new JMenuItem(COPY_STRING);
        itemPaste = new JMenuItem(PASTE_STRING);
        itemDelete = new JMenuItem(DELETE_STRING);
        itemSelectAll = new JMenuItem(SELECT_ALL_STRING);

        itemCut.addActionListener(popupListener);
        itemCopy.addActionListener(popupListener);
        itemPaste.addActionListener(popupListener);
        itemDelete.addActionListener(popupListener);
        itemSelectAll.addActionListener(popupListener);

        popup.add(itemCut);
        popup.add(itemCopy);
        popup.add(itemPaste);
        popup.add(itemDelete);
        popup.addSeparator();
        popup.add(itemSelectAll);

        return popup;
    }

    protected void showURL(URL url) {
        // override to react to URL clicks
    }

    protected void updatePopupMenu() {
        // Cut
        itemCut.setEnabled(isEditable() && (getSelectedText() != null));

        // Copy
        itemCopy.setEnabled(getSelectedText() != null);

        // Paste
        try {
            Transferable clipboardContent = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this);
            itemPaste.setEnabled(isEditable() && (clipboardContent != null) && clipboardContent.isDataFlavorSupported(DataFlavor.stringFlavor));
        } catch (Exception e) {
            itemPaste.setEnabled(false);
        }

        // Delete
        if (isEditable()) {
            itemDelete.setVisible(true);
            itemDelete.setEnabled(getSelectedText() != null);
        } else {
            itemDelete.setVisible(false);
        }

    // Select All
    // always visible and enabled...
    }

    private ActionListener createPopupListener() {
        return new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (e.getSource() == itemCut) {
                    cut();
                } else if (e.getSource() == itemCopy) {
                    copy();
                } else if (e.getSource() == itemPaste) {
                    paste();
                } else if (e.getSource() == itemDelete) {
                    deleteSelection();
                } else if (e.getSource() == itemSelectAll) {
                    selectAll();
                }
            }
        };
    }
}
