import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

/**
 * A program that uses a Map from Strings to Sets of Integers to do efficient
 * searches of a text file.  Sadly, the search is for a single word at a time.
 * Try it on THIS file!
 * 
 * @author Mark Young (A00000000)
 */
public class FileSearcher {

    private static final Scanner KBD = new Scanner(System.in);

    public static void main(String[] args) {
        printIntroduction();
        List<String> lines = readLines();
        Map<String, Set<Integer>> locations = makeLocationMap(lines);
        search(lines, locations);
    }

    /**
     * Tell user what you do.
     */
    private static void printIntroduction() {
        System.out.println("\n"
                + "This program lets you search a file.\n");
    }

    /**
     * Get the file and read its lines.
     */
    private static List<String> readLines() {
        // create required variables
        String fileName;
        List<String> contents = new ArrayList<>();

        // get file name
        System.out.print("Enter a file name: ");
        fileName = KBD.nextLine();
        
        // try to read the file
        try (Scanner file = new Scanner(new File(fileName))) {
            
            // file exists: read it
            while (file.hasNextLine()) {
                contents.add(file.nextLine());
            }
        } catch (FileNotFoundException fnf) {
            
            // file not found: exit program
            System.err.println("\nSorry, I can't open that file.");
            System.exit(1);
        }

        return contents;
    }

    /**
     * Make a map showing where each word is. The map generated uses each word
     * present in the list as a key to a set of line numbers. The line numbers 
     * correspond to the position in the given list. If a line number is in the
     * set, then the word is on that line.
     *
     * The map is case insensitive, with the lower-case version of the word
     * being used as the key.
     *
     * Lines are split on white-space and punctuation.
     *
     * NOTE: because the lines are split on punctuation, contractions are split
     * into two parts -- so "doesn't" gets recorded as two words, "doesn" and
     * "t". An exercise for the reader is to come up with a plan to replace
     * contractions in a way that'd allow them to be handled better.
     *
     * @param lines the lines to make the map from.
     * @return a map from words to the numbers of the lines it appears on.
     */
    private static Map<String, Set<Integer>> makeLocationMap(
            List<String> lines
    ) {
        // create required variables
        Map<String, Set<Integer>> result = new HashMap<>();
        
        // for each line in the file (saved in list of lines)
        for (int lineNumber = 0; lineNumber < lines.size(); ++lineNumber) {
            
            // get the line
            String line = lines.get(lineNumber);
            
            // break the line up on spaces and punctuation
            for (String word : line.split("[\\s\\p{Punct}]+")) {
                
                // save word in loswer case (for case insensitive search)
                word = word.toLowerCase();
                if (result.containsKey(word)) {
                    // we've already seen this word before: add to its set
                    result.get(word).add(lineNumber);
                } else {
                    // haven't seen this word before: make a new set
                    Set<Integer> s = new TreeSet<>();
                    s.add(lineNumber);
                    result.put(word, s);
                }
            }
        }
        return result;
    }

    /**
     * Repeatedly look for a word in the file.
     * 
     * @param lines the list of lines from the file.
     * @param locations the map from words to line numbers.
     */
    private static void search(
            List<String> lines,
            Map<String, Set<Integer>> locations
    ) {
        // create required variables
        String searchWord;
        Set<Integer> lineNumbers;
        
        // get the search word (ONE WORD ONLY!)
        System.out.print("\nEnter the word you'd like to find: ");
        searchWord = KBD.nextLine();
        
        // quit when there is no word
        while (!searchWord.equals("")) {
            
            // switch to lower case (for case insensitive search)
            searchWord = searchWord.toLowerCase();
            
            // check to see if it's there
            if (locations.containsKey(searchWord)) {
                
                // it's there: print out the lines (with numbers)
                System.out.println("Your word appears in these lines: ");
                lineNumbers = locations.get(searchWord);
                for (int lineNo : lineNumbers) {
                    System.out.printf("%6d: %s%n",
                            lineNo, lines.get(lineNo));
                }
            } else {
                
                // not there: report failure
                System.out.println("Your word doesn't appear in any line.");
            }

            // get next search word
            System.out.print("\nEnter the word you'd like to find: ");
            searchWord = KBD.nextLine();
        }
    }

}
