
// GutenSearch.java
// Andrew Davison, ad@coe.psu.ac.th, July 2023

/* Use the Gutendex API (https://gutendex.com/)
   to search Project Gutenberg (https://www.gutenberg.org/),
   and retrieve books (and other resources, such as audio).

   Three types of search are possible: using keywords to search
   the book's title and author fields, using a topic to search 
   in a book's bookshelf and subject fields, and search
   by book ID.

   The retrieved information is listed by default and numbered.

   This number and a media type can be used to download a book
   or display it in the browser (if the file type is HTML or text).

   Another approach is to search by ID with a media type. The
   single match will be listed, and the accommpanying file retrieved.

   If the resource is not a book (e.g. audio, music score) then
   the parent folder of the selected media type is always displayed in
   the browser. This is because such resources usully consist of 
   multiple parts, and its easiest to select the one you want by
   clicking on its link in the browser.

   The command line arguments are processed using the Picocli
   library (https://picocli.info/ and 
   http://coe.psu.ac.th/~ad/party3/picocli/).

   The JSON data returned by HtpClient is converted to Java objects
   using Google GSON (https://github.com/google/gson), with the
   mappings defined in Gutenberg.java.


   Usage: GutenSearch [-hV] [-i=<id>] [-k=<keyWords>] [-m=<media>] [-n=<bookNum>] [-t=<topic>]
   Searches Project Gutenberg using the Gutendex API.
     -h, --help               Show this help message and exit.
     -i, --id=<id>            Search using a book ID
     -k, --keys=<keyWords>    Search using a keywords string
     -m, --media=<media>      Retrieve the media
     -n, --number=<bookNum>   Get details about the nth match
     -t, --topic=<topic>      Search using a topic
     -V, --version            Print version information and exit.

  Examples:
    > compile GutenSearch.java

    > run GutenSearch

    > run GutenSearch -k "dickens great"
    > run GutenSearch -k "dickens great" -n 1 -m epub
    > run GutenSearch -k "dickens great" -n 1 -m html

    > run GutenSearch -k rouse
    > run GutenSearch -k rouse -n 1 -m pdf
    > run GutenSearch -k rouse -n 1 -m octet

    > run GutenSearch -k "quartet in G"
    > run GutenSearch -k "quartet in G" -n 10 -m mpeg

    > run GutenSearch -k "greenmantle" -n 2 -m ogg

    > run GutenSearch -i 10184
    > run GutenSearch -i 10184 -m mpeg

    > run GutenSearch -t puzzles
*/

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 = "GutenSearch", 
         mixinStandardHelpOptions = true,
         version = "GutenSearch 1.0",
         description = "Searches Project Gutenberg using the Gutendex API.")
public class GutenSearch implements Callable<Integer>
{

  // variables used to hold command arguments 
  @Option(names = {"-k", "--keys"},
          description = "Search using a keywords string")
  private String keyWords = null; 

  @Option(names = {"-t", "--topic"},
          description = "Search using a topic")
  private String topic = null;

  @Option(names = {"-i", "--id"},
          description = "Search using a book ID")
  private int id = -1;

  @Option(names = {"-n", "--number"},
          description = "Get details about the nth match")
  private int bookNum = -1;

  @Option(names = {"-m", "--media"},
          description = "Retrieve the media")
  private String media = null;

  @Spec
  private CommandSpec spec;


  public Integer call()
  // called by Picocli with the command variables initialized
  { 
    String query = null;
    if (keyWords != null)
      query = "search=" + HTTPUtils.encode(keyWords);
    else if (topic != null)
      query = "topic=" + HTTPUtils.encode(topic);
    else if (id != -1)
      query = "ids=" + id;
    else { // default help
      spec.commandLine().usage(System.out);
      return 0;
    }

    String request = "https://gutendex.com/books/?" +
                                 query + "&language=en";
  
    // String resp = HTTPUtils.getResponse(request);
    // System.out.println("Response: " + resp);
  
    Gutenberg matches = getBooks(request);
    System.out.println("No. matches: " + matches.count + "\n");
  
    if (matches.books == null) {
      System.out.println("No matches returned");
      return 1;
    }
    int numMatches = matches.books.size();

    if (isRetrieving(numMatches)) {
      Book book = matches.books.get(bookNum-1);
      return retrieveBook(book);
    }
    else {  // list all the matches
      for(int i=0; i < numMatches; i++)
        bookInfo(i+1, matches.books.get(i));
    }

    return 0;
  } /* call */


  private Gutenberg getBooks(String request)
  {
    String response = HTTPUtils.getResponse(request);
    return new Gson().fromJson(response, Gutenberg.class);
        // map JSON data to Java objects
  }




  private boolean isRetrieving(int numMatches)
  // should a book be retrieved?
  {
    if (media != null) {
      if ((bookNum > 0) && (bookNum <= numMatches))  // valid bookNum
        return true;
      else if ((bookNum == 0) || (bookNum > numMatches)) {
        // has a book number, but it's invalid
        System.out.println("Book number out of range; using 1\n");
        bookNum = 1;
        return true;
      }
      else if ((bookNum == -1) && (id != -1)) {
        // no book number, but id use will have returned a single match
        bookNum = 1;
        return true;
      }
    }
    else {  // media is null
      if (bookNum != -1) {
        System.out.println("No media, so will list book number " + 
                                          bookNum + "\n");
        return true;
      }
    }

    return false;
  }  // end of isRetrieving()



  private void bookInfo(int bookNum, Book book)
  {
    System.out.println(bookNum + ". \"" + book.title + "\"");
    System.out.print("Authors: ");
    for(Person auth: book.authors) 
       System.out.print(auth.name + "; ");
    System.out.println();
    System.out.println("ID: " + book.id);

    System.out.println("Subjects: " +
          Arrays.toString(book.subjects.toArray()));
  
    System.out.println("Media type: " + book.media_type);
  
    // list all the initialized media:url links for this book
    TreeMap<String, String> links = book.formats.getLinks();
    for (String key: links.keySet())
      System.out.println("  " + key + ": " + links.get(key));
  
    System.out.println();
  }  // end of bookInfo()


  private int retrieveBook(Book book)
  {
    if (media == null)  // list the book number info
      bookInfo(bookNum, book);
    else {  // media is specified
      media = media.toLowerCase().trim();

      // look in the books's media:url map
      TreeMap<String, String> links = book.formats.getLinks();
      if (!links.containsKey(media)) {
        System.out.println(media + "is not a media type for book " +
                                            bookNum + ":");
        System.out.println("  " + links.keySet());
        return 1;
      }

      bookInfo(bookNum, book);  // list the book
      String mediaURL = links.get(media);
      System.out.println("Retrieving: " + mediaURL);
 
      // either display in browser or download as a file
      if (!book.media_type.equals("Text")) {    // the media isn't text;
        mediaURL = HTTPUtils.getParent(mediaURL);  // show parent dir
        HTTPUtils.showInBrowser(mediaURL);         // in browser
      }
      else if (media.startsWith("txt") || media.startsWith("html"))
        // the book's media type is text or HTML, so display in the browser
        HTTPUtils.showInBrowser(mediaURL);   // show in browser
      else {  // save the resource in a local file
        String fnm = simplifyTitle(book) + "." + 
                                   getExt(mediaURL, media);
        HTTPUtils.download(mediaURL, fnm);
      }
    }
    return 0;
  }  // end of retrieveBook()



  private String simplifyTitle(Book book)
  // remove spaces and truncate if > 30 chars
  {
    String title = book.title.replaceAll(" ", "");
    return ((title.length() > 30) ? title.substring(0,30) : title);
  }  // end of simplifyTitle()


  private String getExt(String urlStr, String media)
  // decide on the extension for the locally saved file
  {
    if (media.equals("octet")) {
      // use the URL's extension rather than "octet"
      int lastIndex = urlStr.lastIndexOf('.');
      if (lastIndex != -1)
        return urlStr.substring(lastIndex+1);
    }

    return media;
  }  // end of getExt()

  // --------------------------------------

  public static void main(String[] args)
  {
    int exitCode = new CommandLine(new GutenSearch()).execute(args);
    System.exit(exitCode);
  } /* main */


}  // end of GutenSearch class

