package org.incava.text;

import java.io.*;
import java.util.*;
import org.incava.io.FileExt;
import org.incava.log.Log;


/**
 * Calculates the edit distance between two strings.
 */
public class SpellChecker
{
    public static final int DEFAULT_MAX_DISTANCE = 4;

    protected static final int COMP_LEN = 20;

    protected static final int ARR_SIZE = COMP_LEN + 1;

    private List words = new ArrayList();

    /**
     * Computes the Levenstein edit distance between the two words, with a
     * maximum of 3, at which point the distance is no longer computed.
     */
    public int editDistance(String str1, String str2)
    {
        return editDistance(str1, str2, 3);
    }

    /**
     * Computes the Levenstein edit distance between the two words.
     */
    public int editDistance(String str1, String str2, int maximum)
    {
        int len1 = Math.min(str1.length(), COMP_LEN);
        int len2 = Math.min(str2.length(), COMP_LEN);
        
        // A minimum threshold of three is used for better results with short
        // strings (A modification to the original C code.)
        
        int threshold = Math.max(maximum, (int)Math.floor((double)1 + (len1 + 2) / 4.0));
        
        int diff = Math.abs(len1 - len2);
        if (diff > threshold) {
            return -1 * diff;
        }
        else {
            return compare(str1, len1, str2, len2);
        }
    }

    public boolean nearMatch(String str1, String str2)
    {
        int edist = editDistance(str1, str2);
        
        // the edit distance is misleading for very short words, so it must be
        // no more than the length of either word:
        return edist >= 0 && edist <= DEFAULT_MAX_DISTANCE && edist < str1.length() && edist < str2.length();
    }

    /**
     * Adds the given dictionary. Returns whether it could be read and had content.
     */
    public boolean addDictionary(String dictionary)
    {
        Log.log("adding dictionary: " + dictionary);
        String[] contents = FileExt.readFile(dictionary, new String[0]);
        if (contents == null || contents.length == 0) {
            return false;
        }
        else {
            for (int i = 0; i < contents.length; ++i) {
                // Log.log("adding word: '" + contents[i] + "'");
                addWord(contents[i]);
            }
            return true;
        }
    }

    public void addWord(String word)
    {
        if (word.length() > 0) {
            words.add(word);
        }
    }

    public boolean hasWord(String word)
    {
        return words.contains(word);
    }

    public boolean isCorrect(String word, int maxEditDistance, List nearMatches)
    {
        if (hasWord(word)) {
            return true;
        }
        else if (nearMatches != null) {
            char[] wordChars = word.toCharArray();
            int wordLen = wordChars.length;
        
            Iterator it = words.iterator();
            while (it.hasNext()) {
                String w = (String)it.next();

                if (w.length() == 0) {
                    Log.stack("w is empty");
                }
                
                if (wordChars[0] == w.charAt(0) || wordChars[0] == w.charAt(0)) {
                    int ed = editDistance(word, w, maxEditDistance);
                    if (ed >= 0 && ed <= maxEditDistance) {
                        // LOGF(1, "adding '%s' as a near match", s.c_str());
                        while (ed >= nearMatches.size()) {
                            nearMatches.add(new ArrayList());
                        }
                        List matches = (List)nearMatches.get(ed);
                        // Log.log("adding " + ed + " => '" + w + "'");
                        matches.add(w);
                    }
                }
            }
        }
        return false;
    }
    
    public boolean isCorrect(String word, List nearMatches)
    {
        return isCorrect(word, DEFAULT_MAX_DISTANCE, nearMatches);
    }

    /**
     * Compares the two characters. English words should probably be case
     * insensitive; code should not.
     */
    protected int compare(String str1, int len1, String str2, int len2)
    {
        final int ADDITION = 1;
        final int CHANGE   = 2;
        final int DELETION = 1;

        int[][] distance = new int[ARR_SIZE][ARR_SIZE];
        distance[0][0] = 0;
    
        for (int j = 1; j < ARR_SIZE; ++j) {
            distance[0][j] = distance[0][j - 1] + ADDITION;
            distance[j][0] = distance[j - 1][0] + DELETION;
        }
    
        for (int i = 1; i <= len1; ++i) {
            for (int j = 1; j <= len2; ++j) {
                distance[i][j] = min3(distance[i - 1][j - 1] + (str1.charAt(i - 1) == str2.charAt(j - 1) ? 0 : CHANGE),
                                      distance[i][j - 1] + ADDITION,
                                      distance[i - 1][j] + DELETION);
            }
        }
        
        return distance[len1][len2];
    }

    protected static int min3(int x, int y, int z) 
    {
        return (x < y) ? (x < z ? x : z) : (y < z ? y : z);
    }

}
