package hirondelle.movies.edit;

import java.util.*;
import java.io.*;
import java.util.regex.Pattern;
import java.util.logging.Logger;
import hirondelle.movies.util.Util;
import java.math.BigDecimal;

import hirondelle.movies.exception.InvalidInputException;
import hirondelle.movies.main.MainWindow;

/**
 Data Access Object (DAO) for {@link Movie} objects.
 
  <P> Implements persistence for movie information. This class uses a simple text file called
  <tt>movie_list_for_<<em>user name</em>>.txt</tt>, stored locally, in the application's
  home directory.
  Each logged in user gets their own list. Each logged in user can see their own list, 
  but they cannot see anyone else's list.
  
  <P>The format of the file is specific to this application. The file should not be edited 
  directly by an end user, in case the format is violated.
   
  <P>Upon startup, all records in the datastore are read into memory. Edits are performed initially
  only in memory. When the application shuts down, then the updated data store is written
  back to the disk, for use during the next launch of the application.
 */
public final class MovieDAO {

  /**
    Save all data to a text file. Must be called explicitly when the
    app shuts down, in order to save all edits.
  */
  public void shutdown() {
    fLogger.fine("Saving all miovie records to file.");
    String fileContents = buildFileContents();
    writeStringToFile(fileContents);
  }

  /** Add a new {@link Movie}. */
  void add(Movie aMovie) {
    String id = nextId();
    aMovie.setId(id.toString());
    fTable.put(id, aMovie);
  }

  /** Change an existing {@link Movie}. */
  void change(Movie aMovie) {
    fTable.put(aMovie.getId(), aMovie);
  }

  /**
   * List all {@link Movie}s. Order is the natural order of the {@link Movie} class
   * (descending date, then title).
   */
  List<Movie> list() {
    List<Movie> result = new ArrayList<Movie>(fTable.values());
    Collections.sort(result);
    return result;
  }

  /** Delete an existing {@link Movie}, given the movie id. */
  void delete(String aMovieId) {
    fTable.remove(aMovieId);
  }

  // PRIVATE //
  private static final Map<String, Movie> fTable = new LinkedHashMap<String, Movie>();
  private static int fNextId = 0;
  private static final String MOVIES_FILE_NAME = "movie_list_for_";
  private static final String ENCODING = "UTF-8";
  private static final String DELIMITER = "|";
  private static final String NULL = "NULL";
  private static final Logger fLogger = Util.getLogger(MovieDAO.class);

  static {
    readInMovieFileUponStartup();
    fLogger.config("Number of movies read in from file: " + fTable.size());
  }

  private static void readInMovieFileUponStartup() {
    File file = new File(getMovieFileName());
    fLogger.fine("Reading movies from file :" + file.getAbsolutePath());
    String line = "";
    Scanner scanner = null;
    try {
      scanner = new Scanner(file);
      while (scanner.hasNextLine()) {
        line = scanner.nextLine();
        if (Util.textHasContent(line)) {
          parseLine(line);
        }
      }
    }
    catch (FileNotFoundException ex) {
      fLogger.config("Movies  file not present. Will be created when the app closes.");
    }
    catch (InvalidInputException ex) {
      fLogger.severe("Movies file: date-viewed field not in expected format: " + line);
    }
    catch (InputMismatchException ex) {
      fLogger.severe("Movies file: Not in expected format: " + line);
    }
    catch (NoSuchElementException ex) {
      fLogger.severe("Movies file: Not in expected format: " + line);
    }
    finally {
      if (scanner != null) scanner.close();
    }
  }

  private static void parseLine(String aLine) throws InvalidInputException {
    Scanner scanner = new Scanner(aLine);
    // note how the quoting is needed here, since '|' is a special character in
    // regular expressions :
    scanner.useDelimiter(Pattern.quote(DELIMITER));
    scanner.useLocale(Locale.US);
    if (scanner.hasNext()) {
      String title = scanner.next();
      Date viewed = Util.parseDate(maybeNull(scanner.next()), "Date Viewed");
      BigDecimal rating = Util.parseBigDecimal(maybeNull(scanner.next()), "Rating");
      String comment = maybeNull(scanner.next());
      Movie movie = new Movie(nextId().toString(), title, viewed, rating, comment);
      fTable.put(movie.getId(), movie);
    }
    scanner.close();
  }

  private static String nextId() {
    ++fNextId;
    return String.valueOf(fNextId);
  }

  private void appendTo(StringBuilder aText, Object aField, String aAppend) {
    if (Util.textHasContent(Util.format(aField))) {
      aText.append(Util.format(aField));
    }
    else {
      aText.append(NULL);
    }
    aText.append(aAppend);
  }

  private static String maybeNull(String aText) {
    return NULL.equals(aText) ? null : aText;
  }

  private static String getMovieFileName() {
    return MOVIES_FILE_NAME + MainWindow.getInstance().getUserName().toLowerCase(Locale.ENGLISH) + ".txt";
  }

  /** Create a string, holding all movie records. */
  private String buildFileContents() {
    String NEW_LINE = System.getProperty("line.separator");
    StringBuilder result = new StringBuilder();
    for (Movie movie : fTable.values()) {
      appendTo(result, movie.getTitle(), DELIMITER);
      appendTo(result, movie.getDateViewed(), DELIMITER);
      appendTo(result, movie.getRating(), DELIMITER);
      appendTo(result, movie.getComment(), NEW_LINE);
    }
    return result.toString();
  }

  /** Write string containing all movie records to a file - overwrite the whole file. */
  private void writeStringToFile(String aFileContents) {
    Writer output = null;
    try {
      FileOutputStream fos = new FileOutputStream(getMovieFileName());
      OutputStreamWriter out = new OutputStreamWriter(fos, ENCODING);
      output = new BufferedWriter(out);
      output.write(aFileContents);
    }
    catch (FileNotFoundException ex) {
      fLogger.severe("Cannot find movies.txt file.");
    }
    catch (UnsupportedEncodingException ex) {
      fLogger.severe("System does not support UTF-8 encoding.");
    }
    catch (IOException ex) {
      fLogger.severe("Problem while saving movies.txt file.");
    }
    finally {
      if (output != null) {
        try {
          output.close();
        }
        catch (IOException ex) {
          fLogger.severe("Cannot close stream.");
        }
      }
    }
  }
}