package com.gmail.dpierron.calibre.opds;

/**
 * Abstract class containing functions and variables common to all catalog types
 */

import com.gmail.dpierron.calibre.cache.CachedFile;
import com.gmail.dpierron.calibre.cache.CachedFileManager;
import com.gmail.dpierron.calibre.configuration.ConfigurationHolder;
import com.gmail.dpierron.calibre.configuration.ConfigurationManager;
import com.gmail.dpierron.calibre.datamodel.*;
import com.gmail.dpierron.tools.i18n.Localization;
import com.gmail.dpierron.tools.Helper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Document;
import org.jdom2.Element;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.*;
import java.util.zip.CRC32;

public abstract class SubCatalog {
  // cache some widely used objects.
  private final static Logger logger = LogManager.getLogger(SubCatalog.class);
  // Get some non-mutable configuration options once for efffeciency that are used widely in subcatalog variants
  // TODO:  Decide if perhaps these should be moved to CatalogManager?
  protected static ConfigurationHolder currentProfile;
  protected static Integer maxBeforeSplit;
  protected static Integer maxSplitLevels;
  protected static Integer maxBeforePaginate;
  protected static Boolean useExternalIcons;
  protected static Boolean useExternalImages;
  protected static Boolean includeCoversInCatalog;
  protected static String booksURI;
  protected static Collator collator = Collator.getInstance(ConfigurationManager.getLocale());
  protected static final CRC32 crc32 = new CRC32();
  protected static CachedFile defaultCoverFile;
  protected static String defaultCoverUri;

  //  PROPERTIES

  // This variable is set to the level (if any) for a particular catalog instance.
  // It would be a null/empty string for top level catalogs.   It will be set when
  // generating any additional level  This could be a custom catalog, the Featured
  // catalog or perhaps an additional level from extra tags/custom columns.
  private String catalogLevel;

  // This identifies the particular catalog type.   It is set within the classes
  // derived from this class.  It is used in conjuction with the level to work
  // out the default catalog and basefilename for a particular catalog instance.
  private String catalogType;

  // The folder in which the files for this sub-catalog are to be placed.
  // It should always be set - for the top level it is an empty string
  // It should always be stored without level and/or security information as
  // there are methods available to get the version with these added.
  private String catalogFolder;

  // The filename on which files in this catalog are based.
  // If not set then it is assumed to be the same as the catalog folder
  private String catalogBaseFilename;

  // The full path for the folder and base filename including the security code,
  // level in the folder part and all relevant separators. It is a cached copy for
  // effeciency reasons as it is needed for each catalog entry.
  private String catalogFolderBaseFilename;

  // Set to true if the XSL file for the list entries has changed
  private static Boolean xslCatalogChanged;
  // Set to true if  the XSL for the full entries has changed.
  private static Boolean xslFullEntryChanged;

  private List<Object> stuffToFilterOut;

  private List<Book> books;

  private String optimizeUrlPrefix;       // String that is used when trying to optimize URL's

  // CONSTRUCTORS

  public SubCatalog(List<Book> books) {
    this(null, books);
    initalise();
  }

  public SubCatalog() {
    initalise();
  }

  public SubCatalog(List<Object> stuffToFilterOut, List<Book> books) {
    initalise();
    setStuffToFilterOut(stuffToFilterOut);
    setBooks(books);
    //if (crc32 == null)
    //  crc32 = new CRC32();
  }

  /**
   * Initialise the static variables to the correct values for the
   * catalog that is bing generated
   */
  private static void initalise() {
    if (currentProfile == null) {
      currentProfile = ConfigurationManager.getCurrentProfile();
      maxBeforeSplit = currentProfile.getMaxBeforeSplit();
      maxSplitLevels = currentProfile.getMaxSplitLevels();
      maxBeforePaginate = currentProfile.getMaxBeforePaginate();
      useExternalIcons = currentProfile.getExternalIcons();
      useExternalImages = currentProfile.getExternalImages();
      includeCoversInCatalog = currentProfile.getIncludeCoversInCatalog();
      booksURI = currentProfile.getUrlBooks();
      collator = Collator.getInstance(ConfigurationManager.getLocale());
      xslCatalogChanged = ! CatalogManager.isGenerateFileSameAsCatalogFile(Constants.CATALOG_XSL);
      xslFullEntryChanged = ! CatalogManager.isGenerateFileSameAsCatalogFile(Constants.FULLENTRY_XSL);
      defaultCoverFile  = CachedFileManager.addCachedFile(new File(CatalogManager.getGenerateFolder(), Constants.DEFAULT_IMAGE_FILENAME));
      defaultCoverUri = Constants.PARENT_PATH_PREFIX + Constants.DEFAULT_IMAGE_FILENAME;
    }
  }
  /**
   * Ensure all the static variables are reset to be correct for the
   * catalog that is about to be generated.
   */
  public static void reset() {
    currentProfile = null;
    initalise();
  }

  private void setOptimizUrlPrefix() {
    optimizeUrlPrefix = Constants.PARENT_PATH_PREFIX + getCatalogPrefix() + getCatalogFolder() + Constants.FOLDER_SEPARATOR;
  }
  // METHODS

  /**
   * Get the current catalog level for this catalog instance
   * If we are not in a sub-level this will be empty
   *
   * @return
   */
  public String getCatalogLevel() {
    if (catalogLevel == null) {
      catalogLevel = "";
    }
    return catalogLevel;
  }

  /**
   * Set the catalog level for this particular catalog instance
   * It is only set for catalogs that are not top-level ones
   *
   * @param newlevel
   */
  public void setCatalogLevel(String newlevel) {
    assert newlevel != null;
    catalogLevel = newlevel;
    catalogFolderBaseFilename = null;
    setOptimizUrlPrefix();
  }

  /**
   * set the Catalog Level based on the breadcrumbs to this point
   * This is used when a complete set of sub-catalogs are required.
   * TODO assumes that the breadcrumbs are unique - this assumption needs validation
   *
   * @param breadcrumbs
   */
  public void setCatalogLevel(Breadcrumbs breadcrumbs) {
    assert (breadcrumbs != null);
    setCatalogLevel(encryptString(breadcrumbs.toString()));
    setOptimizUrlPrefix();
  }

  /**
   * Routine to get CRC32 value for given string
   * Used as helper function when encrypting folder and file names
   *
   * @param data
   * @return
   */
  protected static String encryptString(String data) {
    crc32.reset();
    crc32.update(data.getBytes());
    return Long.toHexString(crc32.getValue());
  }

  /**
   * If the user has asked for encrypted filename, then an
   * encryption string is added to the start of the filename
   * that is derived from the actual filename, and then the
   * filename in the clear is added.  This should mean that
   * catalogs are easy to read while the filenames are hard to guess.
   * <p/>
   * As a special case we need to separate out the foldername
   * (if present) and only encrypt the filename part.
   *
   * @param filename
   * @return
   */
  private static String encryptFilename(String filename) {
    if (CatalogManager.getSecurityCode().length() == 0) {
      // Do nothing if encryption not active
      return filename;
    }
    int pos = filename.indexOf(Constants.SECURITY_SEPARATOR);
    if (pos != -1) {
      int dummy = 1;
    }
    return encryptString(filename) + Constants.SECURITY_SEPARATOR + filename.substring(pos + 1);
  }

  /**
   * Get the current level and if necessary add the prefix if not empty
   *
   * @return
   */
  private String getCatalogPrefix() {
    if (catalogLevel == null)
      catalogLevel = "";
    String result = (CatalogManager.getSecurityCode().length() == 0 ? "" : CatalogManager.getSecurityCodeAndSeparator()) + catalogLevel;
    if (catalogLevel.length() > 0)
      result += Constants.LEVEL_SEPARATOR;
    return result;
  }

  /**
   * Get the current catalog type.
   * If it has never been set we assume an empty string
   *
   * @return
   */
  public String getCatalogType() {
    if (catalogType == null)
      catalogType = "";
    return catalogType;
  }

  /**
   * Set the catalog type.
   * This would normally only be used for special catalog types as the main
   * ones will have set this to a final value as part of class initialisation
   *
   * @param type
   */
  public void setCatalogType(String type) {
    assert (type != null);
    catalogType = catalogFolder = catalogBaseFilename = type;
    catalogFolderBaseFilename = null;
    setOptimizUrlPrefix();
  }

  /**
   * Get the folder for this sub-catalog
   *
   * @return The foldername for this catalog
   */
  public String getCatalogFolder() {
    if (Helper.isNullOrEmpty(catalogFolder)) {
      catalogFolder = getCatalogFolder(getCatalogType());
    }

    //  Debugging asserts - could be removed if not wanted
    assert catalogFolder.indexOf(Constants.SECURITY_SEPARATOR) == -1 : "Program error: catalogFolder contains SECURITY_SEPARATOR (" + catalogFolder + ")";
    // assert catalogFolder.indexOf(Constants.LEVEL_SEPARATOR) == -1 :
    //       "Program error: catalogFolder contains LEVEL_SEPARATOR (" + catalogFolder + ")";

    return catalogFolder;
  }

  /**
   * Get the full folder name for the given folder type
   * Needs to take into account any level we may be working at
   * and also any secutiry code that might be active.
   *
   * @param foldertype // The type (ignoring level) of folder we want
   * @return // The folder name including any level and security prefix if type not empty
   */
  public String getCatalogFolder(String foldertype) {
    assert (foldertype != null);
    if (foldertype.length() == 0) {
      return foldertype;
    }
    return getCatalogPrefix() + foldertype;
  }

  /**
   * Get the catalog folder for the given type with the name derived
   * from catalog type.  It should have the security prefix the catalog name
   * but omit any level information.   This is primarily used for the
   * sub-catalog types such as 'books' and 'author' which are always
   * at the top level.
   *
   * @return The catalog name preceded with any security infomation, but no level
   */
  public String getCatalogFolderWithSecurityNoLevel() {
    assert Helper.isNotNullOrEmpty(catalogType) : "Program Error catalogType not set";
    return getCatalogFolderWithSecurityNoLevel(catalogType);
  }

  /**
   * Get the catalog folder for the given type with the name derived
   * from type.  It should have the security prefix the catalog name
   * but omit any level information.   This is primarily used for the
   * sub-catalog types such as 'books' and 'author' which are always
   * at the top level.
   *
   * @param foldertype The catalog 'type'
   * @return The catalog name preceded with any security infomation, but no level
   */
  public String getCatalogFolderWithSecurityNoLevel(String foldertype) {
    assert (Helper.isNotNullOrEmpty(foldertype)) : "Program Error: foldertype not set";
    String result = (CatalogManager.getSecurityCode().length() == 0 ? "" : CatalogManager.getSecurityCodeAndSeparator()) + foldertype;

    // Debugging asserts  - could be removed if not wanted
    int pos = result.indexOf(Constants.SECURITY_SEPARATOR);
    assert result.substring(pos + 1).indexOf(Constants.SECURITY_SEPARATOR) == -1 : "Program error: Two occurences of SECURITY_SEPARATOR (" + result + ")";
    assert result.indexOf(Constants.LEVEL_SEPARATOR) == -1 : "Program error: Unexpected LEVEL_SEPARATOR (" + result + ")";

    return result + foldertype;
  }

  /**
   * Get the catalog folder for the given type with the name derived
   * from type.  It should have the security prefix the catalog name
   * but omit any level information.   This is primarily used for the
   * sub-catalog types such as 'books' and 'author' which are always
   * at the top level.
   *
   * @param foldertype The catalog 'type'
   * @return The catalog name preceded with any security infomation, but no level
   */
  public String getCatalogFolderWithLevelAndSecurity(String foldertype) {
    assert (Helper.isNotNullOrEmpty(foldertype)) : "Program Error: foldertype not set";
    String result = (CatalogManager.getSecurityCode().length() == 0 ? "" : CatalogManager.getSecurityCodeAndSeparator()) + foldertype;

    // Debugging asserts - could be removed if not wanted
    int pos = result.indexOf(Constants.SECURITY_SEPARATOR) + 1;
    assert result.substring(pos).indexOf(Constants.SECURITY_SEPARATOR) == -1 : "Program error: Two occurences of SECURITY_SEPARATOR (" + result + ")";
    pos = result.indexOf(Constants.LEVEL_SEPARATOR);
    assert result.substring(pos).indexOf(Constants.LEVEL_SEPARATOR) == -1 : "Program error: Two occurences of LEVEL_SEPARATOR (" + result + ")";

    return result;
  }

  /**
   * Set the folder to be used
   * It is always stored decoded and without any trailing slash
   * There should also not be any security code present - if so remove it.
   * <p/>
   * NOTE: For convenience we also allow a full folder.filename path
   * to be passed in and then we extract the folder part.
   *
   * @param folder folder name to set
   */
  public void setCatalogFolder(String folder) {
    assert folder != null;

    int pos = folder.indexOf(Constants.FOLDER_SEPARATOR);
    if (pos != -1) {
      folder = folder.substring(0, pos);
      assert folder.indexOf(Constants.FOLDER_SEPARATOR) == -1 : "Program Error: Unexpected occurence of FOLDER_SEPARATOR (folder=" + folder + ")";
    }

    pos = folder.indexOf(Constants.SECURITY_SEPARATOR);
    if (pos != -1) {
      assert (folder.substring(0, pos).equals(CatalogManager.getSecurityCode())) : "Program Error:  Security Code does not seem to match expected value (folder=" + folder + ")";
      assert folder.indexOf(Constants.SECURITY_SEPARATOR, pos + 1) == -1 : "Program error: Unexpected Second Occurencs of SECURITY_SEPARATOR (folder=" +
          folder + ")";
      ;
      folder = folder.substring(pos + 1);
    }

    pos = folder.indexOf(Constants.LEVEL_SEPARATOR);
    if (pos != -1) {
      assert (folder.substring(0, pos).equals(catalogLevel)) : "Program Error:  Catalog level does not seem to match expected value  (folder=" + folder + ")";
      assert folder.indexOf(Constants.LEVEL_SEPARATOR, pos + 1) == -1 : "Program error: Unexpected second occurencs of LEVEL_SEPARATOR (folder=" + folder + ")";
      folder = folder.substring(pos + 1);
    }

    catalogFolder = folder;
    setOptimizUrlPrefix();
  }

  /**
   * Variant of setFolder when we want to split according to Id
   *
   * @param folder
   * @param id
   */
  public void setCatalogFolderSplit(String folder, String id) {
    setCatalogFolder(folder + Constants.TYPE_SEPARATOR + (int) (Long.valueOf(id) / 1000));
  }

  /**
   * Get the Current Catalog Base filename
   * <p/>
   * If both the folder name, catalog type and catalog level are not set we treat
   * this as a special case and add in the security code.
   *
   * @return
   */
  public String getCatalogBasefilename() {
    if (catalogBaseFilename == null) {
      catalogBaseFilename = getCatalogType();
    }

    // Debugging assert - could be removed if not wanted
    assert catalogBaseFilename.indexOf(Constants.FOLDER_SEPARATOR) == -1 : "Program Error:  Unexpected FOLDER_SEPARATOR (" + catalogBaseFilename + ")";

    if (catalogLevel.length() == 0 && catalogFolder.length() == 0 && catalogType.length() == 0) {
      // The special case for top level
      return CatalogManager.getSecurityCodeAndSeparator() + catalogBaseFilename;
    } else {
      // The normal case
      return catalogBaseFilename;
    }
  }

  /**
   * Set the base filename to be used for this catalog.
   * Only needed when it cannot be derived automatically from the type
   * <p/>
   * NOTE:  The name is always stored 'in the clear' so any security code
   * or level type information needs removing.
   *
   * @param name
   */
  public void setCatalogBaseFilename(String name) {
    assert Helper.isNotNullOrEmpty(name) : "Program Error: invalid name parameter (" + name + ")";
    // We want to skip over any leading folder name
    int pos = name.indexOf(Constants.FOLDER_SEPARATOR);

    // Debugging assert - could be removed if not wanted
    assert name.substring(pos + 1).indexOf(Constants.FOLDER_SEPARATOR) == -1 : "Program Error: Multiple FOLDER_SEPARATORS found (" + name + ")";
    if (pos != -1) {
      name = name.substring(pos + 1);     // Remove the folder part
    }
    // We also want to remove any leading occurrence of security code
    if (CatalogManager.getSecurityCode().length() > 0 && name.startsWith(CatalogManager.getSecurityCodeAndSeparator())) {
      name = name.substring(CatalogManager.getSecurityCodeAndSeparator().length());  // Remove the security code
    }
    // Finally we want to remove any existing encryption string
    pos = name.indexOf(Constants.SECURITY_SEPARATOR);
    name = name.substring(pos + 1);
    catalogBaseFilename = name;
    catalogFolderBaseFilename = null;
  }


  /**
   * Get the base folder/file name based on the object propertied
   * Level is added from the current object as require
   *
   * @return
   */
  public String getCatalogBaseFolderFileName() {
    if (catalogFolderBaseFilename == null) {
      // Special case of empty folder and level (as used for index files!)
      if (catalogFolder.length() == 0 & catalogLevel.length() == 0) {
        catalogFolderBaseFilename = getCatalogBasefilename();
      } else {
        catalogFolderBaseFilename = getCatalogPrefix() + getCatalogFolder();  // This will include security/level prefixes
        catalogFolderBaseFilename += Constants.FOLDER_SEPARATOR + encryptFilename(getCatalogBasefilename());
      }
    }

    //int pos = catalogFolderBaseFilename.indexOf(Constants.SECURITY_SEPARATOR);
    //pos = catalogFolderBaseFilename.indexOf(Constants.LEVEL_SEPARATOR);
    //assert catalogFolderBaseFilename.substring(pos+1).indexOf(Constants.LEVEL_SEPARATOR) == -1 :
    //    "Program error: Two occurences of LEVEL_SEPARATOR (" + catalogFolderBaseFilename + ")";

    return catalogFolderBaseFilename;
  }

  /**
   * Get the folder/file name based on the type parameter
   * Level is added from the current object as require .
   * It will have the embedded level/security information if needed.
   *
   * @return
   */
  public String getCatalogBaseFolderFileName(String type) {
    assert Helper.isNotNullOrEmpty(type);
    String folder = getCatalogFolder(type);
    return folder + ((folder.length() != 0) ? Constants.FOLDER_SEPARATOR : "") + encryptFilename(type);
  }

  public String getCatalogBaseFolderFileNameNoLevel(String type) {
    assert Helper.isNotNullOrEmpty(type);
    String result = (CatalogManager.getSecurityCode().length() == 0 ? "" : CatalogManager.getSecurityCodeAndSeparator()) + type;
    if (result.length() > 0)
      result += Constants.FOLDER_SEPARATOR;
    return result + encryptFilename(type);
  }

  /**
   * Get the full base folder/filename including the speified id.
   * It will get the level/security information from the current catalog properties.
   *
   * @param id
   * @return
   */
  public String getCatalogBaseFolderFileNameId(String id) {
    String name = getCatalogBaseFolderFileName();
    int pos = name.indexOf(Constants.FOLDER_SEPARATOR);
    String folder = "";
    if (pos != -1) {
      folder = name.substring(0, pos + 1);
      name = name.substring(pos + 1);
    }
    String result = encryptFilename(name + Constants.TYPE_SEPARATOR + id);
    return folder + result;
  }

  /**
   * Get the full base folder/filename for the given type and id
   * It will get the level information from the current catalog properties.
   * Security information will also be added as required
   *
   * @param type
   * @param id
   * @return
   */
  public String getCatalogBaseFolderFileNameId(String type, String id) {
    String folder = getCatalogFolder(type);
    return folder + ((folder.length() != 0) ? Constants.FOLDER_SEPARATOR : "") + encryptFilename(type + Constants.TYPE_SEPARATOR + id);
  }

  /**
   * Get the full base folder/filename for the given type and id
   * It will get the level information from the current catalog properties.
   * Security information will also be added as required.
   * <p/>
   * To keep the number of files in a single folder down (which can affect
   * perforance we store a maximum of 1000 book id;s in a single folder
   * (although in practise it is likely to be slightly less due to gaps
   * in the Calibre Id sequence after books have been deleted/altered/merged.
   *
   * @param type
   * @param id
   * @return
   */
  public String getCatalogBaseFolderFileNameIdSplit(String type, String id, int splitSize) {
    String filename = getCatalogBaseFolderFileNameId(type, id);
    int pos = filename.indexOf(Constants.FOLDER_SEPARATOR);
    assert pos != -1;
    filename = filename.substring(0, pos) + Constants.TYPE_SEPARATOR + ((long) (Long.parseLong(id) / splitSize)) + filename.substring(pos);
    return filename;
  }

  /**
   * Get the full base folder/filename for the given type and id
   * Security information will be added, but no level information.
   * This is intended for entry types that are always at the top level
   * (such as books)
   *
   * @param type
   * @param id
   * @return
   */
  private static String getCatalogBaseFolderFileNameIdNoLevel(String type, String id) {
    String result = (CatalogManager.getSecurityCode().length() == 0 ? "" : CatalogManager.getSecurityCodeAndSeparator()) + type;
    if (result.length() > 0)
      result += Constants.FOLDER_SEPARATOR;
    result = result + encryptFilename(type + Constants.TYPE_SEPARATOR + id);
    return result;
  }

  /**
   * Get the full base folder/filename for the given type and id
   * Security information will be added, but no level information.
   * This is intended for entry types that are always at the top level
   * (such as books)
   * <p/>
   * To keep the number of files in a single folder down (which can affect
   * perforance we store a maximum of 1000 book id;s in a single folder
   * (although in practise it is likely to be slightly less due to gaps
   * in the Calibre Id sequence after books have been deleted/altered/merged.
   *
   * @param id
   * @return
   */
  public static String getCatalogBaseFolderFileNameIdNoLevelSplit(String type, String id, int splitSize) {
    String filename = getCatalogBaseFolderFileNameIdNoLevel(type, id);
    int pos = filename.indexOf(Constants.FOLDER_SEPARATOR);
    assert pos != -1;
    filename = filename.substring(0, pos) + Constants.TYPE_SEPARATOR + ((long) (Long.parseLong(id) / splitSize)) + filename.substring(pos);
    return filename;
  }

  /**
   * Determine if the icon prefix should be for the current or parent folder
   *
   * @param inSubDir
   * @return
   */
  protected String getIconPrefix(boolean inSubDir) {
    return inSubDir ? Constants.PARENT_PATH_PREFIX : Constants.CURRENT_PATH_PREFIX;
  }

  /**
   * Optimise the URN to simplify them if pointing to files in sthe currentcatalog folder
   * <p/>
   * NOTE:  We should never optimize breadcrumb URL's as we do not know ehere they are called from
   *
   * @param url the unoptimised URL
   * @return the optimized URL
   */
  public String optimizeCatalogURL(String url) {
    assert optimizeUrlPrefix != null : "Program Error:  optimizeUrlPrefix should not be null!";
    // See if start is pointing back to current folder?
    if (url.startsWith(optimizeUrlPrefix)) {
      // If so we can strip the folder name part
      int pos = optimizeUrlPrefix.length();
      assert url.length() > pos : "Program Error: URL only has prefix!";
      //TODO  Activate the following code if trace shows would achieve expected results
      if (logger.isTraceEnabled())
        logger.trace("should be able to optimize following URL: " + url + ", (folder=" + getCatalogFolder() + ") to " + Constants.CURRENT_PATH_PREFIX +
            url.substring(pos));
      // return Constants.CURRENT_PATH_PREFIX + url.substring(pos);
    }
    return url;
  }

  /**
   * Set the list of books to be included in this (sub)catalog
   *
   * @param books
   */
  void setBooks(List<Book> books) {
    this.books = null;
    if (Helper.isNotNullOrEmpty(stuffToFilterOut)) {
      this.books = filterOutStuff(books);
    }
    if (this.books == null) {
      this.books = books;
    }
  }

  /**
   * Get the list of books associated with this sub-catalog
   *
   * @return
   */
  List<Book> getBooks() {
    return books;
  }
  /**
   * Function to sort books by timestamp (last modified)
   *
   * @param books
   */
  static void sortBooksByTimestamp(List<Book> books) {
    // sort the books by timestamp
    Collections.sort(books, new Comparator<Book>() {

      public int compare(Book o1, Book o2) {
        Date ts1 = (o1 == null ? new Date() : o1.getTimestamp());
        Date ts2 = (o2 == null ? new Date() : o2.getTimestamp());
        return ts2.compareTo(ts1);
      }

    });
  }

  /**
   * Sort the list of books alphabetically
   * We allow the field that is to be used for sorting
   * titles to be set as a configuration parameter
   *
   * @param books
   */
  static void sortBooksByTitle(List<Book> books) {
    Collections.sort(books, new Comparator<Book>() {
      public int compare(Book o1, Book o2) {
        String title1 = o1.getTextToSort();
        String title2 = o2.getTextToSort();
        return Helper.checkedCollatorCompareIgnoreCase(title1, title2, collator);
      }
    });
  }

  /**
   * Get the list of stuff acting as a filter for this sub-catalog
   *
   * @return
   */
  List<Object> getStuffToFilterOut() {
    return stuffToFilterOut;
  }

  /**
   * Get the list of stuff to filter out extended by new values
   *
   * @param newStuff
   * @return
   */
  List<Object> getStuffToFilterOutAnd(Object newStuff) {
    List<Object> result = new ArrayList<Object>();
    if (stuffToFilterOut != null)
      result.addAll(stuffToFilterOut);
    if (newStuff != null)
      result.add(newStuff);
    return result;
  }

  /**
   * Set the list of stuff to filter out
   *
   * @param stuffToFilterOut
   * @return
   */
  SubCatalog setStuffToFilterOut(List<Object> stuffToFilterOut) {
    this.stuffToFilterOut = stuffToFilterOut;
    return this;
  }

  /**
   * Get the list of books filtered according to the filter criteria
   *
   * @param originalBooks
   * @return
   */
  List<Book> filterOutStuff(List<Book> originalBooks) {
    // by default, simply return the book list
    return originalBooks;
  }

  /**
   * Extract the folder part of the filename
   *
   * @param pCatalogFileName
   * @return
   */
  public String getFolderName(String pCatalogFileName) {
    assert (Helper.isNotNullOrEmpty(pCatalogFileName)) : "Program Error: empty filename!";
    int pos = pCatalogFileName.indexOf(Constants.FOLDER_SEPARATOR);
    return (pos == -1) ? pCatalogFileName : pCatalogFileName.substring(0, pos);
  }

  /**
   * Determine if the conditions for SplitByLetter to be active are tru
   *
   * @param splitOption
   * @param count
   * @return
   */
  public Boolean checkSplitByLetter(SplitOption splitOption, int count) {
    return (splitOption == SplitOption.SplitByLetter) && (maxSplitLevels > 0) && count > maxBeforeSplit;
  }

  /**
   * Determine if Splitoption should be changed from SplitByLetter to Paginate
   * because we have already split by the maximum number of levels requested..
   *
   * @param splitLetters
   * @return
   */
  public SplitOption checkSplitByLetter(String splitLetters) {
    return splitLetters.length() < maxSplitLevels ? SplitOption.SplitByLetter : SplitOption.Paginate;
  }

  boolean isInDeepLevel() {
    return Helper.isNotNullOrEmpty(stuffToFilterOut);
  }



  /**
   * @return a result composed of the resulting OPDS entry, and the relative url to the subcatalog
   */
  // public abstract Composite<Element, String> getSubCatalogEntry(Breadcrumbs pBreadcrumbs, boolean inSubDir) throws IOException;

  /**
   * Create the XML and HTML files (as required by configuration parameters) from
   * the XML document that has just been created.
   *
   * @param feed           The feed that is to be used to generate the output files
   * @param xmlFilename    The name of the output file.
   * @param feedType       The type of file that is to be generated
   * @param isHtmlOptimiseAllowed  Set when HTML optimisation mustbe suppressed
   * @throws IOException Any exception would be unexpected, but it is always theoretically possible!
   */
  public void createFilesFromElement(Element feed,
                String xmlFilename,
                HtmlManager.FeedType feedType,
                Boolean isHtmlOptimiseAllowed)
                  throws IOException {

    // Various asserts to help with identifying logic faults in the program!
    assert feed != null : "Programerror: Unexpected attempt to create file from non-existent feed";
    assert Helper.isNotNullOrEmpty(xmlFilename) : "Program error: Attempt to create XML file for empty/null filename";
    assert !xmlFilename.startsWith(CatalogManager.getGenerateFolder().toString()) : "Program Error:  filename should not include catalog folder (" +
        xmlFilename + ")";
    // int pos = outputFilename.indexOf(Constants.SECURITY_SEPARATOR);
    // assert outputFilename.substring(pos+1).indexOf(Constants.SECURITY_SEPARATOR) == -1 :
    //    "Program error: Two occurences of SECURITY_SEPARATOR (" + outputFilename + ")";
    // pos = outputFilename.indexOf(Constants.LEVEL_SEPARATOR);
    // assert outputFilename.substring(pos+1).indexOf(Constants.LEVEL_SEPARATOR) == -1 :
    //    "Program error: Two occurences of LEVEL_SEPARATOR (" + outputFilename + ")";


    if (!xmlFilename.endsWith(Constants.XML_EXTENSION)) {
      xmlFilename += Constants.XML_EXTENSION;
    }
    CachedFile xmlFile = CachedFileManager.addCachedFile(CatalogManager.storeCatalogFile(xmlFilename));
    // Avoid creating files that already exist.
    // (if xml file exists then HTML one will as well)
    if (xmlFile.exists()) {
      logger.trace("\n\n*** Attempt to generate file already done (" + xmlFilename + ") - see if it can be optimised out! ***\n");
      //      if (logger.isTraceEnabled()) logger.trace("\n\n*** Attempt to generate file already done (" + outputFilename + ") - see if it can be optimised out! ***\n");
      return;
    }

    // Create as a DOM object
    // TODO  Check if there might be a cheaper way to do this not using DOM?
    Document document = new Document();
    document.addContent(feed);

    // write the XML file
    // (unless the user has suppressed the OPDS catalogs)
    if (currentProfile.getGenerateOpds()) {
      FileOutputStream fos = null;
      try {
        fos = new FileOutputStream(xmlFile);
        String prettyXML = JDOMManager.getPrettyXML().outputString(document);
        String compactXML = JDOMManager.getCompactXML().outputString(document);
        String rawXML = JDOMManager.getRawXml().outputString(document);

        JDOMManager.getPrettyXML().output(document, fos);;
      } catch (RuntimeException e) {
        logger.warn("Error writing file " + xmlFilename + "(" + e.toString() + ")"); Helper.statsWarnings++;
      } finally {
        if (fos != null)
          fos.close();
        xmlFile.clearCachedInformation();
      }
    }

    //  generate corresponding HTML file

    CachedFile htmlFile = CachedFileManager.addCachedFile(HtmlManager.getHtmlFilename(
        xmlFile.getAbsolutePath()));
    if (htmlFile.exists()) {
      logger.error("Program Error?  Attempt to recreate existing HTML file '" + htmlFile + "'"); Helper.statsErrors++; Helper.statsErrors++;
      return;
    }

    // See if we can optimise things by avoiding generating the HTML file
    // if:
    // - The flag to allow HTML optimisation is true
    //   (typically false for book lists after browse-by-cover mode changed/unknown)
    // - the target HTML file already exists in the catalog
    // - the XML file is identical to the one already in the catalog
    // - the XSL file is older than the HTML file

    CachedFile catalogXmlFile = CachedFileManager.addCachedFile(CatalogManager.getCatalogFolder() + xmlFile.getAbsolutePath().substring(CatalogManager.getGenerateFolderpathLength()));
    CachedFile catalogHtmlFile = CachedFileManager.addCachedFile(CatalogManager.getCatalogFolder() + htmlFile.getAbsolutePath().substring(CatalogManager.getGenerateFolderpathLength()));
    boolean xslChanged;

    switch (feedType) {
      case MainCatalog:
      case Catalog:
            xslChanged = xslCatalogChanged;
            break;
      case BookFullEntry:
            xslChanged = xslFullEntryChanged;
            break;
      default:
            logger.error("Program Error: unrecognised feedType for file '" + xmlFile + "'" ); Helper.statsErrors++;
            return;
    }
    if (catalogXmlFile.exists()
    && catalogXmlFile.getCrc() == xmlFile.getCrc()) {
      catalogXmlFile.setChanged(false);
      xmlFile.setChanged(false);
    }
    if (isHtmlOptimiseAllowed
    && ! xslChanged
    && catalogHtmlFile.exists()
    && xmlFile.isChanged() == false ) {
//    && CatalogManager.isSourceFileSameAsTargetFile(xmlFile, catalogXmlFile)) {
      catalogHtmlFile.setChanged(false);
      htmlFile.setChanged(false);
      Helper.statsHtmlUnchanged++;
    } else {
      if (currentProfile.getGenerateHtml()) {
        CatalogManager.htmlManager.generateHtmlFromDOM(document, htmlFile.getAbsoluteFile(),
            feedType);
        htmlFile.clearCachedInformation();
        Helper.statsHtmlChanged++;
//      } else {
//        CachedFileManager.removeCachedFile(htmlFile);
      }
    }
    // See if we need to keep the XML file
    if (currentProfile.getGenerateOpds()) {
      if (xmlFile.isChanged() == false) {
        xmlFile.delete();     // We do not keep the XML file in the temp folder if marked as unchanged
        Helper.statsXmlUnchanged++;
      } else {
        Helper.statsXmlChanged++;
      }
    } else {
      xmlFile.delete();
      CachedFileManager.removeCachedFile(xmlFile);
      Helper.statsXmlDiscarded++;
    }
  }

  /*
   * Decide if a Series cross-reference should be generated for this book
   *
   * Takes into account if this is the only book and the relevant setting
   */
  protected boolean isSeriesCrossreferences(Book book) {
    if (! currentProfile.getGenerateCrossLinks()
    ||  ! currentProfile.getIncludeSerieCrossReferences()) {
      return false;
    }
    Series series = book.getSeries();
    if (series == null) {
      return false;
    }

    if (currentProfile.getSingleBookCrossReferences()
    ||  DataModel.getMapOfBooksBySeries().get(series).size() > 1) {
      return true;
    }

    return false;
  }

  /**
   * Decide if an Author cross-reference should be generated for this book
   *
   * Does not take into account whether an author has a single book
   */
  protected boolean isAuthorCrossReferences(Book book) {
    if (! currentProfile.getGenerateCrossLinks()
    ||  ! currentProfile.getIncludeAuthorCrossReferences()) {
      return false;
    }
    return book.hasAuthor();
  }

  /**
   * Decide if Tag cross-references should be generated for this book
   *
   * Does not take into account whether a tag has a single book
   */
  protected boolean isTagCrossReferences(Book book) {
    if (! currentProfile.getGenerateCrossLinks()
    ||  ! currentProfile.getIncludeTagCrossReferences()) {
      return false;
    }
    List<Tag> authors = book.getTags();
    return (book.getTags() != null);
  }

  /**
   * Decide if the rating cross reference should be included.
   *
   * Takes into account if this is the only book and the relevant setting
   * Decide if Ratings cross-reference should be generated for this book
   */
  protected boolean isRatingCrossReferences(Book book) {
    if (! currentProfile.getGenerateCrossLinks()
    ||  ! currentProfile.getIncludeRatingCrossReferences()) {
      return false;
    }
    BookRating rating = book.getRating();
    if (rating == null) {
      return false;
    }
    if (currentProfile.getSingleBookCrossReferences()
    ||  DataModel.getMapOfBooksByRating().get(rating).size() > 1) {
      return true;
    }
    return false;
  }

  /**
   * #c2o-208
   * Create additional links for a paginated set depending on
   * - the current pages
   * - the maximum page count
   *
   * A Prev link is created if currenta page > 1
   * A Last link is created if (max pages - current page) > 2
   * A First link is created if Current Page > 2
   *
   * NOTE:  This is always called on pages AFTRER the first,
   *        to provide the links to be inserted into the previous page
   *
   * @param filename    Base filename for the URL.
   * @param pageNumber  current page number
   * @param maxPages    maximum pages in the set.
   */
  public Element createPaginateLinks (Element feed, String filename, int pageNumber, int maxPages) {

    int pos = filename.lastIndexOf(Constants.XML_EXTENSION);
    if (pos > 0)  filename = filename.substring(0,pos);
    pos = filename.lastIndexOf(Constants.TYPE_SEPARATOR);
    assert pos > 0;
    // assert Integer.toString(pageNumber).equals((filename.substring(pos + 1)));
    if (! Integer.toString(pageNumber).equals((filename.substring(pos + 1)))) {
      int dummy = 1;
    }
    filename = filename.substring(0,pos + 1);

      // Prev link
    if (pageNumber > 1) {
        feed.addContent(FeedHelper.getNavigationLink(filename + (pageNumber-1) + Constants.XML_EXTENSION,
                                                     FeedHelper.RELATION_PREV,
                                                     Localization.Main.getText("title.prevpage", pageNumber-1, maxPages)));
    }
    // First link
    if (pageNumber > 2) {
      feed.addContent(FeedHelper.getNavigationLink(filename + "1" + Constants.XML_EXTENSION,
                                                   FeedHelper.RELATION_FIRST,
                                                   Localization.Main.getText("title.firstpage", 1, maxPages)));
    }
    // Last link
    if ((maxPages - pageNumber) > 1) {
      feed.addContent(FeedHelper.getNavigationLink(filename + maxPages + Constants.XML_EXTENSION,
                                                   FeedHelper.RELATION_LAST,
                                                   Localization.Main.getText("title.lastpage", maxPages, maxPages)));
    }

    // Next link
    // It is always one page out because of the way the result is used
    if (pageNumber == 1)  return null;
    return FeedHelper.getNavigationLink(filename + (pageNumber) + Constants.XML_EXTENSION,
                                        FeedHelper.RELATION_NEXT,
                                        Localization.Main.getText("title.nextpage", pageNumber, maxPages));

  }

  // TODO  GENERALISED SUB-CATALOG TYPE HANDLING

  // TODO:           WORK IN PROGRESS !

  // See if we can generalise the cration of sub-catalog pages to a
  // treatment with a series of standardised methods implemented in each specific
  // specific sub-catalog type.    This would have the huge advantage of greatly
  // reducing the chance of errors in a particular type as well as making the
  // maintenance of the types simpler.  It will also simplify adding new types.

  // The method to be used to sort objects of this type
/*
  protected abstract void sortMethod(List<Object> obj);
*/

  /**
   * Get a page withina sub-catalog
   *
   * @param pBreadcrumbs
   * @param listobjects
   * @param inSubDir
   * @param from
   * @param title
   * @param summary
   * @param urn
   * @param pFilename
   * @param splitOption
   * @param icon
   * @param firstElements
   * @param options
   * @return
   * @throws IOException
   */
/*
  protected Element getListOfObjects(Breadcrumbs pBreadcrumbs,
      List<? extends GenericDataObject> listobjects,
      boolean inSubDir,
      int from,
      String title,
      String summary,
      String urn,
      String pFilename,
      SplitOption splitOption,
      String icon,
      List<Element> firstElements,
      Option... options) throws IOException {

    if (logger.isDebugEnabled()) logger.debug("getListOfBooks: START");

    // Special case of first time through when not all values set
    assert listobjects != null;
    // if (listobjects == null) listobjects = getBooks();
    if (pFilename == null)  pFilename = getCatalogBaseFolderFileName();

    //  Now some consistency checks

    // Now get on with main processing
    int catalogSize = listobjects.size();
    if (logger.isDebugEnabled()) logger.debug("getListOfBooks:catalogSize=" + catalogSize);

    if (from != 0) inSubDir = true;
    if (Helper.isNotNullOrEmpty(pBreadcrumbs) &&  pBreadcrumbs.size() > 1) inSubDir = true;
    if (inSubDir && icon.startsWith(Constants.CURRENT_PATH_PREFIX))
      icon = Constants.PARENT_PATH_PREFIX + icon.substring(2);

    // Work out any split options
    // Fixes #716917 when applied to author books list
    boolean willSplitByLetter;
    boolean willSplitByDate;
    if (splitOption == null) {
      // ITIMPI: Null seems to be equivalent to SplitByLetter !
      //         Might be better to replace calls by explicit value?
      splitOption = SplitOption.SplitByLetter;
      if (logger.isDebugEnabled()) logger.debug("getListOfBooks:splitOption=null.  Changed to SplitByLetter");
    }
    switch (splitOption) {
      case Paginate:
        if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=Paginate");
        willSplitByLetter = false;
        willSplitByDate = false;
        break;
      case DontSplitNorPaginate:
        if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=DontSplitNorPaginate");
        assert from == 0 : "getListBooks: DontSplitNorPaginate, from=" + from;
        willSplitByLetter = false;
        willSplitByDate = false;
        break;
      case DontSplit:
        // Bug #716917 Do not split on letter (used in Author and Series book lists)
        if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=DontSplit");
        assert from == 0 : "getListBooks: DontSplit, from=" + from;
        willSplitByLetter = false;
        willSplitByDate = false;
        break;
      case SplitByDate:
        if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=SplitByDate");
        assert from == 0 : "getListBooks: splitByDate, from=" + from;
        willSplitByLetter = checkSplitByLetter(splitOption, listobjects.size());
        willSplitByDate = true;
        break;
      case SplitByLetter:
        if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=SplitByLetter");
        assert from == 0 : "getListBooks: splitByLetter, from=" + from;
        willSplitByLetter = checkSplitByLetter(splitOption, listobjects.size());
        willSplitByDate = false;
        break;
      default:
        // ITIMPI:  Not sure that this case can ever arise
        //          Just added as a safety check
        if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=" + splitOption);
        assert from == 0 : "getListBooks: unknown splitOption, from=" + from;
        willSplitByLetter = checkSplitByLetter(splitOption, listobjects.size());
        willSplitByDate = false;
        break;
    }
    // See if SplitByLetter conditions actually apply?
    if ((currentProfile.getBrowseByCover())
        &&  (currentProfile.getBrowseByCoverWithoutSplit())) {
      willSplitByLetter = false;
    }
    if (logger.isTraceEnabled()) logger.trace("getListOfBooks:willSplitByLetter=" + willSplitByLetter);
    if (logger.isTraceEnabled()) logger.trace("getListOfBooks:willSplitByDate=" + willSplitByDate);
    if (logger.isTraceEnabled()) logger.trace("listing books from=" + from + ", title=" + title);

    int pageNumber = Summarizer.getPageNumber(from + 1);
    int maxPages = Summarizer.getPageNumber((willSplitByDate || willSplitByLetter) ? 0 : catalogSize);

    // generate the book list files
    String filename = pFilename + Constants.PAGE_DELIM + Integer.toString(pageNumber);
    String urlExt = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, pBreadcrumbs.size() > 1  || inSubDir);

    Element feed;
    feed = FeedHelper.getFeedRootElement(pBreadcrumbs, title, urn, urlExt, true);
    // Update breadcrumbs ready for next iteration
    Breadcrumbs breadcrumbs;
    // #c2o-204 breadrumbs should already be correct if listing firt page of books for an author.
    if (from ==0 && getCatalogFolder().startsWith(Constants.AUTHOR_TYPE)) {
      breadcrumbs = pBreadcrumbs;
    } else {
      breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, urlExt);
    }

    // list the books (or split them)
    List<Element> result;
    if (willSplitByDate) {
      // Split by date listing
      result = getListOfObjectsSplitByDate( breadcrumbs,
                                            DataModel.splitObjectsByDate(listobjects),
                                            true,   // Musy be true if splitting by date
                                            title,
                                            urn,
                                            pFilename,
                                            icon,
                                            options);
    } else if (willSplitByLetter) {
      // Split by letter listing
      result = getListOfObjectsSplitByLetter( breadcrumbs,
                                              DataModel.splitObjectsByLetter(listobjects),
                                              true, // Must be true if splitting by letter
                                              title,
                                              urn,
                                              pFilename,
                                              SplitOption.SplitByLetter,
                                              icon,
                                              options);
    } else {
      // Paginated listing
      result = new LinkedList<Element>();
      String progressText = Breadcrumbs.getProgressText(breadcrumbs);
      progressText += " (" + Summarizer.getBookWord(listobjects.size()) + ")";
      CatalogManager.callback.showMessage(progressText.toString());
      for (int i = from; i < listobjects.size(); i++) {
        // check if we must continue
        CatalogManager.callback.checkIfContinueGenerating();

        // See if we need to do the next page
        if ((splitOption != SplitOption.DontSplitNorPaginate) && ((i - from) >= maxBeforePaginate)) {
          // TODO #c2o-208   Add Previous, First and Last links if needed
          // ... YES - so go for next page
          if (logger.isDebugEnabled()) logger.debug("making a nextpage link");
          Element nextLink = getListOfObjects(pBreadcrumbs,
                                              listobjects,
                                              true,             // Awlays in SubDir (need to check this)
                                              i,                // Continue nfrom where we were
                                              title,
                                              summary,
                                              urn,
                                              pFilename, splitOption != SplitOption.DontSplitNorPaginate ? SplitOption.Paginate : splitOption,
                                              icon,
                                              null,             // No firstElements
                                              options);
          result.add(0, nextLink);
          break;
        } else {
          // ... NO - so add book to this page
          Object book = listobjects.get(i);
          if (logger.isTraceEnabled()) logger.trace("getListOfObjects: adding object to the list : " + book);
          try {
            logger.trace("getListOfBooks: breadcrumbs=" + breadcrumbs + ", book=" + book + ", options=" + options);
            Element entry = getDetailedEntry(breadcrumbs, book, options);
            if (entry != null) {
              if (logger.isTraceEnabled()) logger.trace("getListOfBooks: entry=" + entry);
              result.add(entry);
              TrookSpecificSearchDatabaseManager.addBook((Book) book, entry);
            }
          } catch (RuntimeException e) {
            logger.error("getListOfBooks: Exception on book: " + ((Book)book).getDisplayName() + "[" + ((Book)book).getId() + "]", e); Helper.statsErrors++;
            throw e;
          }
        }
      }
    }
    // if needed, add the first elements to the feed
    if (Helper.isNotNullOrEmpty(firstElements))
      feed.addContent(firstElements);
    // add the book entries to the feed
    feed.addContent(result);

    Element entry;
    String urlInItsSubfolder = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, inSubDir);

    entry = createPaginateLinks(feed, filename, pageNumber, maxPages);
    createFilesFromElement(feed, filename, HtmlManager.FeedType.Catalog);
    if (from == 0) {
      entry = FeedHelper.getCatalogEntry(title, urn, urlInItsSubfolder, summary, icon);
    }
    return entry;
  };
*/

  /**
   * Produce a list plit by letter
   *
   * @param pBreadcrumbs
   * @param mapOfObjectsByLetter
   * @param inSubDir
   * @param baseTitle
   * @param baseUrn
   * @param baseFilename
   * @param splitOption
   * @param icon
   * @param options
   * @return
   * @throws IOException
   */
/*
  protected List<Element> getListOfObjectsSplitByLetter(
      Breadcrumbs pBreadcrumbs,
      Map<String,  List<? extends GenericDataObject>> mapOfObjectsByLetter,
      boolean inSubDir,
      String baseTitle,
      String baseUrn,
      String baseFilename,
      SplitOption splitOption,
      String icon,
      Option... options) throws IOException {
    if (Helper.isNullOrEmpty(mapOfObjectsByLetter))
      return null;

    if (pBreadcrumbs.size() > 1) inSubDir = true;

    String sTitle = baseTitle;
    if (Helper.isNotNullOrEmpty(sTitle))
      sTitle += ", ";

    List<Element> result = new LinkedList<Element>();
    SortedSet<String> letters = new TreeSet<String>(mapOfObjectsByLetter.keySet());
    for (String letter : letters) {
      // generate the letter file
      String letterFilename = Helper.getSplitString(baseFilename, letter, Constants.TYPE_SEPARATOR);
      String letterUrn = Helper.getSplitString(baseUrn, letter, Constants.URN_SEPARATOR);

      List<? extends GenericDataObject> objectsInThisLetter = mapOfObjectsByLetter.get(letter);
      String letterTitle;
      if (letter.equals("_"))
        letterTitle = Localization.Main.getText("splitByLetter.book.other");
      else
        letterTitle = Localization.Main.getText("splitByLetter.letter", Localization.Main.getText("bookword.title"),
            letter.length() > 1 ? letter.substring(0,1) + letter.substring(1).toLowerCase() : letter);

      // try and list the items to make the summary
      String summary = Summarizer.summarizeBooks(objectsInThisLetter);

      Element element = null;
      if (objectsInThisLetter.size() > 0) {
        element = getListOfObjects(pBreadcrumbs,
                                  objectsInThisLetter,
                                  true,              // Always inSubDir if in letter
                                  0,                 // start at first page
                                  letterTitle,
                                  summary,
                                  letterUrn,
                                  letterFilename,
                                  checkSplitByLetter(letter),
                                  icon,
                                  null,              // No firstElements
                                  options);
      }
      else
      {
        // ITIMPI:  Assert to check if the logic can ever let this be zero!
        assert (objectsInThisLetter.size() <= 0) : "booksInThisLetter=" + objectsInThisLetter.size() + " for letter '" + letter + "'";
      }

      if (element != null)
        result.add(element);
    }
    return result;

  };
*/
  /**
   * Produce a list split by date
   *
   * Only used as part of the 'recent' section at the moment
   * so maybe it should only be present in that calss?
   *
   * @param pBreadcrumbs
   * @param mapOfObjectsByDate
   * @param inSubDir
   * @param baseTitle
   * @param baseUrn
   * @param baseFilename
   * @param icon
   * @param options
   * @return
   * @throws IOException
   */
/*
  protected  List<Element> getListOfObjectsSplitByDate(
      Breadcrumbs pBreadcrumbs,
      Map<DateRange, List<GenericDataObject>> mapOfObjectsByDate,
      boolean inSubDir,
      String baseTitle,
      String baseUrn,
      String baseFilename,
      String icon,
      Option... options) throws IOException {
    if (Helper.isNullOrEmpty(mapOfObjectsByDate))
      return null;
    String sTitle = baseTitle;
    if (Helper.isNotNullOrEmpty(sTitle))
      sTitle = sTitle + ", ";

    if (pBreadcrumbs.size() > 1) inSubDir = true;

    List<Element> result = new LinkedList<Element>();
    SortedSet<DateRange> ranges = new TreeSet<DateRange>(mapOfObjectsByDate.keySet());
    for (DateRange range : ranges) {
      // generate the range file
      String rangeFilename = baseFilename + Constants.TYPE_SEPARATOR + range;

      String rangeUrn = Helper.getSplitString(baseUrn, range.toString(), Constants.URN_SEPARATOR);

      String rangeTitle = LocalizationHelper.getEnumConstantHumanName(range);
      List<? extends GenericDataObject> booksInThisRange = mapOfObjectsByDate.get(range);

      // try and list the items to make the summary
      String summary = Summarizer.summarizeBooks(booksInThisRange);

      Element element = null;
      if (booksInThisRange.size() > 0) {
        element = getListOfObjects(pBreadcrumbs,
                                  booksInThisRange,
                                  true,         // Always inSubDir
                                  0,            // Start at first page
                                  rangeTitle,
                                  summary,
                                  rangeUrn,
                                  rangeFilename,
                                  SplitOption.Paginate,
                                  icon,
                                  null,
                                  options);
      }

      if (element != null)
        result.add(element);
    } // end of for
    return result;
  }
*/
//  public abstract <T extends GenericDataObject> List<T> getObjectList() ;

  /**
   * Get the detailed entry for this object type
   * We need to over-ride this method in each subcatalog type
   * as the details are going to be very type dependent.
   *
   * @param pBreadcrumbs
   * @param obj
   * @param options
   * @return
   * @throws IOException
   */
  public abstract Element getDetailedEntry(Breadcrumbs pBreadcrumbs,
                                          Object  obj,
                                          Option... options) throws IOException;

}
