
// IASearch.java
// Andrew Davison, ad@coe.psu.ac.th, July 2023

/* Search for texts using the REST API of
   the Internet Archive advanced search:
        https://archive.org/advancedsearch.php

  Usage: IASearch [-dhiV] [-c=<creatorWords>] [-m=<maxMatches>]
                  [-s=<subjectWords>] [-t=<titleWords>] [-v=<viewNum>]
  Searches the Internet Archive for texts.
    -c, --creator=<creatorWords>
                               Search using creator keywords.
    -d, --downloadable         Only show downloadable matches.
    -h, --help                 Show this help message and exit.
    -i, --info                 Brief text info.
    -m, --max=<maxMatches>     Max no. of matches (default = 50).
    -s, --subject=<subjectWords>
                               Search using subject keywords.
    -t, --title=<titleWords>   Search using title keywords.
    -v, --view=<viewNum>       View the numbered Download webpage.
    -V, --version              Print version information and exit.


  Examples:
    > compile IASearch.java

    > run IASearch
    > run IASearch -c "rouse ball"
    > run IASearch -t "mathematical recreation*"
    > run IASearch -s "history mathematics*"
    > run IASearch -c "martin gardner" -s puzzle* -di

    > run IASearch -t "mathematics million*" -d
    > run IASearch -t "mathematics million*" -v 8

    > run IASearch -c "donald knuth" -d

    > run IASearch -t algorithms -c sedgewick

    > run IASearch -c "ian fleming" -s bond
    > run IASearch -c "ian fleming" -s bond -t diamonds -d
    > run IASearch -c "ian fleming" -s bond -v 89 -i

    > run IASearch -t "games magazine 50" -v1

    > run IASearch -t "best games" -m 30
    > run IASearch -t "best puzzles" -m 30 -d -v 20
*/

import java.util.*;
import java.util.concurrent.*;

import picocli.CommandLine;
import picocli.CommandLine.*;
import picocli.CommandLine.Model.CommandSpec;

import com.google.gson.Gson;



@Command(name = "IASearch", 
         mixinStandardHelpOptions = true,
         version = "IASearch 1.0",
         description = "Searches the Internet Archive for texts.")
public class IASearch implements Callable<Integer>
{

  private static final int MAX_MATCHES = 50;

  private static final String DOWN_LINK = "http://www.archive.org/download/";
     /* add a doc's identifier to this string to get 
        the URL for the download page */


  // variables that represent the command line arguments 
  @Option(names = {"-c", "--creator"},
          description = "Search using creator keywords.")
  private String creatorWords = null; 

  @Option(names = {"-t", "--title"},
          description = "Search using title keywords.")
  private String titleWords = null;

  @Option(names = {"-s", "--subject"},
          description = "Search using subject keywords.")
  private String subjectWords = null;

  @Option(names = { "-d", "--downloadable" }, 
          description = "Only show downloadable matches.")
  private boolean showDowns = false;

  @Option(names = { "-i", "--info" }, 
          description = "Brief text info.")
  private boolean showInfo = false;

  @Option(names = { "-v", "--view" }, 
          description = "View the numbered Download webpage.")
  private int viewNum = -1;

  @Option(names = { "-m", "--max" }, 
          description = "Max no. of matches (default = " +
                                       MAX_MATCHES + ").")
  private int maxMatches = MAX_MATCHES;


  @Spec
  private CommandSpec spec;


  public Integer call()
  // called by Picocli with the command variables initialized
  { 
    String query = buildQuery();
    if (query.equals("")) {   // show help instead
      spec.commandLine().usage(System.out);
      return 0;
    }

    if ((maxMatches <= 0) || (maxMatches > MAX_MATCHES)) {
      System.out.println("Max matches out of range; using " + MAX_MATCHES);
      maxMatches = MAX_MATCHES;
    }

    String request = 
      "https://archive.org/advancedsearch.php?q=" + query +
      "mediatype:(texts)+language:(eng*)&" +      // eng or English
      "fl[]=identifier&fl[]=year&fl[]=format&" +
      "fl[]=title&fl[]=creator&fl[]=publisher&" +
      "fl[]=subject&&fl[]=description&" +
      "sort[]=year:desc&" +            // ordered by descending year
      "rows=" + maxMatches + "&page=1&output=json";

    /* Each of the fl[] parameters should have a corrresponding field
       in the Doc class in InternetArchive.java */

    String response = HTTPUtils.getResponse(request);
    if (response == null)
      return 1;

    // HTTPUtils.prettyPrint(response);   // useful for debugging
    InternetArchive ia = new Gson().fromJson(response, 
                                             InternetArchive.class);
        // map JSON data to Java objects

    int status = ia.responseHeader.status;
    if (status != 0)  {
      System.out.println("IA Query Status error: " + status);
      return 1;
    }
   
    int numFound = ia.response.numFound;
    ArrayList<Doc> docs = ia.response.docs;
    System.out.println("No. of matches: " + numFound + "\n");
    if (docs.size() != numFound)
      System.out.println("Only retrieved " + docs.size() + "\n");

    // load web page for a doc or list match info
    if ((viewNum != -1) && isViewable(docs.size())) {
      Doc doc = docs.get(viewNum-1);
      docInfo(viewNum, doc);
      HTTPUtils.showInBrowser(DOWN_LINK + doc.identifier);
    }
    else {
      if (showDowns)
        System.out.println("Showing downloadable matches only\n");
      for(int i=0; i < docs.size(); i++)
        docInfo(i+1, docs.get(i));
    }
    return 0;
  } /* call */



  private String buildQuery()
  // construct the search part of the URL string
  {
    String[] searches = new String[3];

    if (creatorWords != null)
      searches[0] = "creator:(" + HTTPUtils.encode(creatorWords) + ")+";

    if (titleWords != null)
      searches[1] = "title:(" + HTTPUtils.encode(titleWords) + ")+";

    if (subjectWords != null)
      searches[2] = "subject:(" + HTTPUtils.encode(subjectWords) + ")+";

    String query = "";
    for (String srch : searches)
      query += (srch == null) ? "" : srch;   // add nothing if null

    // System.out.println("query: " + query);
    return query;
  }  // end of buildQuery()



  private void docInfo(int viewNum, Doc doc)
  // display the doc information for this number
  {
    List<String> formats = doc.getFormat();
    boolean isDownloadable = isDownloadable(formats);

    if (showDowns && !isDownloadable)
      // don't show this doc since not downloadable
      return;

    System.out.println(viewNum + ". \"" + doc.title + "\"");
    System.out.println("Creator(s): " + 
            Arrays.toString(doc.getCreator().toArray()) );
    
    if (doc.getPublisher() != null)
      System.out.println("Publisher: " + 
            Arrays.toString(doc.getPublisher().toArray()) );

    System.out.println("Year: " + 
                  ((doc.year == 0) ? "----" : doc.year));

    System.out.println("Subjects: " + 
            Arrays.toString(doc.getSubject().toArray()) );

    if (showInfo)
      System.out.println("Info: " + 
            Arrays.toString(doc.getDescription().toArray()) );

    // System.out.println("ID: " + doc.identifier);

    // Collections.sort(formats);
    // System.out.println("Formats: " + Arrays.toString(formats.toArray()));
    System.out.println("Downloadable: " + isDownloadable);
    System.out.println("Webpage: " + DOWN_LINK + doc.identifier);
    System.out.println();
  }  // end of docInfo()


  private static boolean isDownloadable(List<String> formats)
  // examine the formats to determine if the doc can be downloaded
  {
    boolean isDown = false;
    for (String format: formats) {
       format = format.toLowerCase().trim();
       if (format.equals("djvu") || format.equals("epub") ||
           format.equals("html") || format.equals("text") ||
           format.equals("rich text format") ||
           format.equals("text pdf") ||
           format.equals("additional text pdf") ||
           format.equals("image container pdf") ||
           format.equals("grayscale pdf") )
         isDown = true;

       if (format.contains("encrypted"))
         return false;   // this invalidates any other found formats
    }
    return isDown;
  }  // end of isDownloadable()




  private boolean isViewable(int numMatches)
  // can the supplied doc number be viewed?
  {
    if ((viewNum > 0) && (viewNum <= numMatches))  // valid number
      return true;
    else if ((viewNum == 0) || (viewNum > numMatches)) {
      // has a doc number, but it's invalid
      System.out.println("Book number out of range; using 1\n");
      viewNum = 1;
      return true;
    }

    return false;
  }  // end of isViewable()


  // --------------------------------------

  public static void main(String[] args)
  {
    int exitCode = new CommandLine(new IASearch()).execute(args);
    System.exit(exitCode);
  } /* main */


}  // end of IASearch class

