import java.applet.*;
import javax.mail.*;
import java.security.*;
import com.sun.mail.imap.*;
import java.io.*;
import java.util.*;

/**
 * Create Haiku from a list of phrases, loaded from an IMAP folder or File.
 * Can be run as an applet (if signed) or application.
 *
 * Created by Mike Bremford, 2009-05-18. Use as you like so long as I'm
 * acknowledged as the author.
 */
public class Haiku extends Applet
{
    private Collection fives, sevens;
    private Random random = new Random();

    /**
     * Possibly add a phrase to generator. If the phrase is in latin script
     * and has 5 or 7 syllables it is accepted and this method returns true.
     * @param phrase the phrase
     * @return true if the phrase was accepted, false otherwise
     */
    public boolean addPhrase(String phrase) {
        for (int i=0;i<phrase.length();i++) {
            // Skip if it contains non-latin letters
            char c = phrase.charAt(i);
            int type = Character.getType(c);
            if (type == Character.OTHER_LETTER || Character.isDigit(c) || "@&#".indexOf(c)>=0 || (c>=0x370 && c<=0x900)) {
                return false;
            }
        }
        phrase = phrase.toLowerCase();
        phrase = phrase.trim().replaceAll("\\s+", " ");
        String[] words = getWords(phrase);
        int num = 0;
        for (int i=0;i<words.length;i++) {
            num += getNumSyllables(words[i]);
        }
        if (num==5) {
            return fives.add(phrase);
        } else if (num==7) {
            return sevens.add(phrase);
        } else {
            return false;
        }
    }

    /**
     * Return the number of unique Haiku that can be generated
     */
    public int getCount() {
        return Math.max(0, fives.size() * sevens.size() * (fives.size()-1));
    }

    /**
     * Return a specific Haiku
     */
    public String getHaiku(final int i) {
        try {
            if (i >= getCount()) {
                throw new IllegalArgumentException(i+" outside range 0-"+(getCount()-1));
            }
            if (!(fives instanceof List)) {
                if (fives.size()==1 || sevens.size()==0) {
                    throw new IllegalStateException("Not enough phrases");
                }
                fives = new ArrayList(fives);
                sevens = new ArrayList(sevens);
            }
            int n1 = i;
            int n3 = n1 % (fives.size()-1);
            n1 /= (fives.size()-1);
            int n2 = n1 % sevens.size();
            n1 /= sevens.size();
            if (n3>=n1) n3++;
            Object p1 = ((List)fives).get(n1);
            Object p2 = ((List)sevens).get(n2);
            Object p3 = ((List)fives).get(n3);
            String haiku = p1 + "\n" + p2 + "\n" + p3;
//            haiku += " (i="+i+" n1="+n1+"/"+fives.size()+" n2="+n2+"/"+sevens.size()+" n3="+n3+"/"+fives.size()+")";
            return haiku;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * Return a random Haiku
     */
    public String getRandomHaiku() {
        if (getCount()==0) {
           return "lacking in data\ni have no useful results\nhaiku disappoints?";
        } else {
            return getHaiku(random.nextInt(getCount()));
        }
    }

    /**
     * Given a one-line phrase, split it into individual words
     */
    private static String[] getWords(String phrase) {
        phrase = phrase.replaceAll("'", "");
        phrase = phrase.replaceAll("[^A-Za-z]", " ");
        return phrase.split(" +");
    }

    /**
     * Algorithm from Dr. Rudolph Flesch (1949)
     * http://portal.acm.org/citation.cfm?id=10583
     */
    private static int getNumSyllables(String word) {
        int count = 0;
        if (word.length()>2) {
            if (word.endsWith("ed") || word.endsWith("es")) {
                word = word.substring(0, word.length()-2);
            } else if (word.endsWith("e") && !word.endsWith("le")) {
                word = word.substring(0, word.length()-1);
            }
        }
        boolean lastvowel = false;
        for (int i=0;i<word.length();i++) {
            char c = word.charAt(i);
            boolean vowel = "aeiouy".indexOf(c) >= 0;
            if (vowel && !lastvowel) {
                count++;
            }
            lastvowel = vowel;
        }
        if (count==0) {
            count= 1;
        }
        return count;
    }

    /**
     * Load the like of phrases from the specified File, which should have a list
     * of lines containing 5 or 7 syllables each (any that don't are ignored).
     * @param file the File to load the phrases from 
     */
    public void loadFromFile(File file) throws IOException {
        if (!(fives instanceof Set)) {
            fives = new HashSet();
            sevens = new HashSet();
        }
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(file));
            String s;
            while ((s=reader.readLine())!=null) {
                addPhrase(s);
            }
        } finally {
            try { if (reader!=null) reader.close(); } catch (Exception e) {}
        }
    }

    /**
     * Load phrases from the Subject field of the specified IMAP URL. This method
     * is privileged, and so can be called from a digitally-signed Applet.
     * @param urlstring the URL to connect to
     * @param max the number of messages to load (0 for all of them, which can be slow)
     */
    public void loadFromIMAP(final String urlstring, final int max) throws Exception {
        Exception e = (Exception)AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                try {
                    loadFromIMAP(urlstring, max, null);
                } catch (Exception e) {
                    return e;
                }
                return null;
            }
        });
        if (e!=null) {
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * Load phrases from the Subject field of the specified IMAP URL
     * @param urlstring the URL to connect to
     * @param max the number of messages to load (0 for all of them, which can be slow)
     * @param file if not null, the File to write the header responses to
     */
    public void loadFromIMAP(final String urlstring, final int max, final File file) throws Exception {
        if (!(fives instanceof Set)) {
            fives = new HashSet();
            sevens = new HashSet();
        }

        Properties props = new Properties();
        URLName url = new URLName(urlstring);
        if ("imaps".equals(url.getProtocol())) {
            url = new URLName("imap", url.getHost(), url.getPort(), url.getFile(), url.getUsername(), url.getPassword());
            props.setProperty("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            props.setProperty("mail.imap.socketFactory.fallback", "false");
            props.setProperty("mail.imap.port",  "993");
            props.setProperty("mail.imap.socketFactory.port", "993");
        }
        Session session = Session.getInstance(props);
        Store store = new IMAPStore(session, url);
        PrintStream out = null;

        try {
            store.connect();
            IMAPFolder folder = (IMAPFolder)store.getFolder(url);
            folder.open(Folder.READ_ONLY);

            Message[] msgs = folder.getMessages();
            System.out.print("Reading "+(max==0 ? "all" : Integer.toString(Math.min(msgs.length, max)))+" of "+msgs.length+" messages from \""+url.getFile()+"\"...");
            if (max!=0 && max < msgs.length) {
                List l = new ArrayList(Arrays.asList(msgs));
                Collections.shuffle(l);
                Message[] newmsgs = new Message[max];
                for (int i=0;i<max;i++) {
                    newmsgs[i] = (Message)l.get(i);
                }
                msgs = newmsgs;
            }
            FetchProfile profile = new FetchProfile();
            profile.add(FetchProfile.Item.ENVELOPE);
            folder.fetch(msgs, profile);
            System.out.println(" done");

            if (file!=null) {
                out = new PrintStream(new FileOutputStream(file), true);
            }
            for (int i=0;i<msgs.length;i++) {
                try {
                    Message msg = msgs[i];
                    String subject = msg.getSubject();
                    if (subject!=null) {
                        if (out!=null) {
                            out.println(subject);
                        }
                        addPhrase(subject);
                    }
                } catch (Exception e) {}
            }
        } finally {
            try { if (out!=null) out.close(); } catch (Exception e) {}
            try { store.close(); } catch (Exception e) {}
        }
    }

    public static void main(String[] args) throws Exception {
        int num = 1;
        int max = 0;
        String url = null;
        File file = null;
        for (int i=0;i<args.length;i++) {
            if (args[i].equals("-f")) {
                file = new File(args[++i]);
            } else if (args[i].equals("-i")) {
                url = args[++i];
            } else if (args[i].equals("-m")) {
                max = Integer.parseInt(args[++i]);
            } else if (args[i].equals("-n")) {
                num = Integer.parseInt(args[++i]);
            }
        }
        Haiku haiku = new Haiku();
        if (url!=null) {
            haiku.loadFromIMAP(url, max, file);
        } else if (file!=null) {
            haiku.loadFromFile(file);
        } else {
            System.out.println("Usage: java Haiku [-f filename] [-i imapurl] [-n number]");
            System.out.println("       Specify both an imapURL and a filename to save the Subject headers to that file");
            System.out.println("       eg: java Haiku -i 'imap://test%40gmail.com:password@imap.gmail.com/[Google Mail]/Spam' -f headers.txt");
            System.out.println("           java Haiku -f headers.txt");
            System.exit(0);
        }
        int count = haiku.getCount();
        System.out.println("There are "+count+" possible unique haiku");
        if (num > count) {
            num = count;
        }
        for (int i=0;i<num;i++) {
            System.out.println((num==count ? haiku.getHaiku(i) : haiku.getRandomHaiku())+"\n");
        }
    }
}
