package de.duehl.swing.ui.text.html;

/*
 * Copyright 2021 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.IOException;
import java.net.URL;

import javax.swing.JEditorPane;
import javax.swing.JViewport;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;

import de.duehl.basics.text.Text;
import de.duehl.swing.ui.GuiTools;
import de.duehl.swing.ui.colors.Colorizer;
import de.duehl.swing.ui.text.TextualComponent;

/**
 * Diese Klasse zeigt HTML an.
 *
 * Bei direkter Verwendung bitte unbedingt prüfen, ob nicht der HtmlPanel, HtmlCardPanel oder gar
 * HtmlAndSourceCodePanel die bessere Wahl wäre.
 *
 * @version 1.01     2021-12-09
 * @author Christian Dühl
 */

class HtmlComponent implements TextualComponent {

    /** EditorPane zur Darstellung des HTML. */
    private final JEditorPane htmlComponent;

    /**
     * Konstruktor.
     *
     * Bei direkter Verwendung bitte unbedingt prüfen, ob nicht der HtmlPanel die bessere Wahl
     * wäre.
     */
    public HtmlComponent() {
        htmlComponent = new JEditorPane();

        initComponent();
    }

    private void initComponent() {
        htmlComponent.setContentType("text/html");
        //html.setFocusable(false);       // Verhindert Markieren und Editieren.
        htmlComponent.setEditable(false); // Verhindert nur das Editieren, man kann aber Text
                                          // markieren und per Copy und Paste entnehmen!
    }

    /** Zeigt den Cursor an (rot und nicht blinkend). */
    public void showCursor() {
        htmlComponent.setCaretColor(Color.RED);
        htmlComponent.getCaret().setVisible(true);
        htmlComponent.getCaret().setBlinkRate(0);

        /*
         * Der Befehl
         *     htmlComponent.getCaret().setVisible(true);
         * zeigt den Cursor an, allerdings beenden sich die Programme dann nicht mehr,
         * wohl wegen des flasher's in DefaultCaret, obwohl das bei setEditable(true) klappt.
         *
         * Setzt man die Blinkrate auf 0, wird der flasher in DefaultCaret angehalten.
         * Daher beenden sich dann auch die Programme wieder.
         *
         *
         * Will man den Cursor blinkend, so müsste man am Ende entweder die htmlComponent
         * editierbar machen oder die Bilk-Rate auf 0 setzen.
         *
         * Das wäre aber mit deutlich mehr Aufwand in allen verwendenden Programmen verbunden,
         * daher belasse ich das nun einfach mal so.
         * Man müsste eine Methode hier schreiben und durch die Kette der Html-Klassen schleifen,
         * sowie beim quit() im Aufrufenden Programm die Methode aufrufen.
         */
    }

    /** Setzt den anzuzeigenden Text. */
    @Override
    public void setText(String html) {
        htmlComponent.setText(html);
    }

    /**
     * Schreibt den Text der Seite hinter der URL in den Hauptteil des Dialogs und setzt den
     * Textcursor auf den Anfang des Dokuments.
     *
     * @param url
     *            URL, aus der der Inhalt gelesen wird.
     */
    public void showHtml(URL url) {
        showHtmlWithoutSettingCaretPosition(url);
        htmlComponent.setCaretPosition(0); // hier wird das wirklich gebraucht.
    }

    /**
     * Schreibt den Text in den Hauptteil des Dialogs und setzt den Textcursor NICHT auf den Anfang
     * des Dokuments. Ansonsten funktionieren Links auf Abschnitte im gleichen Dokument nicht,
     * er springt kurz darunter, dann wieder an den Anfang zurück!
     *
     * @param url
     *            Datei, aus der der Inhalt gelesen wird.
     */
    private void showHtmlWithoutSettingCaretPosition(URL url) {
        try {
            tryToShowHtmlWithoutSettingCaretPosition(url);
        }
        catch (IOException exception) {
            // e.printStackTrace();
            throw new RuntimeException("Fehler beim öffnen der URL '" + url.toString() + "'.",
                    exception);
        }
    }

    /**
     * Schreibt den Text in den Anzeigebereich.
     *
     * @param url
     *            URL aus der der Inhalt gelesen wird.
     * @throws IOException Wenn es beim Einlesen der URL Probleme gibt.
     */
    private void tryToShowHtmlWithoutSettingCaretPosition(URL url) throws IOException {
        htmlComponent.setPage(url);
        htmlComponent.addHyperlinkListener(createHyperLinkListener());
    }

    /**
     * Erstellt einen HyperlinkListener, so dass man im HTML Links anklicken kann.
     *
     * Siehe http://www.java2s.com/Code/Java/Swing-JFC/DemonstratingtheHyperlinkListener.htm.
     */
    private HyperlinkListener createHyperLinkListener() {
        return new HyperlinkListener() {
            @Override
            public void hyperlinkUpdate(HyperlinkEvent event) {
                HyperlinkEvent.EventType typ = event.getEventType();

                if (typ == HyperlinkEvent.EventType.ACTIVATED) {
                    reactOnClickedLink(event.getURL());
                }
            }
        };
    }

    /** Reagiert auf den Klick auf einen Link. */
    private void reactOnClickedLink(URL url) {
        URL shownUrl = htmlComponent.getPage();
        if (null == shownUrl) {
            showHtml(url);
        }
        else {
            /*
             * Da man auch von bla#Teil1 zu bla#Teil2 ohne Setzen der Caret-Position auskommen
             * möchte, werden vor dem Vergleich der URLs die Teile ab # abgeschnitten:
             */
            String urlString = Text.skipSublinkPart(url.toString());
            String shownUrlString = Text.skipSublinkPart(shownUrl.toString());
            if (urlString.equals(shownUrlString)) {
                //System.out.println("Zeige ohne SettingCaretPosition:\n\t"
                //        + "alte URL: " + shownUrl + "\n\t"
                //        + "neue URL: " + url);
                showHtmlWithoutSettingCaretPosition(url);
            }
            else {
                //System.out.println("Zeige mit SettingCaretPosition:\n\t"
                //        + "alte URL: " + shownUrl + "\n\t"
                //        + "neue URL: " + url);
                showHtml(url);
            }
        }
    }

    /**
     * Turns on or off automatic drag handling. In order to enable automatic drag handling, this
     * property should be set to {@code true}, and the component's {@code TransferHandler} needs to
     * be {@code non-null}. The default value of the {@code dragEnabled} property is {@code false}.
     * <p>
     * The job of honoring this property, and recognizing a user drag gesture, lies with the look
     * and feel implementation, and in particular, the component's {@code TextUI}. When automatic
     * drag handling is enabled, most look and feels (including those that subclass
     * {@code BasicLookAndFeel}) begin a drag and drop operation whenever the user presses the
     * mouse button over a selection and then moves the mouse a few pixels. Setting this property
     * to {@code true} can therefore have a subtle effect on how selections behave.
     * <p>
     * If a look and feel is used that ignores this property, you can still begin a drag and drop
     * operation by calling {@code exportAsDrag} on the component's {@code TransferHandler}.
     *
     * @param enable
     *            whether or not to enable automatic drag handling
     */
    public void setDragEnabled(boolean enable) {
        htmlComponent.setDragEnabled(enable);
    }

    /**
     * Returns the position of the text insertion caret for the text component.
     *
     * @return the position of the text insertion caret for the text component &ge; 0
     */
    public int getCaretPosition() {
        return htmlComponent.getCaretPosition();
    }

    /**
     * Sets the position of the text insertion caret for the <code>TextComponent</code>. Note that
     * the caret tracks change, so this may move if the underlying text of the component is
     * changed. If the document is <code>null</code>, does nothing. The position must be between 0
     * and the length of the component's text or else an exception is thrown.
     *
     * @param position
     *            the position
     * @exception IllegalArgumentException
     *                if the value supplied for <code>position</code> is less than zero or greater
     *                than the component's text length
     */
    @Override
    public void setCaretPosition(int position) {
        htmlComponent.setCaretPosition(position);
    }

    /**
     * Returns the <code>Component</code>'s "visible rectangle" - the intersection of this
     * component's visible rectangle, <code>new Rectangle(0, 0, getWidth(), getHeight())</code>,
     * and all of its ancestors' visible rectangles.
     *
     * @return the visible rectangle
     */
    public Rectangle getVisibleRect() {
        return htmlComponent.getVisibleRect();
    }

    /**
     * Forwards the <code>scrollRectToVisible()</code> message to the <code>JComponent</code>'s
     * parent. Components that can service the request, such as <code>JViewport</code>, override
     * this method and perform the scrolling.
     *
     * @param rectangle
     *            the visible <code>Rectangle</code>
     * @see JViewport
     */
    public void scrollRectToVisible(Rectangle rectangle) {
        htmlComponent.scrollRectToVisible(rectangle);
    }

    /**
     * Returns the selected text's start position. Return 0 for an empty document, or the value of
     * dot if no selection.
     *
     * @return the start position &ge; 0
     */
    public int getSelectionStart() {
        return htmlComponent.getSelectionStart();
    }

    /**
     * Returns the selected text's end position. Return 0 if the document is empty, or the value of
     * dot if there is no selection.
     *
     * @return the end position &ge; 0
     */
    public int getSelectionEnd() {
        return htmlComponent.getSelectionEnd();
    }

    /**
     * Sets the selection start to the specified position. The new starting point is constrained to
     * be before or at the current selection end. <p>
     *
     * This is available for backward compatibility to code that called this method on
     * <code>java.awt.TextComponent</code>. This is implemented to forward to the
     * <code>Caret</code> implementation which is where the actual selection is maintained.
     *
     * @param start
     *            the start position of the text &ge; 0
     */
    public void setSelectionStart(int start) {
        htmlComponent.setSelectionStart(start);
    }

    /**
     * Sets the selection end to the specified position. The new end point is constrained to be at
     * or after the current selection start. <p>
     *
     * This is available for backward compatibility to code that called this method on
     * <code>java.awt.TextComponent</code>. This is implemented to forward to the
     * <code>Caret</code> implementation which is where the actual selection is maintained.
     *
     * @param end
     *            the end position of the text &ge; 0
     */
    public void setSelectionEnd(int end) {
        htmlComponent.setSelectionEnd(end);
    }

    /**
     * Gibt den in der text-Komponente selektierten Text zurück.
     *
     * Falls dieser nicht ermittelt werden kann, wird der leere String zurückgegeben.
     */
    public String getSelectedText() {
        try {
            String selectedText = htmlComponent.getSelectedText();
            if (null == selectedText) {
                return "";
            }
            else {
                return selectedText;
            }
        }
        catch (Throwable throwable) {
            return "";
        }
    }

    /** Löscht die Selektion. */
    public void clearSelection() {
        htmlComponent.select(0,  0);
    }

    /** Gibt die Länge des Textes zurück. */
    @Override
    public int getTextLength() {
        return htmlComponent.getText().length();
    }

    /** Zeichnet den Editor neu. */
    @Override
    public void repaint() {
        htmlComponent.repaint();
        htmlComponent.validate();
    }

    /** Setzt die gewünschte Größe des Text-Elements. */
    @Override
    public void setPreferredSize(int width, int height) {
        htmlComponent.setPreferredSize(new Dimension(width, height));
    }

    /** Färbt die übergebene Komponente ein, falls ein Colorizer übergeben wurde. */
    @Override
    public void setColors(Colorizer colorizer) {
        if (colorizer != null) {
            colorizer.setColors(htmlComponent);
        }
    }

    /** Gibt die in die Oberfläche einzubauende Komponente zurück. */
    @Override
    public Component getComponent() {
        return htmlComponent;
    }

    /** Gibt an, ob das Element den Fokus hat. */
    public boolean hasFokus() {
        return htmlComponent.hasFocus();
    }

    /** Setzt die Hintergrundfarben für die Darstellung. */
    public void setBackground(Color backgroundColor) {
        htmlComponent.setOpaque(true);
        htmlComponent.setBackground(backgroundColor);
    }

    /**
     * Lässt die Tastenkombinationen Page-Up und Page-Down an die übergeordnete Komponente
     * weiterreichen.
     */
    public void ignorePageUpAndPageDown() {
        GuiTools.ignorePageUpAndPageDownInComponent(htmlComponent);
    }

    /**
     * Lässt die Tastenkombinationen Pfeiltaste nach oben und Pfeiltaste nach oben in dem Feld für
     * den Namen an die übergeordnete Komponente weiterreichen.
     */
    public void ignoreUpAndDown() {
        GuiTools.ignoreUpAndDownInComponent(htmlComponent);
    }

    /**
     * Lässt die Tastenkombinationen Ctrl-Pos1 und Ctrl-End in dem Feld für den Namen an die
     * übergeordnete Komponente weiterreichen.
     */
    public void ignoreCtrlPos1AndCtrlEnd() {
        GuiTools.ignoreCtrlPos1AndCtrlEndInComponent(htmlComponent);
    }

    /** Gibt den HTML-Text zurück. */
    public String getHtmlText() {
        return htmlComponent.getText();
    }

}
