package org.argosdic;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.document.Document;
import org.apache.lucene.search.Hits;
import org.argosdic.action.ActionMap;
import org.argosdic.action.BaseAction;
import org.argosdic.action.program.BackAction;
import org.argosdic.action.program.ForwardAction;
import org.argosdic.action.program.SearchAction;
import org.argosdic.action.program.SpeakAction;
import org.argosdic.action.program.SpellCheckAction;
import org.argosdic.dictionary.Dictionary;
import org.argosdic.dictionary.DictionaryManager;
import org.argosdic.dictionary.DictionaryServer;
import org.argosdic.dictionary.HistoryListener;
import org.argosdic.dictionary.SearchHistory;
import org.argosdic.preference.ArgosDicPreferenceStore;
import org.argosdic.resource.ResourceManager;
import org.argosdic.speech.SpeechManager;
import org.argosdic.util.ExceptionHandler;
import org.argosdic.widget.ArgosDicMenuManager;
import org.argosdic.widget.ArgosDicToolBarManager;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.util.Assert;
import org.eclipse.jface.window.ApplicationWindow;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tray;
import org.eclipse.swt.widgets.TrayItem;

/**
 * ArgosDic.java
 * 
 * @author Xavier Cho
 * @version $Revision: 1.25 $ $Date: 2005/03/13 09:10:16 $
 */
public class ArgosDic extends ApplicationWindow {
    private static Log log = LogFactory.getLog(ArgosDic.class);

    private DictionaryServer server;
    private Browser browser;
    private List list;
    private Button searchButton;
    private Text combo;
    private ActionMap actionMap;
    private boolean skipTextModifiedEvent = false;

    /**
     * Defualt constructor of ArgosDic class
     *
     * @param display
     */
    public ArgosDic() {
        super(null);
    }

    /**
     * @see org.eclipse.jface.window.Window#configureShell(org.eclipse.swt.widgets.Shell)
     */
    protected void configureShell(Shell shell) {
        if (log.isDebugEnabled()) {
            log.debug("Configuring shell..."); //$NON-NLS-1$
        }

        this.actionMap = createActionMap();

        ResourceManager resources = ResourceManager.getInstance();

        if (log.isDebugEnabled()) {
            log.debug("Initializing preference store..."); //$NON-NLS-1$
        }

        try {
            IPreferenceStore preferences = new ArgosDicPreferenceStore();
            JFacePreferences.setPreferenceStore(preferences);
        } catch (IOException e) {
            String msg = resources.getString("error.message.preferences"); //$NON-NLS-1$
            ExceptionHandler.handleException(msg, e);
        }

        Thread thread = new Thread(new Runnable() {
            public void run() {
                if (log.isDebugEnabled()) {
                    log.debug("Initializing speech manager..."); //$NON-NLS-1$
                }
                SpeechManager manager = SpeechManager.getInstance();
            }
        });

        thread.start();

        setBlockOnOpen(true);
        addMenuBar();
        addToolBar(SWT.FLAT);
        addStatusLine();

        super.configureShell(shell);

        Window.setExceptionHandler(new ExceptionHandler());

        shell.setText(resources.getString("application.title")); //$NON-NLS-1$
        shell.setImage(resources.getImage("application.icon").createImage()); //$NON-NLS-1$

        Point size = getInitialSize();
        Point location = getInitialLocation(size);

        shell.setBounds(location.x, location.y, size.x, size.y);

        try {
            this.server = new DictionaryServer();
            this.server.start();
        } catch (IOException e) {
            String msg = resources.getString("error.message.server"); //$NON-NLS-1$
            ExceptionHandler.handleException(msg, e);

            System.exit(-1);
        }

        SearchHistory history = server.getSearchHistory();
        history.addHistoryListener(new HistoryListener() {
            public void historyUpdated(Event event) {
                SearchHistory history = server.getSearchHistory();

                actionMap.get(BackAction.class.getName()).setEnabled(
                    history.isBackEnabled());
                actionMap.get(ForwardAction.class.getName()).setEnabled(
                    history.isForwardEnabled());
            }
        });
    }

    protected ActionMap createActionMap() {
        if (log.isDebugEnabled()) {
            log.debug("Initializing action map..."); //$NON-NLS-1$
        }

        ActionMap map = new ActionMap();

        ResourceManager resources = ResourceManager.getInstance();

        Collection actions = resources.getActionNames();
        Iterator it = actions.iterator();

        while (it.hasNext()) {
            String name = (String) it.next();

            try {
                Constructor constructor =
                    Class.forName(name).getConstructors()[0];

                BaseAction action =
                    (BaseAction) constructor.newInstance(new Object[] { this });

                map.put(action);
            } catch (Exception e) {
                log.warn("Unable to instantiate action class " + name, e); //$NON-NLS-1$
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("Loaded " + actions.size() + " action classes."); //$NON-NLS-1$ //$NON-NLS-2$
        }

        return map;
    }

    private void showDefinition(String word) {
        combo.setText(word);

        StringBuffer buffer = new StringBuffer();
        buffer.append(server.getUrl());
        buffer.append("/"); //$NON-NLS-1$
        buffer.append(word);
        buffer.append(".dic"); //$NON-NLS-1$

        this.browser.setUrl(buffer.toString());
    }

    public void search() {
        String word = combo.getText();

        if (word != null && word.trim().length() > 0) {
            search(word, true);
        }
    }

    public void fuzzySearch() {
        String word = combo.getText();

        if (word != null && word.trim().length() > 0) {
            if (DictionaryServer.isSimpleQuery(word)) {
                word = word.concat("~"); //$NON-NLS-1$
                search(word, true);
            }
        }
    }

    public void search(String word) {
        search(word, true);
    }

    public void search(String word, boolean updateHistory) {
        if (word != null) {
            try {
                if (!word.equals(combo.getText())) {
                    combo.setText(word);
                }

                run(true, false, new SearchWorker(word, updateHistory));
            } catch (Exception e) {
                ResourceManager resources = ResourceManager.getInstance();

                String msg = resources.getString("error.message.unknown"); //$NON-NLS-1$
                ExceptionHandler.handleException(msg, e);
            }
        }
    }

    /**
     * @see org.eclipse.jface.window.ApplicationWindow#createMenuManager()
     */
    protected MenuManager createMenuManager() {
        return new ArgosDicMenuManager(this);
    }

    /**
     * @see org.eclipse.jface.window.ApplicationWindow#createToolBarManager(int
     *          style)
     */
    protected ToolBarManager createToolBarManager(int style) {
        return new ArgosDicToolBarManager(this);
    }

    /**
     * @see org.eclipse.jface.window.Window#createContents(org.eclipse.swt.widgets.Composite)
     */
    protected Control createContents(Composite parent) {
        Composite composite = new Composite(parent, SWT.NONE);

        ResourceManager resources = ResourceManager.getInstance();

        //FIXME: workaround for GridLayout bug : #43787
        GridLayout gridLayout1 = new GridLayout();
        gridLayout1.numColumns = 1;
        gridLayout1.marginWidth = 0;
        gridLayout1.marginHeight = 0;
        gridLayout1.horizontalSpacing = 0;
        gridLayout1.verticalSpacing = 0;

        composite.setLayout(gridLayout1);

        Composite panel = new Composite(composite, SWT.NONE);
        panel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        GridLayout gridLayout2 = new GridLayout();
        gridLayout2.numColumns = 3;

        panel.setLayout(gridLayout2);

        Label label = new Label(panel, SWT.NONE);

        GridData gridData1 = new GridData();
        gridData1.horizontalIndent = 3;

        label.setText(resources.getString("label.word")); //$NON-NLS-1$
        label.setLayoutData(gridData1);

        this.combo = new Text(panel, SWT.BORDER);

        GridData gridData2 = new GridData(GridData.FILL_HORIZONTAL);

        combo.setLayoutData(gridData2);
        combo.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                handleTextModified();
            }
        });

        this.searchButton = new Button(panel, SWT.PUSH);
        searchButton.setEnabled(false);
        searchButton.setText(resources.getString("label.search")); //$NON-NLS-1$
        searchButton.setToolTipText(resources.getString("label.search.tooltip")); //$NON-NLS-1$

        searchButton.addListener(SWT.Selection, new Listener() {
            public void handleEvent(Event e) {
                search();
            }
        });

        GridData gridData3 = new GridData();
        gridData3.widthHint = 100;

        searchButton.setLayoutData(gridData3);

        getShell().setDefaultButton(searchButton);

        SashForm sash = new SashForm(composite, SWT.HORIZONTAL);
        sash.setLayoutData(new GridData(GridData.FILL_BOTH));

        this.list = new List(sash, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL);

        list.addListener(SWT.Selection, new Listener() {
            public void handleEvent(Event e) {
                if (list.getSelectionCount() > 0) {
                    String word = list.getSelection()[0];

                    showDefinition(word);
                }
            }
        });

        this.browser = new Browser(sash, SWT.WRAP | SWT.BORDER);
        browser.setUrl(server.getUrl() + "/index.html"); //$NON-NLS-1$

        sash.setWeights(new int[] { 20, 80 });

        combo.setFocus();

        Tray tray = getShell().getDisplay().getSystemTray();
        if (tray != null) {
            TrayItem trayItem = new TrayItem(tray, SWT.NONE);

            trayItem.setText(getShell().getText());
            trayItem.setImage(resources.getImage("image.dictionary").createImage()); //$NON-NLS-1$
            trayItem.addSelectionListener(new SelectionListener() {

                public void widgetSelected(SelectionEvent e) {
                }

                public void widgetDefaultSelected(SelectionEvent e) {
                    Shell shell = getShell();
                    shell.setVisible(!shell.isVisible());
                }
            });
        }

        return composite;
    }

    /**
     * @see org.eclipse.jface.window.Window#getInitialLocation(org.eclipse.swt.graphics.Point)
     */
    protected Point getInitialLocation(Point initialSize) {
        IPreferenceStore preferences = JFacePreferences.getPreferenceStore();

        int x = preferences.getInt("location.x"); //$NON-NLS-1$
        int y = preferences.getInt("location.y"); //$NON-NLS-1$

        return new Point(x, y);
    }

    /**
     * @see org.eclipse.jface.window.Window#getInitialSize()
     */
    protected Point getInitialSize() {
        IPreferenceStore preferences = JFacePreferences.getPreferenceStore();

        int x = preferences.getInt("size.width"); //$NON-NLS-1$
        int y = preferences.getInt("size.height"); //$NON-NLS-1$

        return new Point(x, y);
    }

    public ActionMap getActionMap() {
        return actionMap;
    }

    public String getSearchWord() {
        return combo.getText();
    }

    public SearchHistory getSearchHistory() {
        return server.getSearchHistory();
    }

    private void handleTextModified() {
        String keyword = combo.getText();
        int length = keyword.trim().length();

        boolean enabled = (length > 0);

        searchButton.setEnabled(enabled);
        actionMap.get(SearchAction.class.getName()).setEnabled(enabled);
        actionMap.get(SpeakAction.class.getName()).setEnabled(enabled);
        actionMap.get(SpellCheckAction.class.getName()).setEnabled(
            DictionaryServer.isSimpleQuery(keyword));
    }

    /**
     * @see org.eclipse.jface.window.Window#handleShellCloseEvent()
     */
    protected void handleShellCloseEvent() {
        Shell shell = getShell();

        Point location = shell.getLocation();
        Point size = shell.getSize();

        super.handleShellCloseEvent();

        if (log.isInfoEnabled()) {
            log.info("Saving user preferences..."); //$NON-NLS-1$
        }

        ArgosDicPreferenceStore preferences =
            (ArgosDicPreferenceStore) JFacePreferences.getPreferenceStore();

        preferences.setValue("location.x", location.x); //$NON-NLS-1$
        preferences.setValue("location.y", location.y); //$NON-NLS-1$
        preferences.setValue("size.width", size.x); //$NON-NLS-1$
        preferences.setValue("size.height", size.y); //$NON-NLS-1$

        DictionaryManager manager = DictionaryManager.getInstance();
        Dictionary dictionary = manager.getSelectedDictionary();

        if (dictionary != null) {
            preferences.setValue("default.dictionary", dictionary.getId()); //$NON-NLS-1$
        }

        SpeechManager.getInstance().dispose();

        try {
            preferences.save();
        } catch (IOException e) {
            if (log.isWarnEnabled()) {
                String msg = "Failed to save user preferences data."; //$NON-NLS-1$
                log.warn(msg, e);
            }
        }

        try {
            server.stop();
        } catch (IOException e) {
            if (log.isWarnEnabled()) {
                String msg = "Failed to stop dictionary server."; //$NON-NLS-1$
                log.warn(msg, e);
            }
        }
    }

    public static void main(String[] args) {
        try {
            ApplicationWindow application = new ArgosDic();
            application.open();
        } catch (Throwable t) {
            if (log.isFatalEnabled()) {
                String msg = "Unable to initialize application."; //$NON-NLS-1$
                log.fatal(msg, t);
            }
        } finally {
            Display.getCurrent().dispose();
            System.exit(0);
        }
    }

    private class SearchWorker implements IRunnableWithProgress, Runnable {
        private String word;
        private boolean updateHistory;
        private Hits hits;

        private SearchWorker(String word, boolean updateHistory) {
            Assert.isNotNull(word);

            this.word = word;
            this.updateHistory = updateHistory;
        }

        /**
         * @see org.eclipse.jface.operation.IRunnableWithProgress#run(org.eclipse.core.runtime.IProgressMonitor)
         */
        public void run(IProgressMonitor monitor)
            throws InvocationTargetException, InterruptedException {

            try {
                ResourceManager resources = ResourceManager.getInstance();

                monitor.beginTask(resources.getString("label.progress"), IProgressMonitor.UNKNOWN); //$NON-NLS-1$

                this.hits = server.search(word, updateHistory);
                getShell().getDisplay().syncExec(this);
            } catch (final Exception e) {
                getShell().getDisplay().syncExec(new Runnable() {
                    public void run() {
                        ResourceManager resources =
                            ResourceManager.getInstance();

                        String msg = resources.getString("error.message.unknown"); //$NON-NLS-1$
                        ExceptionHandler.handleException(msg, e);
                    }
                });
            } finally {
                monitor.done();
            }
        }

        public void run() {
            Assert.isNotNull(hits);

            if (hits == null || hits.length() == 0) {
                browser.setUrl(server.getUrl() + "/index.html"); //$NON-NLS-1$
            } else {
                try {
                    int count = hits.length();
                    String[] items = new String[count];

                    for (int i = 0; i < count; i++) {
                        Document document = hits.doc(i);
                        String entry = document.getField("word").stringValue(); //$NON-NLS-1$

                        if (count == 1) {
                            skipTextModifiedEvent = true;

                            combo.setText(entry);

                            skipTextModifiedEvent = false;
                        }

                        items[i] = entry;
                    }

                    Arrays.sort(items);
                    list.setItems(items);

                    showDefinition(word);
                } catch (Exception e) {
                    ResourceManager resources = ResourceManager.getInstance();

                    String msg = resources.getString("error.message.unknown"); //$NON-NLS-1$
                    ExceptionHandler.handleException(msg, e);
                }
            }
        }
    }
}
