/*
 * 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.apache.openmeetings;

/**
 * David Bismut, david.bismut@gmail.com
 * Intern, SETLabs, Infosys Technologies Ltd. May 2004 - Jul 2004
 * Ecole des Mines de Nantes, France
 */
import static org.apache.openmeetings.util.OpenmeetingsVariables.getWebAppRootKey;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.accessibility.AccessibleHypertext;
import javax.swing.JEditorPane;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import javax.swing.text.html.parser.ParserDelegator;

import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;

/**
 * A "HTML" JEditorPane embedded with a special HTMLDocument that detects urls
 * and displays them as hyperlinks. When CTRL is pressed, the urls are
 * clickable.
 *
 * @author David Bismut
 *
 */
public class EditorPaneLinkDetector extends JEditorPane {
	private static final long serialVersionUID = 1L;
	private static final Logger log = Red5LoggerFactory.getLogger(EditorPaneLinkDetector.class, getWebAppRootKey());

	/**
	 * Creates a <code>EditorPaneLinkDetector</code>.
	 */
	public EditorPaneLinkDetector() {

		HTMLEditorKit htmlkit = new HTMLEditorKit();

		StyleSheet styles = htmlkit.getStyleSheet();
		StyleSheet ss = new StyleSheet();

		ss.addStyleSheet(styles);

		ss.addRule("body {font-family:arial;font-size:12pt}");
		ss.addRule("p {font-family:arial;margin:2}");

		HTMLDocument doc = new HTMLDocLinkDetector(ss);

		setEditorKit(htmlkit);

		setDocument(doc);

		addMouseMotionListener(new MouseMotionAdapter() {
			@Override
			public void mouseMoved(MouseEvent e) {

				AccessibleJTextComponent context = (AccessibleJTextComponent) getAccessibleContext()
						.getAccessibleEditableText();

				AccessibleHypertext accText = (AccessibleHypertext) context
						.getAccessibleText();

				int index = accText.getIndexAtPoint(e.getPoint());

				int linkIndex = accText.getLinkIndex(index);
				if (linkIndex == -1) {
					setToolTipText(null);
					return;
				}

				String linkDesc = accText.getLink(linkIndex)
						.getAccessibleActionDescription(0);

				String toolTipText = "<html><body style='margin: 3'>"
						+ linkDesc
						+ "<br><b>CTRL + click to follow link</b></body></html>";
				setToolTipText(toolTipText);
			}
		});

		addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {

				if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
					if (isEditable()) {
						setEditable(false);
					}
				} else {
					if (!isEditable()) {
						setEditable(true);
					}
				}

			}

			@Override
			public void keyReleased(KeyEvent e) {

				if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
					setEditable(true);
				}

			}
		});
	}

	protected class HTMLDocLinkDetector extends HTMLDocument {
		private static final long serialVersionUID = 1L;

		public HTMLDocLinkDetector(StyleSheet ss) {
			super(ss);

			setAsynchronousLoadPriority(4);
			setTokenThreshold(100);
			setParser(new ParserDelegator());
		}

		/**
		 * Returns true if the Element contains a HTML.Tag.A attribute, false
		 * otherwise.
		 *
		 * @param e
		 *            the Element to be checkd
		 * @return
		 */
		protected boolean isLink(Element e) {

			return (e.getAttributes().getAttribute(HTML.Tag.A) != null);

		}

		/**
		 * This method corrects or creates a url contained in an Element as an
		 * hyperlink.
		 *
		 * @param e
		 *            the Element to be computed
		 * @throws BadLocationException
		 */
		protected void computeLinks(Element e) throws BadLocationException {

			int caretPos = getCaretPosition();
			try {
				if (isLink(e))
					correctLink(e);
				else
					createLink(e);
			} catch (IOException ex) {
				log.error("Error", ex);
			}
			setCaretPosition(Math.min(caretPos, getLength()));
		}

		/**
		 * The method corrects the url inside an Element, that is supposed to be
		 * an element containing a link only. This function is typically called
		 * when the url is beeing edited. What the function does is to remove
		 * the html tags, so the url is actually edited in plain text and not as
		 * an hyperlink.
		 *
		 * @param e
		 *            the Element that contains the url
		 * @throws BadLocationException
		 * @throws IOException
		 */
		protected void correctLink(Element e) throws BadLocationException,
				IOException {

			int length = e.getEndOffset() - e.getStartOffset();

			boolean endOfDoc = e.getEndOffset() == getLength() + 1;

			// to avoid catching the final '\n' of the document.
			if (endOfDoc)
				length--;

			String text = getText(e.getStartOffset(), length);

			setOuterHTML(e, text);

			// insert final spaces ignored by the html
			Matcher spaceMatcher = Pattern.compile("(\\s+)$").matcher(text);

			if (spaceMatcher.find()) {
				String endingSpaces = spaceMatcher.group(1);
				insertString(Math.min(getLength(), e.getEndOffset()),
						endingSpaces, null);
			}
		}

		/**
		 * The method check if the element contains a url in plain text, and if
		 * so, it creates the html tag HTML.Tag.A to have the url displayed as
		 * an hyperlink.
		 *
		 * @param e
		 *            element that contains the url
		 * @throws BadLocationException
		 * @throws IOException
		 */
		protected void createLink(Element e) throws BadLocationException,
				IOException {

			int caretPos = getCaretPosition();

			int startOffset = e.getStartOffset();
			int length = e.getEndOffset() - e.getStartOffset();

			boolean endOfDoc = e.getEndOffset() == getLength() + 1;
			// to avoid catching the final '\n' of the document.
			if (endOfDoc)
				length--;

			String text = getText(startOffset, length);

			Matcher matcher = Pattern.compile(
					"(?i)(\\b(http://|https://|www.|ftp://|file:/|mailto:)\\S+)(\\s+)")
					.matcher(text);

			if (matcher.find()) {
				String url = matcher.group(1);
				//String prefix = matcher.group(2);
				String endingSpaces = matcher.group(3);

				// to ignore characters after the caret
				int validPos = startOffset + matcher.start(3) + 1;
				if (validPos > caretPos)
					return;

				Matcher dotEndMatcher = Pattern.compile("([\\W&&[^/]]+)$")
						.matcher(url);

				//Ending non alpha characters like [.,?%] shouldn't be included
				// in the url.
				String endingDots = "";
				if (dotEndMatcher.find()) {
					endingDots = dotEndMatcher.group(1);
					url = dotEndMatcher.replaceFirst("");
				}

				text = matcher.replaceFirst("<a href='" + url + "'>" + url
						+ "</a>" + endingDots + endingSpaces);

				setOuterHTML(e, text);

				// insert initial spaces ignored by the html
				Matcher spaceMatcher = Pattern.compile("^(\\s+)").matcher(text);

				if (spaceMatcher.find()) {
					String initialSpaces = spaceMatcher.group(1);
					insertString(startOffset, initialSpaces, null);
				}

				// insert final spaces ignored by the html
				spaceMatcher = Pattern.compile("(\\s+)$").matcher(text);

				if (spaceMatcher.find()) {
					String extraSpaces = spaceMatcher.group(1);
					int endoffset = e.getEndOffset();
					if (extraSpaces.charAt(extraSpaces.length() - 1) == '\n') {
						extraSpaces = extraSpaces.substring(0, extraSpaces
								.length() - 1);
						endoffset--;
					}
					insertString(Math.min(getLength(), endoffset), extraSpaces,
							null);
				}
			}
		}

		@Override
		public void remove(int offs, int len) throws BadLocationException {

			super.remove(offs, len);
			Element e = getCharacterElement(offs - len);
			computeLinks(e);
		}

		@Override
		public void insertString(int offs, String str, AttributeSet a)
				throws BadLocationException {

			super.insertString(offs, str, a);
			Element e = getCharacterElement(offs);
			computeLinks(e);
		}
	}
}
