<%@ Page Language="C#" EnableViewState="False" %>

<script runat="server">
//=============================================================================
// System  : Sandcastle Help File Builder
// File    : SearchHelp.aspx
// Author  : Eric Woodruff  (Eric@EWoodruff.us)
// Updated : 07/03/2007
// Note    : Copyright 2007, Eric Woodruff, All rights reserved
// Compiler: Microsoft C#
//
// This file contains the code used to search for keywords within the help
// topics using the full-text index files created by the help file builder.
//
// This code is published under the Microsoft Public License (Ms-PL).  A copy
// of the license should be distributed with the code.  It can also be found
// at the project website: http://www.CodePlex.com/SHFB.   This notice, the
// author's name, and all copyright notices must remain intact in all
// applications, documentation, and source files.
//
// Version     Date     Who  Comments
// ============================================================================
// 1.5.0.0  06/24/2007  EFW  Created the code
//=============================================================================

private class Ranking
{
    public string Filename, PageTitle;
    public int Rank;
    
    public Ranking(string file, string title, int rank)
    {
        Filename = file;
        PageTitle = title;
        Rank = rank;
    }
}    

/// <summary>
/// Render the search results
/// </summary>
/// <param name="writer">The writer to which the results are written</param>
protected override void Render(HtmlTextWriter writer)
{
    FileStream fs = null;
    BinaryFormatter bf;
    string searchText, ftiFile;
    char letter;
    bool sortByTitle = false;

    // The keywords for which to search should be passed in the query string
    searchText = this.Request.QueryString["Keywords"];

    if(String.IsNullOrEmpty(searchText))
    {
        writer.Write("<b class=\"PaddedText\">Nothing found</b>");
        return;
    }

    // An optional SortByTitle option can also be specified
    if(this.Request.QueryString["SortByTitle"] != null)
        sortByTitle = Convert.ToBoolean(this.Request.QueryString["SortByTitle"]);

    List<string> keywords = this.ParseKeywords(searchText);
    List<char> letters = new List<char>();
    List<string> fileList;
    Dictionary<string, List<long>> ftiWords, wordDictionary =
        new Dictionary<string,List<long>>();

    try
    {
        // Load the file index
        fs = new FileStream(Server.MapPath("fti/FTI_Files.bin"), FileMode.Open,
            FileAccess.Read);
        bf = new BinaryFormatter();
        fileList = (List<string>)bf.Deserialize(fs);
        fs.Close();

        // Load the required word index files
        foreach(string word in keywords)
        {
            letter = word[0];
            
            if(!letters.Contains(letter))
            {
                letters.Add(letter);
                ftiFile = Server.MapPath(String.Format(
                    CultureInfo.InvariantCulture, "fti/FTI_{0}.bin", (int)letter));

                if(File.Exists(ftiFile))
                {
                    fs = new FileStream(ftiFile, FileMode.Open, FileAccess.Read);
                    ftiWords = (Dictionary<string, List<long>>)bf.Deserialize(fs);
                    fs.Close();

                    foreach(string ftiWord in ftiWords.Keys)
                        wordDictionary.Add(ftiWord, ftiWords[ftiWord]);
                }
            }
        }
    }
    finally
    {
        if(fs != null && fs.CanRead)
            fs.Close();
    }

    // Perform the search and return the results as a block of HTML
    writer.Write(this.Search(keywords, fileList, wordDictionary, sortByTitle));
}

/// <summary>
/// Split the search text up into keywords
/// </summary>
/// <param name="keywords">The keywords to parse</param>
/// <returns>A list containing the words for which to search</returns>
private List<string> ParseKeywords(string keywords)
{
    List<string> keywordList = new List<string>();
    string checkWord;
    string[] words = Regex.Split(keywords, @"\W+");

    foreach(string word in words)
    {
        checkWord = word.ToLower(CultureInfo.InvariantCulture);
        
        if(checkWord.Length > 2 && !Char.IsDigit(checkWord[0]) &&
          !keywordList.Contains(checkWord))
            keywordList.Add(checkWord);
    }

    return keywordList;
}

/// <summary>
/// Search for the specified keywords and return the results as a block of
/// HTML.
/// </summary>
/// <param name="keywords">The keywords for which to search</param>
/// <param name="fileInfo">The file list</param>
/// <param name="wordDictionary">The dictionary used to find the words</param>
/// <param name="sortByTitle">True to sort by title, false to sort by
/// ranking</param>
/// <returns>A block of HTML representing the search results.</returns>
private string Search(List<string> keywords, List<string> fileInfo,
    Dictionary<string, List<long>> wordDictionary, bool sortByTitle)
{
    StringBuilder sb = new StringBuilder(10240);
    Dictionary<string, List<long>> matches = new Dictionary<string, List<long>>();
    List<long> occurrences;
    List<int> matchingFileIndices = new List<int>(),
        occurrenceIndices = new List<int>();
    List<Ranking> rankings = new List<Ranking>();

    string filename, title;
    string[] fileIndex;
    bool isFirst = true;
    int idx, wordCount, matchCount;

// TODO: Support boolean operators (AND, OR and maybe NOT)
        
    foreach(string word in keywords)
    {
        if(!wordDictionary.TryGetValue(word, out occurrences))
            return "<b class=\"PaddedText\">Nothing found</b>";

        matches.Add(word, occurrences);
        occurrenceIndices.Clear();

        // Get a list of the file indices for this match
        foreach(long entry in occurrences)
            occurrenceIndices.Add((int)(entry >> 16));
            
        if(isFirst)
        {
            isFirst = false;
            matchingFileIndices.AddRange(occurrenceIndices);
        }
        else
        {
            // After the first match, remove files that do not appear for
            // all found keywords.
            for(idx = 0; idx < matchingFileIndices.Count; idx++)
                if(!occurrenceIndices.Contains(matchingFileIndices[idx]))
                {
                    matchingFileIndices.RemoveAt(idx);
                    idx--;
                }
        }
    }

    if(matchingFileIndices.Count == 0)
        return "<b class=\"PaddedText\">Nothing found</b>";

    // Rank the files based on the number of times the words occurs
    foreach(int index in matchingFileIndices)
    {
        // Split out the title, filename, and word count
        fileIndex = fileInfo[index].Split('\x0');

        title = fileIndex[0];
        filename = fileIndex[1];
        wordCount = Convert.ToInt32(fileIndex[2]);
        matchCount = 0;
        
        foreach(string word in keywords)
        {
            occurrences = matches[word];

            foreach(long entry in occurrences)
                if((int)(entry >> 16) == index)
                    matchCount += (int)(entry & 0xFFFF);
        }

        rankings.Add(new Ranking(filename, title, matchCount * 1000 / wordCount));
    }

    // Sort by rank in descending order or by page title in ascending order
    rankings.Sort(
        delegate(Ranking x, Ranking y)
        {
            if(!sortByTitle)
                return y.Rank - x.Rank;

            return x.PageTitle.CompareTo(y.PageTitle);
        });

    // Format the file list and return the results
    foreach(Ranking r in rankings)
        sb.AppendFormat("<div class=\"TreeItem\">\r\n<img src=\"Item.gif\"/>" +
            "<a class=\"UnselectedNode\" target=\"TopicContent\" " +
            "href=\"{0}\" onclick=\"javascript: SelectSearchNode(this);\">" +
            "{1}</a>\r\n</div>\r\n", r.Filename, r.PageTitle);

    // Return the keywords used as well in a hidden span
    sb.AppendFormat("<span id=\"SearchKeywords\" style=\"display: none\">{0}</span>",
        String.Join(" ", keywords.ToArray()));

    return sb.ToString();
}

</script>