import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Scanner;

/**
 *  BackendInterface implementation which reads data from a csv and makes use of Tree_Placeholder
 *  to insert read songs. Contains methods to set the range by song year, filter by loudness, and
 *  obtain the five most danceable songs.
 *
 */
public class Backend implements BackendInterface {
  private IterableSortedCollection<Song> tree;
  private List<String> currentList;

  public Backend(IterableSortedCollection<Song> tree) {
    this.tree = tree;
    this.currentList = new ArrayList<>();
  }
  /**
   * Loads data from the .csv file referenced by filename.  You can rely on the exact headers found
   * in the provided songs.csv, but you should not rely on them always being presented in this order
   * or on there not being additional columns describing other song qualities. After reading songs
   * from the file, the songs are inserted into the tree passed to this backend' constructor.  Don't
   * forget to create a Comparator to pass to the constructor for each Song object that you create.
   * This will be used to store these songs in order within your tree, and to retrieve them by year
   * range in the getRange method.
   *
   * @param filename is the name of the csv file to load data from
   * @throws IOException when there is trouble finding/reading file
   */
  @Override
  public void readData(String filename) throws IOException {
    // create Comparator to pass to constructor
    CompareYear songComparator = new CompareYear();
    Scanner scanner = null;
    try {
      scanner = new Scanner(new File(filename));

      if (!scanner.hasNextLine()) {
        return; // handles empty file
      }

      String headerLn = scanner.nextLine(); // read header line and determine order of values
      String[] header = headerLn.split(","); // store the header values in an array

      while (scanner.hasNextLine()) {
        // parse and handle quoted fields
        String song = scanner.nextLine(); // read each song data line
        List<String> data = parseLine(song);

        // initialize song values
        String title = "", artist = "", topGenre = "";
        int year = 0,bpm = 0,nrgy = 0,dnce = 0,dB = 0,live = 0;

        for (int i = 0; i < header.length; i++) { // pull each data value out of the arrays
          String headerName = header[i].trim();
          String songData = data.get(i).trim();

          switch (headerName) { // assign each data value to the header name
            case "title": title = songData; break;
            case "artist": artist = songData; break;
            case "top genre": topGenre = songData; break;
            case "year": year = Integer.parseInt(songData); break;
            case "bpm": bpm = Integer.parseInt(songData); break;
            case "nrgy": nrgy = Integer.parseInt(songData); break;
            case "dnce": dnce = Integer.parseInt(songData); break;
            case "dB": dB = Integer.parseInt(songData); break;
            case "live": live = Integer.parseInt(songData); break;
          }
        }
        // create new Song object after parsing the line and insert it into the tree
        Song newSong = new Song(title, artist, topGenre, year, bpm, nrgy, dnce, dB, live,
            songComparator);
        tree.insert(newSong);
      }
      scanner.close();
    } catch (IOException e) {
      System.out.println("Trouble finding/reading file");
    }
  }

  /**
   * Helper method to parse a single line into a String list
   * @param line to parse into a list
   * @return list of parsed Strings
   */
  private List<String> parseLine(String line) {
    List<String> parsed = new ArrayList<>();
    String str = "";
    boolean inQuotes = false;
    for (int i = 0; i < line.length(); i++) {
      char c = line.charAt(i);
      if (!inQuotes) { // not in quotes
          if (c == ',') {
            parsed.add(str);
            str = "";
          } else if (c == '\"') {
            inQuotes = true;
          } else {
            str += c;
          }
      } else { // in quotes
        if (c == '"') {
          if (line.charAt(i + 1) == '\"') {
            str += '\"';
          } else {
            inQuotes = false;
          }
        } else {
          str += c;
        }
      }
    }
    return parsed;
  }
  /**
   * Creates comparator to compare the years of two Song objects
   */
  class CompareYear implements Comparator<Song> {
    /**
     * Compare song years
     * @param s1 the first song to be compared.
     * @param s2 the second song to be compared.
     * @return negative if s1 < s2, positive if s1 > s2
     */
    @Override
    public int compare(Song s1, Song s2) {
      return Integer.compare(s1.getYear(), s2.getYear());
    }
  }
  /**
   * Retrieves a list of song titles from the tree passed to the constructor. The songs should be
   * ordered by the songs' year, and fall within the specified range of year values.  This year
   * range will also be used by future calls to filterSongs and getFiveMost.
   *
   * If a loudness filter has been set using the filterSongs method below, then only songs that pass
   * that filter should be included in the list of titles returned by this method.
   *
   * When null is passed as either the low or high argument to this method, that end of the range is
   * understood to be unbounded.  For example, a null argument for the height parameter means that
   * there is no maximum year to include in the returned list.
   *
   * @param low  is the minimum year of songs in the returned list
   * @param high is the maximum year of songs in the returned list
   * @return List of titles for all songs from low to high that pass any set filter, or an empty
   * list when no such songs can be found
   */
  @Override
  public List<String> getAndSetRange(Integer low, Integer high) {
    currentList.clear();
    for (Song song : tree) {
      // high and low value set --> add only songs within the range
      if (low != null && high != null) {
        if (song.getYear() >= low && song.getYear() <= high) {
          currentList.add(song.getTitle());
        }
      } else if (low == null && high == null) {
          currentList.add(song.getTitle());
      } else if (low == null) {
        if (song.getYear() <= high) { // high value set --> add all songs that are lower than high
          currentList.add(song.getTitle());
        }
      } else {
        if (song.getYear() >= low) { // low value set --> add all songs that are higher than low
          currentList.add(song.getTitle());
        }
      }
    }

    return currentList;
  }

  /**
   * Retrieves a list of song titles that have a loudness that is smaller than the specified
   * threshold.  Similar to the getRange method: this list of song titles should be ordered by the
   * songs' year, and should only include songs that fall within the specified range of year values
   * that was established by the most recent call to getRange.  If getRange has not previously been
   * called, then no low or high year bound should be used.  The filter set by this method will be
   * used by future calls to the getRange and fiveMost methods.
   *
   * When null is passed as the threshold to this method, then no loudness threshold should be used.
   * This clears the filter.
   *
   * @param threshold filters returned song titles to only include songs that have a loudness that
   *                  is smaller than this threshold.
   * @return List of titles for songs that meet this filter requirement and are within any
   * previously set year range, or an empty list when no such songs can be found
   */
  @Override
  public List<String> applyAndSetFilter(Integer threshold) {
    if (threshold == null) {
      return currentList;
    }
    List<String> filtered = new ArrayList<>();
    for (Song song : tree) {
      // filter through currentList and add songs that are within the threshold
      if (song.getLoudness() <= threshold) {
        filtered.add(song.getTitle());
      }
    }
    currentList = filtered;
    return currentList;
  }

  /**
   * This method returns a list of song titles representing the five most danceable songs that both
   * fall within any attribute range specified by the most recent call to getRange, and conform to
   * any filter set by the most recent call to filteredSongs.  The order of the song titles in this
   * returned list is up to you.
   *
   * If fewer than five such songs exist, return all of them.  And return an empty list when there
   * are no such songs.
   *
   * @return List of five most danceable song titles
   */
  @Override
  public List<String> fiveMost() {
    if (currentList.isEmpty()) {
      return new ArrayList<>(); // list is empty so return an empty list
    }

    List<Song> songs = new ArrayList<>();
    for (Song song : tree) { // collect all Song objects in currentList from the tree
      if (currentList.contains(song.getTitle())) {
        songs.add(song);
      }
    }
    List<String> topFive = new ArrayList<>();
    if (songs.size() <= 5) { // 5 or fewer songs in list so return all
      for (Song song : songs) {
        topFive.add(song.getTitle());
      }
      return topFive;
    }
    for (int i = 0; i < 5; i++) {
      int maxIndex = 0; // reset max
      for (int j = 0; j < songs.size(); j++) { // iterate over songs list until empty
        if (songs.get(j).getDanceability() > songs.get(maxIndex).getDanceability()) {
          maxIndex = j; // set max index to the song with the greatest danceability in the list
        }
      }
      topFive.add(songs.get(maxIndex).getTitle()); // add the max to top five list
      songs.remove(maxIndex); // remove the added song from the songs list to prevent duplicates
    }
    return topFive;
  }
}
