package org.salvipeter.ardict;

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTMLDocument;

public class ArabicDictionary extends JFrame {
        
        private static final long serialVersionUID = 1L;
        
        private JTextField search;
        private JEditorPane entries;
        private Font arabic_font, english_font;
        private List<String[]> dictionary;
        private Map<Character, List<Integer>> index;
        private SwingWorker<String, Void> worker;

        public ArabicDictionary() throws FontFormatException, IOException, URISyntaxException {
                // Set up fonts
                InputStream is = getClass().getResourceAsStream("/ScheherazadeRegOT.ttf");
                arabic_font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(36.0f);
                is.close();
                english_font = new Font("SansSerif", Font.PLAIN, 16);
                        
                // Set up main GUI
                setTitle("Arabic Dictionary");
                setDefaultCloseOperation(EXIT_ON_CLOSE);
                setSize(400, 400);
                setLocationRelativeTo(null);
                
                // Set up the menu
                JMenuBar menu = new JMenuBar();
                JMenu options = new JMenu("Settings");
                menu.add(options);
                JMenuItem help = new JMenuItem("Help");
                JMenuItem smaller = new JMenuItem("Smaller text");
                JMenuItem larger = new JMenuItem("Larger text");
                options.add(help); help.addActionListener(new MenuListener());
                options.add(smaller); smaller.addActionListener(new MenuListener());
                options.add(larger); larger.addActionListener(new MenuListener());
                setJMenuBar(menu);
                
                // Set up widgets
                setLayout(new BorderLayout());
                search = new JTextField();
                search.setFont(arabic_font);
                search.addActionListener(new SearchListener());
                search.getDocument().addDocumentListener(new SearchDocListener());
                entries = new JEditorPane("text/html", "");
                entries.setDocument(new FontedDocument());
                entries.setEditable(false);
                add(search, BorderLayout.PAGE_START);
                add(new JScrollPane(entries), BorderLayout.CENTER);
                
                // Load dictionary
                dictionary = new ArrayList<String[]>();
                BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/buckwalter-sorted.txt")));
                String line;
                while ((line = br.readLine()) != null) {
                        String words[] = line.split("\t");
                        dictionary.add(words);
                }
                br.close();
                
                // Load index
                index = new HashMap<Character, List<Integer>>();
                byte[] one_char = new byte[1];
                byte[] one_int16 = new byte[2];
                BufferedInputStream bis = new BufferedInputStream(getClass().getResourceAsStream("/buckwalter-sorted.idx"));
                while (bis.read(one_char) > 0) {
                        char c = (char)((int)one_char[0] & 0xFF);
                        int i;
                        List<Integer> list = new ArrayList<Integer>();
                        while (true) {
                                bis.read(one_int16);
                                int low = (int)one_int16[0] & 0xFF;
                                int high = (int)one_int16[1] & 0xFF;
                                i = high * 256 + low;
                                if (i == 0)
                                        break;
                                list.add(i-1);
                        }
                        index.put(buckwalterToArabic(c), list);
                }
                bis.close();
        }
        
        private static char buckwalterToArabic(char bw) {
                final String arabic = "ابتثجحخدذرزسشصضطظعغفقكلمنهويءإأؤئآٱٰىًٌٍَُِّْةـ";
                final String buckwalter = "AbtvjHxd*rzs$SDTZEgfqklmnhwy'IOW}|{`YauiFNK~op_";
                int index = buckwalter.indexOf(bw);
                if (index >= 0)
                        return arabic.charAt(index);
                return bw;
        }
        
        private static boolean isDiacritic(char c) {
                final String diacritics = "ًٌٍَُِّْٰ";
                return diacritics.indexOf(c) >= 0;
        }

        private static boolean isAlif(char c) {
                final String alifs = "اإأآٱ";
                return alifs.indexOf(c) >= 0;
        }
        
        private static boolean isArabic(char c) {
                final String arabic = "ابتثجحخدذرزسشصضطظعغفقكلمنهويءإأؤئآٱٰىًٌٍَُِّْةـ";
                return arabic.indexOf(c) >= 0;
        }
        
        private static boolean isJustDiacritics(String str) {
                for (char c : str.toCharArray()) {
                        if (!isDiacritic(c))
                                return false;
                }
                return true;
        }
        
        private static boolean isPrefixFrom(String prefix, String str, int pi, int si) {
                if (prefix.length() == pi)
                        return true;
                if (str.length() == si)
                        return isJustDiacritics(prefix);
                if (prefix.charAt(pi) == str.charAt(si) ||
                                (prefix.charAt(pi) == 'ا' && isAlif(str.charAt(si))))
                        return isPrefixFrom(prefix, str, pi + 1, si + 1);
                if (!isDiacritic(prefix.charAt(pi)) && isDiacritic(str.charAt(si)))
                        return isPrefixFrom(prefix, str, pi, si + 1);
                return false;
        }
        
        private static boolean isPrefix(String prefix, String str) {
                return isPrefixFrom(prefix, str, 0, 0);
        }
        
        private static boolean isMatching(String substr, String str) {
                if (substr.endsWith(".")) {
                        String real_substr = substr.substring(0, substr.length() - 1);
                        return str.toLowerCase().contains(real_substr.toLowerCase());
                }
                if (substr.endsWith("-")) {
                        String real_substr = substr.substring(0, substr.length() - 1);
                        return str.matches("(?i).*\\b" + real_substr + ".*");
                }
                return str.matches("(?i).*\\b" + substr + "\\b.*");
        }
        
        private List<Integer> findArabicWords(String str) {
                List<Integer> starters = index.get(str.charAt(0));
                List<Integer> result = new ArrayList<Integer>();
                if (str.length() == 1 || starters == null)
                        return result;
                for (int i : starters) {
                        if (isPrefix(str, dictionary.get(i)[1]))
                                result.add(i);
                }
                return result;
        }
        
        private List<Integer> findEnglishWords(String str) {
                List<Integer> result = new ArrayList<Integer>();
                if (str.length() < 3)
                        return result;
                for (int i = 0; i < dictionary.size(); ++i) {
                        if (isMatching(str, dictionary.get(i)[0]))
                                result.add(i);
                }
                return result;
        }
        
        private List<Integer> findWords(String str) {
                if (str.length() == 0)
                        return new ArrayList<Integer>();
                if (isArabic(str.charAt(0)))
                        return findArabicWords(str);
                return findEnglishWords(str);
        }
        
        private String formatMatches(List<Integer> words) {
                String header = ""
                                + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n"
                                + "<html><head>\n"
                                + "<style type=\"text/css\"><!--\n"
                                + "table { width: \"90%\" }\n"
                                + ".english { font-family: \"EnglishFont\"; text-align: \"left\" }\n"
                                + ".arabic  { font-family: \"ArabicFont\"; text-align: \"right\" }\n"
                                + "--></style></head>\n"
                                + "<body><table>\n";
                StringBuilder result = new StringBuilder(header);
                for (int i : words) {
                        result.append("<tr><td class=\"english\">");
                        result.append(dictionary.get(i)[0]);
                        result.append("</td><td class=\"arabic\">");
                        result.append(dictionary.get(i)[1]);
                        result.append("</td></tr>\n");
                }
                result.append("</table></body></html>");
                return result.toString();
        }
        
        private void changeFontSizeBy(float x)
        {
                arabic_font = arabic_font.deriveFont(arabic_font.getSize() * x);
                english_font = english_font.deriveFont(english_font.getSize() * x);
                search.setFont(arabic_font);
                entries.updateUI();
        }
        
        private class FontedDocument extends HTMLDocument
        {
                private static final long serialVersionUID = 1L;

                @Override
                public Font getFont(AttributeSet attr) {
                        Object family = attr.getAttribute(StyleConstants.FontFamily);
                        if (family != null && family.equals("ArabicFont"))
                                return arabic_font;
                        return english_font;
                }
        }
        
        private class MenuListener implements ActionListener
        {
                @Override
                public void actionPerformed(ActionEvent event) {
                        JMenuItem item = (JMenuItem)event.getSource();
                        if (item.getText().equals("Smaller text")) {
                                changeFontSizeBy(1.0f/1.2f);
                        } else if (item.getText().equals("Larger text")) {
                                changeFontSizeBy(1.2f);
                        } else {
                                String text = ""
                                                + "Arabic-English / English-Arabic Dictionary\n"
                                                + "           by Peter Salvi, 2013\n"
                                                + "\n"
                                                + "(based on the \"Buckwalter Arabic Wordlist\"\n"
                                                + " acquired from the Perseus Digital Library)\n"
                                                + "\n"
                                                + "Arabic => English:\n"
                                                + "- searches for words starting with the given text\n"
                                                + "  (at least 2 characters)\n"
                                                + "- alif (ا) also stands for أ/إ/آ/ٱ (but you can be specific)\n"
                                                + "- tashkeel can be omitted (but counts if given)\n"
                                                + "- verbs are given in dictionary form (past tense)\n"
                                                + "\n"
                                                + "English => Arabic:\n"
                                                + "- searches for whole words (at least 3 characters)\n"
                                                + "- case insensitive\n"
                                                + "- with a final hyphen (-) it treats the word as a prefix\n"
                                                + "- with a final dot (.) it searches for every occurrence";
                                JOptionPane.showMessageDialog(null, text);
                        }
                }       
        }
        
        private class Scroller implements Runnable
        {
                @Override
                public void run() {
                        entries.scrollRectToVisible(new Rectangle());
                }
        }
        
        private class WordFinder extends SwingWorker<String, Void>
        {
                long wait;
                String search_str;
                
                public WordFinder(String search_str, long wait) {
                        this.search_str = search_str;
                        this.wait = wait;
                }
                
                @Override
                protected String doInBackground() {
                        try {
                                Thread.sleep(wait);
                        } catch (InterruptedException e) {
                        }
                        if (isCancelled())
                                return "";
                        List<Integer> indices = findWords(search_str);
                        if (isCancelled())
                                return "";
                        String text = formatMatches(indices);
                        return text;
                }
                
                @Override
                protected void done() {
                        if (isCancelled())
                                return;
                        try {
                                entries.setText(get());
                                SwingUtilities.invokeLater(new Thread(new Scroller()));
                        } catch (InterruptedException | ExecutionException e) {
                        }
                }
        }

        private class SearchDocListener implements DocumentListener
        {
                public void changed() {
                        if (worker != null)
                                worker.cancel(true);
                        worker = new WordFinder(search.getText(), 300);
                        worker.execute();
                }

                @Override
                public void changedUpdate(DocumentEvent event) {
                        changed();
                }

                @Override
                public void insertUpdate(DocumentEvent event) {
                        changed();
                }

                @Override
                public void removeUpdate(DocumentEvent event) {
                        changed();
                }       
        }
        
        private class SearchListener implements ActionListener
        {               
                @Override
                public void actionPerformed(ActionEvent event) {
                        search.setSelectionStart(0);
                        search.setSelectionEnd(search.getText().length());
                }
        }
        
        public static void main(String[] args) {
                JFrame frame;
                try {
                        frame = new ArabicDictionary();
                        frame.setVisible(true);
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }
}