import java.util.*;
import java.io.IOException;

/**
 * Backend class that implements BackendInterface for song sorting application
 */
public class Backend implements BackendInterface {
    
    // Private fields to store relevant fields
    private IterableSortedCollection<Song> songTree; // stores the songs

    private Integer currentLowYear; // lowest year
    private Integer currentHighYear; // highest year
    private Integer currentLoudnessThreshold;
    
    // Column indices determined from header
    private int danceabilityIndex = -1;
    private int loudnessIndex = -1;
    private int livenessIndex = -1;
    private int titleIndex = -1;
    private int artistIndex = -1;
    private int genreIndex = -1;
    private int yearIndex = -1;
    private int bpmIndex = -1;
    private int energyIndex = -1;
    

    private int maxRequiredColumnIndex = -1;
    
    /**
     * Constructor that takes an IterableSortedCollection of Songs
     * @param tree the sorted collection to store songs
     */
    public Backend(IterableSortedCollection<Song> tree) {
        this.songTree = tree; // set tree
        this.currentLowYear = null; // set lowest year
        this.currentHighYear = null; // set highest year
        this.currentLoudnessThreshold = null; // default loudness threshold
    }
    
    /**
     * Loads data from the csv file referenced by filename
     * @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 {
        // Reset column indices for each new file 
        resetColumnIndices();
        
        // create scanner object to read in file.
        java.util.Scanner scanner = null;

        try { // read in new csv file.
            scanner = new java.util.Scanner(new java.io.File(filename));
            
            // Read and parse header line to determine column positions
            if (scanner.hasNextLine()) {
                String headerLine = scanner.nextLine();
                parseHeader(headerLine); // 
            }
            
            // Validate that we found all required columns
            if (!allRequiredColumnsFound()) {
                throw new IOException("file missing and required columns");
            }
            
            // Create a comparator for sorting songs by year
            Comparator<Song> yearComparator = new Comparator<Song>() {

                // Keeps track of which song came out first.
                @Override
                public int compare(Song s1, Song s2) {
                    return Integer.compare(s1.getYear(), s2.getYear());
                }
            };
            
            // Process each data line
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine().trim(); // get rid of white space at end.

                if (line.isEmpty()) continue; // skip empty lines
                
                try { // parse 
                    Song song = parseSongFromCSV(line, yearComparator);
                    if (song != null) {
                        songTree.insert(song);
                    }
                } catch (Exception e) {
                    // Skip borken lines but continue processing
                    System.err.println("Skipping broken line: " + line);
                }
            }
        } finally { // close the scanner
            if (scanner != null) {
                scanner.close();
            }
        }
    }
    
    /**
     * Parses the header line to determine column positions
     */
    private void parseHeader(String headerLine) {
        // create array to store the locations of each column name.
        String[] headers = parseCSVLine(headerLine);
        
        // iterate over the length of the header line and 
        // match each header to an index.
        for (int i = 0; i < headers.length; i++) {
            // check for case sensitivity 
            String header = headers[i].toLowerCase().trim();
            
            // case by case matching with renaming of columns to actual english.
            switch (header) {
                case "title":
                    titleIndex = i;
                    break;
                case "artist":
                    artistIndex = i;
                    break;
                case "top genre":
                case "genre":
                    genreIndex = i;
                    break;
                case "year":
                    yearIndex = i;
                    break;
                case "bpm":
                    bpmIndex = i;
                    break;
                case "nrgy":
                case "energy":
                    energyIndex = i;
                    break;
                case "dnce":
                case "danceability":
                    danceabilityIndex = i;
                    break;
                case "db":
                case "loudness":
                    loudnessIndex = i;
                    break;
                case "live":
                case "liveness":
                    livenessIndex = i;
                    break;
            }
        }

        this.maxRequiredColumnIndex = getMaxRequiredColumnIndex();
    }
    
    /**
     * Resets all column indices before reading a new file
     */
    private void resetColumnIndices() {
        // not sure if we will need this in the future but yeah.
        titleIndex = -1;
        artistIndex = -1;
        genreIndex = -1;
        yearIndex = -1;
        bpmIndex = -1;
        energyIndex = -1;
        danceabilityIndex = -1;
        loudnessIndex = -1;
        livenessIndex = -1;
    }
    
    /**
     * Checks if all required columns were found in the header
     */
    private boolean allRequiredColumnsFound() {
        return titleIndex >= 0 && artistIndex >= 0 && genreIndex >= 0 &&
               yearIndex >= 0 && bpmIndex >= 0 && energyIndex >= 0 &&
               danceabilityIndex >= 0 && loudnessIndex >= 0 && livenessIndex >= 0;
    }
    
    /**
    * Parses a CSV line into a Song object using column indices from header
     */
    private Song parseSongFromCSV(String line, Comparator<Song> comparator) {
        try {
            String[] parts = parseCSVLine(line); // create array to store fields.
        
            // Check if we have enough columns using new helper method
            if (!hasEnoughColumns(parts)) {
                return null; // Not enough columns
            }
        
            // extract values using the column indices determined from header
            int year = parseIntSafe(getCSVValue(parts, yearIndex));
            int bpm = parseIntSafe(getCSVValue(parts, bpmIndex));
            int energy = parseIntSafe(getCSVValue(parts, energyIndex));
            int danceability = parseIntSafe(getCSVValue(parts, danceabilityIndex));
            int loudness = parseIntSafe(getCSVValue(parts, loudnessIndex));
            int liveness = parseIntSafe(getCSVValue(parts, livenessIndex));
            
            String title = getCSVValue(parts, titleIndex);
            String artist = getCSVValue(parts, artistIndex);
            String genre = getCSVValue(parts, genreIndex);
        
            // create new song object with the parsed fields.
            return new Song(title, artist, genre, year, bpm, energy, 
                        danceability, loudness, liveness, comparator);

        } catch (Exception e) { // error thrown if parsing failed.
            System.err.println("problem on line: " + line + "   " + e.getMessage());
            return null;
        }
    }

    /**
     * Checks if the CSV row has enough columns based on the required column indices
     */
    private boolean hasEnoughColumns(String[] parts) {
        int maxRequiredIndex = getMaxRequiredColumnIndex();
        return parts.length > maxRequiredIndex;
    }

    /**
     * Calculates the maximum column index needed from all required fields
     */
    private int getMaxRequiredColumnIndex() {
        return Math.max(Math.max(titleIndex, artistIndex), 
            Math.max(genreIndex, 
            Math.max(yearIndex, 
            Math.max(bpmIndex, 
            Math.max(energyIndex, 
            Math.max(danceabilityIndex, 
            Math.max(loudnessIndex, livenessIndex)))))));
    }
        
    /**
     * returns and cleans CSV values
     */
     private String getCSVValue(String[] parts, int index) {
            
            // skip over missing values.
            if (index < 0 || index >= parts.length) {
                return "0"; // Default value for missing numeric fields
            }
            
            String value = parts[index].trim(); // get rid of white space.
            
            // check if a string is supposed to be in quotes.
            if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {

                //remove extra quotes.
                value = value.substring(1, value.length() - 1);
                
                // Replace escaped quotes with single quotes
                value = value.replace("\"\"", "\"");
            }
            
            return value; // return the final parsed value 
    }
    
    /**
     * parses an integer
     */
    private int parseIntSafe(String value) {

        try { // try and simply return the string as an integer.
            return Integer.parseInt(value);

        } catch (NumberFormatException e) { // throw error if above doesnt work.
            return 0; 
        }
    }
    
    /**
     * parser that  handles quoted fields
     */
    private String[] parseCSVLine(String line) {
        List<String> result = new ArrayList<>(); // create list to store parsed line results.
        boolean quoted = false; // dummy variable to track quotes.
        StringBuilder field = new StringBuilder(); 
        
        for (int i = 0; i < line.length(); i++) {
            char a = line.charAt(i);
            
            if (a == '"') {
                // Handle escaped character quotes 
                if (quoted && i + 1 < line.length() && line.charAt(i + 1) == '"') {
                    field.append('"'); // make sure first quotes are kept.
                    i++; // Skip the next quote
                } else {
                    quoted = !quoted; // update dummy variable.
                }
            } else if (a == ',' && !quoted) {
                // End of column value
                result.add(field.toString()); 
                field.setLength(0);
            } else {
                field.append(a); // just add the field if no quote adjustments are necessary.
            }
        }

        result.add(field.toString()); // Add last field
        
        return result.toArray(new String[0]); // return an array of the parsed line.
    }
    
    /**
     * Retrieves a list of song titles from the tree with the specified year range
     * @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 the filter
     */
    @Override
    public List<String> getAndSetRange(Integer low, Integer high) {
        // Update the current year range
        this.currentLowYear = low;
        this.currentHighYear = high;
        
        return getFilteredSongs(); // return filtered songs based on the ranges.
    }
    
    /**
     * applies a loudness filter and gets matching song titles
     * @param threshold filters returned song titles to only include songs with smaller loudness
     * @return List of titles for songs that meet the filter and year range
     */
    @Override
    public List<String> applyAndSetFilter(Integer threshold) {
        // Update the current loudness threshold
        this.currentLoudnessThreshold = threshold;
        
        return getFilteredSongs();
    }
     
    /**
     * Helper method to get filtered song titles sorted by year
     */
    private List<String> getFilteredSongs() {
        List<Song> filteredSongs = getFilteredSongObjects();
        
        // Sort by year
        filteredSongs.sort(new Comparator<Song>() {
            
            @Override
            public int compare(Song s1, Song s2) {
                return Integer.compare(s1.getYear(), s2.getYear());
            }
        });
        
        // Extract titles
        List<String> result = new ArrayList<>();
        for (Song song : filteredSongs) {
            result.add(song.getTitle());
        }
        
        return result;
    }

    /**
     * Returns the five most danceable songs that match current filters
     * @return List of five most danceable song titles
     */
    @Override
    public List<String> fiveMost() {
        List<Song> filteredSongs = getFilteredSongObjects();
        
        // Sort by danceability in descending order
        // i ont remember how iterators work :(
            filteredSongs.sort(new Comparator<Song>() {
                @Override
                public int compare(Song s1, Song s2) {
                    int danceCompare = Integer.compare(s2.getDanceability(), s1.getDanceability());
                    if (danceCompare != 0) {
                        return danceCompare;
                    }
                    // If danceability is equal, sort by title as tie-breaker
                    return s1.getTitle().compareTo(s2.getTitle());
                }
            });
        
        // Return up to 5 song titles
        List<String> result = new ArrayList<>();
        int count = Math.min(5, filteredSongs.size());
        // loop through and add em!
        for (int i = 0; i < count; i++) {
            result.add(filteredSongs.get(i).getTitle());
        }
        
        return result;
    }
    
    /**
     * Helper method to get filtered song objects
     */
    private List<Song> getFilteredSongObjects() {
        List<Song> result = new ArrayList<>();
        
        // Iterate through all songs in the tree
        for (Song song : songTree) {
            if (passesYearFilter(song) && passesLoudnessFilter(song)) {
                result.add(song);
            }
        }
        
        return result;
    }
    
    /**
     * Checks if a song passes the current year filter
     */
    private boolean passesYearFilter(Song song) {
        // YET ANOTHER Hleper Method!!
        if (currentLowYear != null && song.getYear() < currentLowYear) {
            return false;
        }
        if (currentHighYear != null && song.getYear() > currentHighYear) {
            return false;
        }
        return true;
    }
    
    /**
     * Checks if a song passes the current loudness filter
     */
    private boolean passesLoudnessFilter(Song song) {
        if (currentLoudnessThreshold == null) {
            return true; // No filter set
        }
        return song.getLoudness() < currentLoudnessThreshold;
    }
    @Override
    public List<String> getMostDanceable(int count) {
        List<Song> filteredSongs = getFilteredSongObjects();

        // Sort by danceability
        filteredSongs.sort(new Comparator<Song>() {
            @Override
            public int compare(Song s1, Song s2) {
                int danceCompare = Integer.compare(s2.getDanceability(), s1.getDanceability());
                if (danceCompare != 0) return danceCompare;
                return s1.getTitle().compareTo(s2.getTitle());
            }
        });

        // Collect up to 'count' songs
        List<String> result = new ArrayList<>();
        for (int i = 0; i < Math.min(count, filteredSongs.size()); i++) {
            result.add(filteredSongs.get(i).getTitle());
        }
        return result;
    }
}

