/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

package org.apache.batik.apps.svgbrowser;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;

import org.apache.batik.gvt.GVTTreeWalker;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.TextNode;
import org.apache.batik.gvt.text.Mark;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.util.gui.ExtendedGridBagConstraints;
import org.apache.batik.util.gui.resource.ActionMap;
import org.apache.batik.util.gui.resource.ButtonFactory;
import org.apache.batik.util.gui.resource.MissingListenerException;
import org.apache.batik.util.gui.resource.ResourceManager;

/**
 * This class represents a Dialog that lets the user searching for text inside
 * an SVG document.
 *
 * @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
 * @version $Id: FindDialog.java,v 1.8 2003/04/11 13:54:35 vhardy Exp $
 */
public class FindDialog extends JDialog implements ActionMap {

    /**
     * The resource file name
     */
    protected final static String RESOURCES =
        "org.apache.batik.apps.svgbrowser.resources.FindDialog";

    // action names
    public final static String FIND_ACTION = "FindButtonAction";

    public final static String CLEAR_ACTION = "ClearButtonAction";

    public final static String CLOSE_ACTION = "CloseButtonAction";

    /**
     * The resource bundle
     */
    protected static ResourceBundle bundle;

    /**
     * The resource manager
     */
    protected static ResourceManager resources;

    static {
        bundle = ResourceBundle.getBundle(RESOURCES, Locale.getDefault());
        resources = new ResourceManager(bundle);
    }

    /** The button factory */
    protected ButtonFactory buttonFactory;

    /** The GVT root into which text is searched. */
    protected GraphicsNode gvtRoot;

    /** The GVTTreeWalker used to scan the GVT Tree. */
    protected GVTTreeWalker walker;

    /** The current index in the TextNode's string. */
    protected int currentIndex;

    /** The TextField that owns the text to search. */
    protected JTextField search;

    /** The next button. */
    protected JButton findButton;

    /** The next button. */
    protected JButton clearButton;

    /** The cancel button. */
    protected JButton closeButton;

    /** The case sensitive button. */
    protected JCheckBox caseSensitive;

    /** The canvas. */
    protected JSVGCanvas svgCanvas;

    /** The highlight button. */
    protected JRadioButton highlightButton;

    /** The highlight and center button. */
    protected JRadioButton highlightCenterButton;

    /** The highlight center and zoom button. */
    protected JRadioButton highlightCenterZoomButton;
    /**
     * Constructs a new <tt>FindDialog</tt>.
     */
    public FindDialog(JSVGCanvas svgCanvas) {
        this(null, svgCanvas);
    }

    /**
     * Constructs a new <tt>FindDialog</tt>.
     */
    public FindDialog(Frame owner, JSVGCanvas svgCanvas) {
        super(owner, resources.getString("Dialog.title"));
        this.svgCanvas = svgCanvas;

        buttonFactory = new ButtonFactory(bundle, this);

        listeners.put(FIND_ACTION, 
		      new FindButtonAction());

        listeners.put(CLEAR_ACTION, 
		      new ClearButtonAction());

        listeners.put(CLOSE_ACTION, 
		      new CloseButtonAction());

	JPanel p = new JPanel(new BorderLayout());
	p.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
	p.add(createFindPanel(), BorderLayout.CENTER);
	p.add(createShowResultPanel(), BorderLayout.SOUTH);

	getContentPane().add(p, BorderLayout.CENTER);
        getContentPane().add(createButtonsPanel(), BorderLayout.SOUTH);
    }

    /**
     * Creates the Find panel.
     */
    protected JPanel createFindPanel() {
        JPanel panel = new JPanel(new GridBagLayout());

        panel.setBorder(BorderFactory.createTitledBorder
                        (BorderFactory.createEtchedBorder(),
                         resources.getString("Panel.title")));

        ExtendedGridBagConstraints gbc = new ExtendedGridBagConstraints();
        gbc.insets = new Insets(2, 2, 2, 2);

        gbc.anchor = ExtendedGridBagConstraints.EAST;
        gbc.fill = ExtendedGridBagConstraints.NONE;
        gbc.setWeight(0, 0);
        gbc.setGridBounds(0, 0, 1, 1);
        panel.add(new JLabel(resources.getString("FindLabel.text")), gbc);

        gbc.fill = ExtendedGridBagConstraints.HORIZONTAL;
        gbc.setWeight(1.0, 0);
        gbc.setGridBounds(1, 0, 2, 1);
        panel.add(search = new JTextField(20), gbc);

        gbc.fill = ExtendedGridBagConstraints.NONE;
        gbc.anchor = ExtendedGridBagConstraints.WEST;
        gbc.setWeight(0, 0);
        gbc.setGridBounds(1, 1, 1, 1);
        caseSensitive = buttonFactory.createJCheckBox("CaseSensitiveCheckBox");
        panel.add(caseSensitive, gbc);

        return panel;
    }

    protected JPanel createShowResultPanel() {
        JPanel panel = new JPanel(new GridBagLayout());

        panel.setBorder(BorderFactory.createTitledBorder
                        (BorderFactory.createEtchedBorder(),
                         resources.getString("ShowResultPanel.title")));

        ExtendedGridBagConstraints gbc = new ExtendedGridBagConstraints();
        gbc.insets = new Insets(2, 2, 2, 2);

        gbc.anchor = ExtendedGridBagConstraints.WEST;
        gbc.fill = ExtendedGridBagConstraints.NONE;
        gbc.setWeight(0, 0);

	ButtonGroup grp = new ButtonGroup();

	highlightButton = buttonFactory.createJRadioButton("Highlight");
	highlightButton.setSelected(true);
	grp.add(highlightButton);
        gbc.setGridBounds(0, 0, 1, 1);
        panel.add(highlightButton, gbc);

	highlightCenterButton = 
	    buttonFactory.createJRadioButton("HighlightAndCenter");
	grp.add(highlightCenterButton);
        gbc.setGridBounds(0, 1, 1, 1);
        panel.add(highlightCenterButton, gbc);

	highlightCenterZoomButton = 
	    buttonFactory.createJRadioButton("HighlightCenterAndZoom");
	grp.add(highlightCenterZoomButton);
        gbc.setGridBounds(0, 2, 1, 1);
        panel.add(highlightCenterZoomButton, gbc);

	return panel;
    }

    /**
     * Creates the buttons panel
     */
    protected JPanel createButtonsPanel() {
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        panel.add(findButton = buttonFactory.createJButton("FindButton"));
        panel.add(clearButton = buttonFactory.createJButton("ClearButton"));
        panel.add(closeButton = buttonFactory.createJButton("CloseButton"));
        return panel;
    }


    /**
     * Sets the graphics node into which text is searched.
     * @param gvtRoot the GVT root node
     */
    public void setGraphicsNode(GraphicsNode gvtRoot) {
        this.gvtRoot = gvtRoot;
        if (gvtRoot != null) {
            this.walker = new GVTTreeWalker(gvtRoot);
        } else {
            this.walker = null;
        }
    }

    /**
     * Returns the next GraphicsNode that matches the specified string or null
     * if any.
     *
     * @param text the text to match
     */
    protected GraphicsNode getNext(String text) {
	if (walker == null && gvtRoot != null) {
	    walker = new GVTTreeWalker(gvtRoot);
	}
        GraphicsNode gn = walker.getCurrentGraphicsNode();
	int index = match(gn, text, currentIndex+text.length());
	if (index >= 0) {
	    currentIndex = index;
	} else {
	    currentIndex = 0;
	    gn = walker.nextGraphicsNode();
	    while (gn != null && 
		   ((currentIndex = match(gn, text, currentIndex)) < 0)) {
		currentIndex = 0;
		gn = walker.nextGraphicsNode();
	    }
	}
        return gn;
    }

    /**
     * Returns the index inside the specified TextNode of the
     * specified text, or -1 if not found.
     *
     * @param node the graphics node to check
     * @param text the text use to match 
     * @param index the index from which to start */
    protected int match(GraphicsNode node, String text, int index) {
        if (!(node instanceof TextNode)
            || !node.isVisible()
            || text == null || text.length() == 0) {
            return -1;
        }
        String s = ((TextNode)node).getText();
        if (!caseSensitive.isSelected()) {
            s = s.toLowerCase();
            text = text.toLowerCase();
        }
        return s.indexOf(text, index);
    }

    /**
     * Shows the current selected <tt>TextNode</tt>.
     */
    protected void showSelectedGraphicsNode() {
        GraphicsNode gn = walker.getCurrentGraphicsNode();
        if (!(gn instanceof TextNode)) {
            return;
        }
	TextNode textNode = (TextNode)gn;
	// mark the selection of the substring found
	String text    = textNode.getText();
	String pattern = search.getText();
        if (!caseSensitive.isSelected()) {
            text    = text.toLowerCase();
            pattern = pattern.toLowerCase();
        }
	int end = text.indexOf(pattern, currentIndex);

	AttributedCharacterIterator aci = 
	    textNode.getAttributedCharacterIterator();
	aci.first();
	for (int i=0; i < end; ++i) {
	    aci.next();
	}
	Mark startMark = textNode.getMarkerForChar(aci.getIndex(), true);
	
	for (int i = 0; i < pattern.length()-1; ++i) {
	    aci.next();
	}
	Mark endMark = textNode.getMarkerForChar(aci.getIndex(), false);
	svgCanvas.select(startMark, endMark);

	// zoom on the TextNode if needed
        if (highlightButton.isSelected()) {
	    return;
	}

	// get the highlight shape in GVT root (global) coordinate sytem
	Shape s = textNode.getHighlightShape();
	AffineTransform at;
	if (highlightCenterZoomButton.isSelected()) {
	    at = svgCanvas.getInitialTransform();
	} else {
	    at = svgCanvas.getRenderingTransform();
	}
	// get the bounds of the highlight shape in the canvas coordinate system
	Rectangle2D gnb = at.createTransformedShape(s).getBounds();
	    
	Dimension canvasSize = svgCanvas.getSize();
	// translate the highlight region to (0, 0) in the canvas coordinate
	// system
	AffineTransform Tx = AffineTransform.getTranslateInstance
	    (-gnb.getX()-gnb.getWidth()/2,
	     -gnb.getY()-gnb.getHeight()/2);
	
	if (highlightCenterZoomButton.isSelected()) {
	    // zoom on the highlight shape such as the shape takes x% of the
	    // canvas size
	    double sx = canvasSize.width/gnb.getWidth();
	    double sy = canvasSize.height/gnb.getHeight();
	    double scale = Math.min(sx, sy) / 8;
	    if (scale > 1) {
		Tx.preConcatenate
		    (AffineTransform.getScaleInstance(scale, scale));
	    }
	}
	Tx.preConcatenate(AffineTransform.getTranslateInstance
			  (canvasSize.width/2, canvasSize.height/2));
	// take into account the initial transform
	AffineTransform newRT = new AffineTransform(at);
	newRT.preConcatenate(Tx);
	// change the rendering transform
	svgCanvas.setRenderingTransform(newRT);
    }

    // ActionMap implementation

    /**
     * The map that contains the listeners
     */
    protected Map listeners = new HashMap(10);

    /**
     * Returns the action associated with the given string
     * or null on error
     * @param key the key mapped with the action to get
     * @throws MissingListenerException if the action is not found
     */
    public Action getAction(String key) throws MissingListenerException {
        return (Action)listeners.get(key);
    }

    //////////////////////////////////////////////////////////////////////////
    // Action implementation
    //////////////////////////////////////////////////////////////////////////

    /**
     * The action associated to the 'find' button.
     */
    protected class FindButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            String text = search.getText();
	    if (text == null || text.length() == 0) {
		return;
	    }
	    GraphicsNode gn = getNext(text);
	    if (gn != null) {
		showSelectedGraphicsNode();
	    } else {
		// end of document reached
		walker = null;
		JOptionPane.showMessageDialog(FindDialog.this, 
					      resources.getString("End.text"), 
					      resources.getString("End.title"),
					      JOptionPane.INFORMATION_MESSAGE);
	    }
	}
    }

    /**
     * The action associated to the 'clear' button.
     */
    protected class ClearButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
	    search.setText(null);
	    walker = null;
        }
    }

    /**
     * The action associated to the 'close' button.
     */
    protected class CloseButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            dispose();
        }
    }
}


