/*
 * @(#)TextView.java  1.1  2002-02-10
 *
 * Copyright (c) 1999-2000 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package ch.randelshofer.view;

import org.monte.media.binary.BinaryPanel;
import org.monte.media.binary.BinaryModel;
import ch.randelshofer.util.*;
import ch.randelshofer.util.regex.*;

import java.awt.*;
import java.awt.dnd.*;

import java.awt.event.*;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import java.net.URL;
import java.net.MalformedURLException;

import java.util.*;

import java.io.*;

import javax.activation.CommandObject;
import javax.activation.DataHandler;

import javax.swing.*;
import javax.swing.text.*;

/**
 * View for plain text.
 *
 * @author  Werner Randelshofer, Hausmatt 10, CH-6405 Immensee, Switzerland
 * @version 1.1 2002-02-11 Support for DnD added. Find panel added.
 *                         Support for multiple charsets added.
 * <br> 1.0.2   2000-10-08 Set small font when Platinum LAF is active.
 * <br> 1.0.1   2000-06-10
 *                     Performance of text loading improved.
 *                     Java Style Guide for menu items used.
 * <br> 1.0     1999-10-19
 */
public class TextView extends AbstractView implements ItemListener, ActionListener {
    /**
     * The text area used for displaying the text.
     */
    private JTextArea textArea;
    
    /**
     * Thread for asynchronous loading of text data.
     */
    private Thread loaderThread;
    
    /**
     * The menu item used for choosing line wrapping.
     */
    private JCheckBoxMenuItem lineWrapItem;
    
    /**
     * The menu item used to invoke the find action.
     */
    private JMenuItem findItem;
    /**
     * The menu item used to invoke the find again action.
     */
    private JMenuItem findAgainItem;
    /**
     * The panel used for the find feature.
     */
    private FindPanel findPanel;
    /**
     * The Matcher used to perform find operation.
     */
    private Matcher matcher;
    
    /**
     * The scroll pane.
     */
    private JScrollPane scrollPane;
    
    /**
     * The current character set.
     */
    private String characterSet;
    
    /**
     * The list of available character sets.
     * Note: replace this constant by java.nio.charset.Charset.availableCharsets()
     * for JDK 1.4.
     */
    private final static String[] availableCharacterSets = {
        "US-ASCII", "IBM-850", "Windows-1252", "ISO-8859-1", "UTF-8", "UTF-16BE", "UTF-16LE", "UTF-16"
    };
    
    /**
     * The menu items used for choosing character sets.
     */
    private LinkedList characterSetItems;
    
    /**
     * The data handler provides the data source for
     * the text.
     */
    private DataHandler dataHandler;
    
    /**
     * Resource bundle for internationalisation.
     */
    private ResourceBundleUtil labels;
    
    public TextView() {
        labels = ResourceBundleUtil.getLAFBundle("ch.randelshofer.view.TextView");
        
        setLayout(new BorderLayout());
        textArea = new JTextArea(40, 60);
        textArea.setEditable(false);
        textArea.setWrapStyleWord(true);
        if (UIManager.getLookAndFeel().getID().equals("MacOS")) {
            textArea.setFont(
            new Font("Monospaced",Font.PLAIN,10)
            );
        } else {
            textArea.setFont(
            new Font("Monospaced",Font.PLAIN,12)
            );
        }

        scrollPane = new JScrollPane(
        textArea,
        JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
        JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS
        );
        scrollPane.setBorder(null);
        add(scrollPane);
        
        // Menu items for the find action
        findItem = new JMenuItem(labels.getString("findItem"));
        findItem.setMnemonic(labels.getMnem("findItem"));
        findItem.setAccelerator(labels.getAcc("findItem"));
        findItem.addActionListener(this);
        findItem.setActionCommand("find");
        
        // Menu items for the find again action
        findAgainItem = new JMenuItem(labels.getString("findAgainItem"));
        findAgainItem.setMnemonic(labels.getMnem("findAgainItem"));
        findAgainItem.setAccelerator(labels.getAcc("findAgainItem"));
        findAgainItem.addActionListener(this);
        findAgainItem.setActionCommand("findAgain");
        
        // Menu items for line wrapping
        lineWrapItem = new JCheckBoxMenuItem(labels.getString("wrapLines"));
        lineWrapItem.setMnemonic(labels.getMnem("wrapLines"));
        lineWrapItem.setAccelerator(labels.getAcc("wrapLines"));
        lineWrapItem.addItemListener(this);
        lineWrapItem.setActionCommand("wrapLines");
        
        // Menu items for character set selection
        characterSet = System.getProperty("file.encoding");
        characterSetItems = new LinkedList();
        ButtonGroup group = new ButtonGroup();
        JRadioButtonMenuItem item;
        for (int i=0; i < availableCharacterSets.length; i++) {
            item = new JRadioButtonMenuItem(availableCharacterSets[i]);
            group.add(item);
            item.setActionCommand("characterSet");
            item.addItemListener(this);
            if (availableCharacterSets[i].equals(characterSet))
                item.setSelected(true);
            characterSetItems.add(item);
        }
        if (group.getSelection() == null) {
            item = new JRadioButtonMenuItem(characterSet);
            group.add(item);
            item.setActionCommand("characterSet");
            item.addItemListener(this);
            item.setSelected(true);
            characterSetItems.add(item);
        }
    }
    
    /**
     * Sets the command context for the bean. The receiver loads
     * the image data provided by the data handler and displays it.
     */
    public void setCommandContext(String verb, DataHandler dh)
    throws IOException {
        dataHandler = dh;
        reload();
    }
    
    /**
     * Reloads the text from the data handler.
     * This method is invoked when the data handler
     * is associated with this view using setCommandContext
     * and when the user changes the character set.
     */
    private void reload()
    throws IOException {
        if (dataHandler == null) return;
        
        // Stop existing loader threads
        Thread oldLoaderThread = loaderThread;
        if (oldLoaderThread != null) {
            loaderThread = null;
            try {
                oldLoaderThread.join();
            } catch (InterruptedException e) {
            }
        }
        
        // Open the input stream. Use a previously
        // defined character set if one is available.
        InputStreamReader isr = new InputStreamReader(dataHandler.getInputStream(), characterSet);
        
        // Read the file
        final BufferedReader in = new BufferedReader(isr);
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        loaderThread = new Thread() {
            public void run() {
                final Document doc = new PlainDocument();
                try {
                    String line;
                    //Document doc = new getDocument();
                    doc.remove(0,doc.getLength());
                    //PrintWriter2 writer = new PrintWriter2(new DocumentWriter(doc));
                    //writer.setLineSeparator("\n");
                    while ( (line = in.readLine()) != null
                    && Thread.currentThread() == loaderThread) {
                        //writer.println(line);
                        doc.insertString(doc.getLength(), line, null);
                        doc.insertString(doc.getLength(), "\n", null);
                    }
                } catch (BadLocationException e) {
                    textArea.setText(e.getMessage());
                } catch (IOException e) {
                    textArea.setText(e.getMessage());
                } finally {
                    try {
                        in.close();
                    } catch (IOException e) {}
                    SwingUtilities.invokeLater(
                    new Runnable() {
                        public void run() {
                            textArea.setDocument(doc);
                            setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                            validate();
                            repaint();
                        }
                    }
                    );
                    loaderThread = null;
                }
            }
        };
        loaderThread.start();
    }
    
    /**
     * Adds menu items to the "View" menu.
     *
     * When the viewer is added to a container with a
     * JMenuBar. Then the container asks the viewer to
     * add his menu items to the "View" menu of the container.
     */
    public void mergeViewMenuItems(JMenu m) {
        JMenu m2;
        JMenuItem mi;
        JRadioButtonMenuItem cmi;
        ButtonGroup group;
        
        m.add(findItem);
        m.add(findAgainItem);
        m.addSeparator();
        m.add(lineWrapItem);
        
        m2 = new JMenu(labels.getString("characterSet"));
        m2.setMnemonic(labels.getMnem("characterSet"));
        
        Iterator iter = characterSetItems.iterator();
        while (iter.hasNext()) {
            m2.add((JMenuItem) iter.next());
        }
        
        m.add(m2);
    }
    /**
     * Adds menu items to the "Settings" menu.
     *
     * When the viewer is added to a container with a
     * JMenuBar. Then the container asks the viewer to
     * add his menu items to the "Settings" menu of the container.
     */
    public void mergeSettingsMenuItems(JMenu m)
    {}
    
    /**
     * Returns true if this viewer has a scrollpane of its own.
     * The container that contains this viewers uses this method to
     * determine if it should wrap a scrollpane around the viewer
     * or not.
     *
     * @return  true if this viewer has a scrollpane of its own.
     */
    public boolean hasScrollPane()
    { return true; }
    
    
    /**
     * Disposes all resources allocated by this viewer.
     */
    public void dispose() {
        loaderThread = null;
    }
    public String toString() {
        return (loaderThread == null)
        ? ("Plain Text View\n\nNumber of Lines: "+textArea.getLineCount()
        +"\nCharacter Set: "+characterSet)
        : ("Plain Text View\n\nLoading text...");
    }
    
    protected void updateViewProperties() {
        updateLineWrap();
        updateCharacterSet();
    }
    
    protected void updateApplicationProperties() {
    }
    
    private void updateLineWrap() {
        boolean b = getViewProperties().getProperty("lineWrap","false").equals("true");
        lineWrapItem.setSelected(b);
        textArea.setLineWrap(b);
    }
    
    private void updateCharacterSet() {
        String newValue = getViewProperties().getProperty("characterSet", characterSet);
        if (! newValue.equals(characterSet)) {
            characterSet = newValue;
            Iterator iter = characterSetItems.iterator();
            while (iter.hasNext()) {
                JRadioButtonMenuItem item = (JRadioButtonMenuItem) iter.next();
                if (item.getText().equals(newValue)) {
                    item.setSelected(true);
                    break;
                }
            }
            
            try {
                reload();
            } catch (IOException e) {
                textArea.setText(e.getMessage());
            }
        }
        getViewProperties().put("title", " as Text "+newValue);
    }
    
    public void itemStateChanged(ItemEvent event) {
        Object source = event.getSource();
        
        if (source == lineWrapItem) {
            getViewProperties().put(
            "lineWrap",
            String.valueOf(event.getStateChange() == ItemEvent.SELECTED)
            );
        } else if (source instanceof JRadioButtonMenuItem) {
            JRadioButtonMenuItem item = (JRadioButtonMenuItem) source;
            if (item.isSelected() && item.getActionCommand().equals("characterSet")) {
                getViewProperties().put("characterSet", item.getText());
            }
        }
    }
    
    public void propertyChange(PropertyChangeEvent event) {
        String key = event.getPropertyName();
        Object source = event.getSource();
        
        if (source == getViewProperties()) {
            if (key.equals("lineWrap")) {
                updateLineWrap();
            } else if (key.equals("characterSet")) {
                updateCharacterSet();
            }
        } else if (source == getApplicationProperties()) {
        }
    }
    
    /**
     * This method adds the given drop target listener to
     * all components of the viewer.
     */
    public void addDropTargetListener(DropTargetListener listener) {
        new DropTarget(this, listener);
        new DropTarget(textArea, listener);
        new DropTarget(scrollPane, listener);
    }
    
    public void actionPerformed(ActionEvent event) {
        Object source = event.getSource();
        if (source == findItem) showFindDialog();
        else if (source == findAgainItem) findAgain();
    }
    
    public void showFindDialog() {
        if (findPanel == null) {
            findPanel = new FindPanel();
            findPanel.setFindString(getViewProperties().getProperty("findString"));
            findPanel.setMatchCase(getViewProperties().getProperty("findMatchCase","false").equals("true"));
            findPanel.setMatchWord(getViewProperties().getProperty("findMatchWord","false").equals("true"));
        }
        if (findPanel.showDialog(this, labels.getString("findDialogTitle"))
        == FindPanel.APPROVE_OPTION) {
            getViewProperties().put("findString",findPanel.getFindString());
            getViewProperties().put("findMatchCase",new Boolean(findPanel.isMatchCase()).toString());
            getViewProperties().put("findMatchWord",new Boolean(findPanel.isMatchWord()).toString());
            
            matcher = new Matcher(
            textArea.getDocument(),
            findPanel.getFindString(),
            findPanel.isMatchCase(),
            findPanel.isMatchWord()
            );
            textArea.setCaretPosition(0);
            findAgain();
        }
    }
    
    public void findAgain() {
        if (matcher == null) {
            if (getViewProperties().getProperty("findString") == null) {
                showFindDialog();
                return;
            }
            matcher = new Matcher(
            textArea.getDocument(),
            getViewProperties().getProperty("findString", ""),
            getViewProperties().getProperty("findMatchCase","false").equals("true"),
            getViewProperties().getProperty("findMatchWord","false").equals("true")
            );
        }
        int index = matcher.find(
        textArea.getCaretPosition() < textArea.getDocument().getLength() - 1
        ? textArea.getCaretPosition()
        : 0
        );
        if (index == -1) {
            getToolkit().beep();
            textArea.setCaretPosition(textArea.getDocument().getLength());
            matcher.reset();
        } else {
            textArea.select(index, index+matcher.getFindString().length());
            textArea.requestFocus();
        }
    }
}