package org.argosdic.dictionary;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.BindException;
import java.net.ServerSocket;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.argosdic.resource.ResourceManager;
import org.argosdic.search.EnglishAnalyzer;
import org.argosdic.util.SpecialFolders;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.util.Assert;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 * DictionaryServer.java
 * @author Xavier Cho
 * @version $Revision: 1.22 $ $Date: 2003/10/19 14:41:35 $
 */
public class DictionaryServer {
    protected static final int STATUS_OK = 200;
    protected static final int STATUS_NOT_MODIFIED = 304;
    protected static final int STATUS_NOT_FOUND = 404;
    protected static final int STATUS_INTERNAL_ERROR = 500;
    protected static final String LINE_SEPARATOR = "\r\n"; //$NON-NLS-1$

    private static Log log = LogFactory.getLog(DictionaryServer.class);

    private ServerSocket socket;
    private Analyzer simpleAnalyzer;
    private Analyzer sentenceAnalyzer;
    private Dictionary dictionary;
    private IndexSearcher searcher;
    private String url;
    private String dataDir;
    private String htmlDir;
    private FontData font;
    private int port;
    private int poolSize;
    private int wildcardQueryThreshold;
    private List workers;
    private SearchHistory searchHistory;
    private long templateModifiedTime;
    private MessageFormat templateFormat;

    public DictionaryServer() {
        ResourceManager resources = ResourceManager.getInstance();

        IPreferenceStore preferences = JFacePreferences.getPreferenceStore();

        this.htmlDir = SpecialFolders.getHtmlDirectory();
        this.dataDir = preferences.getString("data.dir"); //$NON-NLS-1$
        this.port = preferences.getInt("server.port"); //$NON-NLS-1$
        this.poolSize = preferences.getInt("server.workers"); //$NON-NLS-1$
        this.url = "http://localhost:" + port; //$NON-NLS-1$
        this.searchHistory = new SearchHistory();

        handleDictionaryChanged();

        DictionaryManager manager = DictionaryManager.getInstance();
        manager.getSelectedDictionary();
        manager.addDictionaryListener(new DictionaryListener() {
            public void dictionaryUpdated(DictionaryEvent event) {
            }

            public void dictionaryChanged(DictionaryEvent event) {
                handleDictionaryChanged();
            }
        });

        preferences.addPropertyChangeListener(new IPropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent event) {
                handlePreferencesChanged();
            }
        });

        BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);

        handlePreferencesChanged();
    }

    private void handlePreferencesChanged() {
        IPreferenceStore preferences = JFacePreferences.getPreferenceStore();
        this.font = PreferenceConverter.getFontData(preferences, "browser.font"); //$NON-NLS-1$
        this.wildcardQueryThreshold = preferences.getInt("wildcard.query.threshold"); //$NON-NLS-1$
    }

    private void handleDictionaryChanged() {
        if (searcher != null) {
            try {
                searcher.close();
            } catch (IOException e) {
                if (log.isWarnEnabled()) {
                    String msg = "Unable to close IndexSearcher instance."; //$NON-NLS-1$
                    log.warn(msg, e);
                }
            }
            this.searcher = null;
        }

        this.searchHistory.clear();

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

        this.dictionary = manager.getSelectedDictionary();

        if (dictionary != null) {
            StringBuffer buffer = new StringBuffer();
            buffer.append(dataDir);
            buffer.append(File.separator);
            buffer.append(dictionary.getId());

            try {
                this.searcher = new IndexSearcher(buffer.toString());
            } catch (IOException e) {
                if (log.isWarnEnabled()) {
                    String msg = "Unable to use the selected dictionary."; //$NON-NLS-1$
                    log.warn(msg, e);
                }
            }

            Analyzer analyzer = null;
            String analyzerClass = dictionary.getAnalyzer();
            if (analyzerClass != null) {
                if (log.isInfoEnabled()) {
                    log.info("Instantiating anayzer : " + analyzerClass); //$NON-NLS-1$
                }

                try {
                    Class cl = Class.forName(analyzerClass);
                    analyzer = (Analyzer) cl.newInstance();
                } catch (Exception e) {
                    String msg = "Unable to instantiate the specified analyzer class."; //$NON-NLS-1$
                    log.error(msg, e);
                }
            }

            if (analyzer == null) {
                analyzer = new EnglishAnalyzer();

                log.info("Using default anayzer : " + analyzer.getClass()); //$NON-NLS-1$
            }

            this.simpleAnalyzer = new SimpleAnalyzer();
            this.sentenceAnalyzer = analyzer;
        }
    }

    public Hits search(String word) throws ParseException, IOException {
        return search(word, true);
    }

    public Hits search(String word, boolean updateHistory)
        throws ParseException, IOException {
        if (word == null || word.trim().length() == 0) {
            return null;
        } else if (searcher == null) {
            ResourceManager resources = ResourceManager.getInstance();
            Shell parent = Display.getCurrent().getActiveShell();

            String title = resources.getString("dialog.title.warning"); //$NON-NLS-1$
            String msg = resources.getString("error.message.dictionary"); //$NON-NLS-1$

            MessageDialog.openWarning(parent, title, msg);

            return null;
        }

        word = word.trim();
        Hits hits = null;

        Query query = parseQueryString(word);
        hits = searcher.search(query, (Filter) null);

        if (updateHistory) {
            int matches = hits.length();

            if (matches > 0) {
                Document document = null;

                for (int i = 0; i < matches; i++) {
                    Document doc = hits.doc(i);

                    if (word.equalsIgnoreCase(doc.getField("word").stringValue())) { //$NON-NLS-1$
                        document = doc;
                        break;
                    }
                }

                if (document == null) {
                    document = hits.doc(0);
                }

                searchHistory.add(document);
            }
        }

        return hits;
    }

    public static boolean isSimpleQuery(String word) {
        Assert.isNotNull(word);

        boolean simple =
            (word.indexOf(" ") == -1 //$NON-NLS-1$
                && !word.endsWith("*") //$NON-NLS-1$
                && !word.endsWith("~") //$NON-NLS-1$
                && !word.endsWith("?")); //$NON-NLS-1$

        return simple;
    }

    protected Query parseQueryString(String word) throws ParseException {
        Query query = null;

        if (isSimpleQuery(word)) {
            if (word.length() >= wildcardQueryThreshold) {
                word = word.concat("*"); //$NON-NLS-1$
            }
            query = QueryParser.parse(word, "word", simpleAnalyzer); //$NON-NLS-1$
        } else {
            query = QueryParser.parse(word, "word", sentenceAnalyzer); //$NON-NLS-1$

            if (log.isDebugEnabled()) {
                try {
                    Reader reader = new StringReader(word.trim());
                    TokenStream in = sentenceAnalyzer.tokenStream("word", reader); //$NON-NLS-1$

                    for (;;) {
                        Token token = in.next();

                        if (token == null) {
                            break;
                        }

                        log.debug("Found token : " + token.termText()); //$NON-NLS-1$
                    }
                } catch (IOException e) {
                }
            }
        }

        return query;
    }

    /**
     * @return
     */
    public String getUrl() {
        return url;
    }

    public void start() throws IOException {
        if (socket == null) {
            if (log.isInfoEnabled()) {
                log.info("Starting dictionary server..."); //$NON-NLS-1$
            }

            synchronized (this) {
                int retry = 30;

                while ((socket == null || !socket.isBound()) && retry > 0) {
                    try {
                        this.socket = new ServerSocket(port);
                    } catch (BindException e) {
                        retry--;
                        port--;

                        if (log.isWarnEnabled()) {
                            log.warn("Unable to bind to the specified port."); //$NON-NLS-1$
                            log.warn("Retrying with port : " + port); //$NON-NLS-1$
                        }
                    }
                }

                if (socket == null || !socket.isBound()) {
                    String msg = "Unable to bind to the specified port range."; //$NON-NLS-1$
                    throw new IOException(msg);
                } else if (log.isInfoEnabled()) {
                    log.info("Dictionary server has been bound for port : " + port); //$NON-NLS-1$
                }
            }
        }

        if (workers != null && workers.size() > 0) {
            if (log.isWarnEnabled()) {
                log.warn("Dictionary server is already running."); //$NON-NLS-1$
            }
        } else {
            this.workers = new ArrayList(poolSize);

            for (int i = 1; i <= poolSize; i++) {
                if (log.isInfoEnabled()) {
                    log.info("Spawning new server worker (" //$NON-NLS-1$
                    +i + " of  " //$NON-NLS-1$
                    +poolSize + ")..."); //$NON-NLS-1$
                }

                Runnable runnable = new ServerWorker(this, socket, i);
                Thread worker = new Thread(runnable);
                worker.start();

                workers.add(worker);
            }
        }
    }

    public void stop() throws IOException {
        if (socket != null) {
            if (log.isInfoEnabled()) {
                log.info("Stopping dictionary server..."); //$NON-NLS-1$
            }

            socket.close();

            if (workers != null && workers.size() > 0) {
                for (int i = 1; i <= poolSize; i++) {
                    if (log.isInfoEnabled()) {
                        log.info("Stopping server worker (" //$NON-NLS-1$
                        +i + " of  " //$NON-NLS-1$
                        +poolSize + ")..."); //$NON-NLS-1$
                    }

                    Thread worker = (Thread) workers.get(i - 1);
                    worker.interrupt();
                }

                workers.clear();
            }
        }
    }

    /**
     * @return
     */
    public String getDataDir() {
        return dataDir;
    }

    /**
     * @return
     */
    public FontData getFont() {
        return font;
    }

    /**
     * @return
     */
    public String getHtmlDir() {
        return htmlDir;
    }

    /**
     * @return
     */
    public int getPoolSize() {
        return poolSize;
    }

    /**
     * @return
     */
    public int getPort() {
        return port;
    }

    /**
     * @return
     */
    protected IndexSearcher getSearcher() {
        return searcher;
    }

    /**
     * @return
     */
    protected Analyzer getSimpleAnalyzer() {
        return simpleAnalyzer;
    }

    /**
     * @return
     */
    protected Analyzer getSentenceAnalyzer() {
        return sentenceAnalyzer;
    }

    /**
     * @return
     */
    public SearchHistory getSearchHistory() {
        return searchHistory;
    }

    /**
     * @return
     */
    public Dictionary getDictionary() {
        return dictionary;
    }

    protected String getHttpHeader(int status) {
        return getHttpHeader(status, null);
    }

    protected String getHttpHeader(int status, String message) {
        Map headers = new HashMap(1);

        StringBuffer buffer = new StringBuffer();
        buffer.append("text/html"); //$NON-NLS-1$

        if (dictionary != null) {
            buffer.append(";charset="); //$NON-NLS-1$
            buffer.append(dictionary.getCharset());
        }

        headers.put("Content-Type", buffer.toString()); //$NON-NLS-1$
        return getHttpHeader(status, null, headers);
    }

    protected String getHttpHeader(int status, String message, Map headers) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("HTTP/1.0 "); //$NON-NLS-1$
        buffer.append(status);

        if (message != null) {
            buffer.append(" "); //$NON-NLS-1$
            buffer.append(message);
        }

        buffer.append(LINE_SEPARATOR); //$NON-NLS-1$

        if (headers != null) {
            Iterator it = headers.keySet().iterator();
            while (it.hasNext()) {
                Object key = it.next();
                Object value = headers.get(key);

                buffer.append(key);
                buffer.append(": "); //$NON-NLS-1$
                buffer.append(value);
                buffer.append(LINE_SEPARATOR);
            }
        }

        buffer.append(LINE_SEPARATOR); //$NON-NLS-1$

        return buffer.toString();
    }

    protected String getHtmlContent(Object[] args) {
        String path = SpecialFolders.getHtmlDirectory() + "/template.html"; //$NON-NLS-1$
        File file = new File(path);

        if (templateFormat == null
            || templateModifiedTime < file.lastModified()) {
            synchronized (this) {
                BufferedReader reader = null;

                StringBuffer buffer = new StringBuffer();

                try {
                    reader = new BufferedReader(new FileReader(file));

                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        buffer.append(line);
                        buffer.append("\n"); //$NON-NLS-1$
                    }

                    this.templateFormat = new MessageFormat(buffer.toString());
                    this.templateModifiedTime = file.lastModified();
                } catch (IOException e) {
                    if (log.isErrorEnabled()) {
                        String msg = "Unable to read HTML template."; //$NON-NLS-1$
                        log.error(msg, e);
                    }
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
        }

        return templateFormat.format(args);
    }

    public static void main(String[] args) {
        DictionaryServer server = null;

        try {
            server = new DictionaryServer();
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (server != null) {
                try {
                    server.stop();
                } catch (IOException e) {
                }
            }
        }
    }
}
