package ch.usi.inf.ajp22;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Main {

    /**
     * 3 Points
     * create a public static Album method called "longestTitle".
     * If the maximum is not found throw a RuntimeException.
     * input: a list of Album
     * output: the Album with the longest title.
     */

    public static Album longestTitle(List<? extends Album> albums) {
        return albums.stream()
                .max(Comparator.comparingDouble(a -> a.getTitle().length()))
                .orElseThrow(RuntimeException::new);
    }

    /**
     * 4 Points
     * create a public static int method called "sumOfRatingReduce".
     * input: an Album instance.
     * output: int that is the sum of all the tracks ratings on the album.
     * Use the method getTracks from the Album class to get the List of tracks.
     * You MUST use the reduce method, in the variant which takes as input an identity, an accumulator and a combiner.
     * If possible use method references.
     */
    public static int sumOfRatingReduce(Album album) {
        return album.getTracks().stream()
                .map(Track::getLength)
                .reduce(0, Integer::sum, Integer::sum);
    }

    /**
     * 4 Points
     * create a public static int method called "sumOfRatingCollection".
     * input: an Album instance.
     * output: int that is the sum of all the tracks ratings on the album
     * Use the method getTracks from the Album class to get the List of tracks.
     * You MUST use one variant of the collect method. If possible use method references.
     */
    public static int sumOfRatingCollection(Album album) {
        //noinspection SimplifyStreamApiCallChains
        return album.getTracks().stream().map(Track::getRating)
                .collect(Collectors.reducing(0, Integer::sum));
    }


    /**
     * 4 Points
     * create a public static int method called "sumOfRatingMap"
     * input: a ch.usi.inf.ajp22.Album instance.
     * output: int that is the sum of all the tracks ratings on the album.
     * Use the method getTracks from the Album class to get the List of tracks.
     * You MUST use the mapToInt method. If possible use method references.
     */
    public static int sumOfRatingMap(Album album) {
        return album.getTracks().stream().mapToInt(Track::getRating).sum();
    }

    /**
     * 4 Points
     * create a public static Map<String, Long> method called "countTrackOccurrence"
     * input: a list of Track
     * output: a Map<String, Long> where the key is the track title and the value is, how many times
     * the same song occurs in the list.
     * Two songs can be considered the same if they have the same title.
     */
    public static Map<String, Long> countTrackOccurrence(List<? extends Track> tracks) {
        return tracks.stream().collect(Collectors.groupingBy(Track::getTitle, Collectors.counting()));
    }

    /**
     * 4 Points
     * create a public static Map<Artist, List<Album>> method called "groupAlbumByArtist".
     * input: a list of Album
     * output: a Map<Artist, List<Album>> where the key is an Artist and the value is a List of Album which
     * this artist had produced.
     */
    public static Map<Artist, List<Album>> groupAlbumByArtist(List<? extends Album> albums) {
        return albums.stream().collect(Collectors.groupingBy(Album::getArtist, Collectors.toList()));
    }

    /**
     * 4 Points
     * create a public static List<Track> method called "trackFilteredWithPredicate".
     * input: a stream of tracks and a predicate to apply to the track stream.
     * output: a List of Track that has been filtered according to the predicate taken as input.
     */
    public static List<Track> trackFilteredWithPredicate(Stream<? extends Track> tracks, Predicate<? super Track> predicate) {
        return tracks.filter(predicate).collect(Collectors.toList());
    }

    /**
     * 5 Points
     * create a public static Stream<Album> method called getAlbumWithAtLeastNTracks
     * input: a list of Album AND an int called "n"
     * output: a List<Album> where every album in this stream has at least "n" tracks.
     */
    public static Stream<Album> getAlbumWithAtLeastNTracks(List<Album> albums, int n) {
        return albums.stream().filter(a -> a.getTracks().size() >= n);
    }

    /**
     * 5 Points
     * create a public static void method called "printTrackStatistics".
     * input: an Album instance
     * Print Track Statistics based on the length of the tracks of an album taken as input in the following format:
     * ===
     * "Stat for: %s\n", album.getTitle()
     * "Max: %d\nMin: %d\nAve: %f\n"
     * ===
     * Use the method getTracks from the Album class to get the List of tracks.
     * Input: an Album
     */
    public static void printTrackStatistics(Album album) {
        final Supplier<IntStream> tracksStreamSupplier = () -> album.getTracks().stream().mapToInt(Track::getLength);
        final Supplier<IllegalArgumentException> whenZero = () -> {
            throw new IllegalArgumentException("Album has no tracks");
        };

        System.out.printf("===\nStat for: %s\nMax: %d\nMin: %d\nAve: %f\n===\n",
                album.getTitle(),
                tracksStreamSupplier.get().max().orElseThrow(whenZero),
                tracksStreamSupplier.get().min().orElseThrow(whenZero),
                tracksStreamSupplier.get().average().orElseThrow(whenZero));
    }

    /**
     * 5 Points
     * create a public static String method called "getArtistNameAndNickNameFromAlbum".
     * Each author should appear only once.
     * input: a list of Album
     * output: a String in the following format:
     * [artist1.name - artist1.nickname, artist2.name - artist2.nickname, ...]
     */
    public static String getArtistNameAndNickNameFromAlbum(List<? extends Album> albums) {
        return albums.stream().map(Album::getArtist).distinct()
                .map(a -> String.format("%s - %s", a.getName(), a.getNickname()))
                .collect(Collectors.joining(",", "[", "]"));
    }

    /**
     * 5 Points
     * create a public Track method called "combineAllTrackInAlbum"
     * input: a stream of track
     * output: return a new ch.usi.inf.ajp22.Track with the following values:
     * title -> "fake title"
     * releaseDate -> LocaleDate.now()
     * genre -> DISCO
     * length -> the sum of all the lengths in the input track stream.
     * The pipeline processing must be done in parallel.
     * You MUST use the collect method, in the variant which takes as input a supplier, an accumulator and a
     * combiner.
     */
    public static Track combineAllTrackInAlbum(Stream<? extends Track> tracks) {
        return tracks.parallel().collect(
                () -> new Track("fake title", LocalDate.now(), Track.Genre.DISCO, 0),
                (acc, track) -> acc.setLength(acc.getLength() + track.getLength()),
                (acc, other) -> acc.setLength(acc.getLength() + other.getLength()));
    }

    /**
     * 5 Points
     * create a public static Map<Artist, List<Track>> method called "groupTrackByArtist".
     * input: a list of Album
     * output: a Map<Artist, List<Track>> where the key is an Artist and the value is a List of Track which
     * this artist had produced.
     */
    public static Map<Artist, List<Track>> groupTrackByArtist(List<? extends Album> albums) {
        return albums.stream().collect(
                Collectors.groupingBy(
                        Album::getArtist,
                        Collectors.flatMapping(a -> a.getTracks().stream(), Collectors.toList())));
    }

    /**
     * 5 Bonus Points
     * create a public static <T> T[] method called "createArray".
     * This method must create a generic array where the elements are in the same order as in the
     * input Collection.
     * In this function you need neither streams nor lambda.
     * input: a Collection<T> and an IntFunction<T[]>
     * output: an array of generic type
     */
    public static <T> T[] createArray(Collection<T> collection, IntFunction<T[]> generator) {
        return collection.toArray(generator);
    }

    private static void writeToFile(String s) throws IOException {
        BufferedWriter fw = new BufferedWriter(new FileWriter("artist.txt", true));
        fw.write(s);
        fw.close();
    }

    public static void main(String[] args) {
        Random random = new Random();

        /*
         * Initializing some SampleData
         */
        SampleData.appetiteForDestruction
                .getTracks().forEach(track ->
                        track.setRating(random.nextInt(5)));

        SampleData.randomAccessMemories
                .getTracks().forEach(track ->
                        track.setRating(random.nextInt(5)));

        /*
         * 2 Points
         *       Replace the Anonymous class below with a lambda expression.
         */
        BinaryOperator<Track> mixTrack = (t1, t2) -> new Track(t1.getTitle(),
                t1.getReleasedDate(),
                t2.getGenre(),
                t2.getLength());

        /*
         * 3 Points
         *       Create a stream pipeline that:
         *       1) filter all the tracks that have at least 4 as rating
         *       2) sort the filtered tracks using the title field
         *       3) print the tracks
         */
        SampleData.getShuffledTrack().stream().filter(t -> t.getRating() >= 4)
                .sorted(Comparator.comparing(Track::getTitle))
                .forEach(System.out::println);

        /*
         * 4 Points
         *       Print on file the artist's name, obtained with the method SampleData.getArtistList(),
         *       using the writeToFile method. Use a lambda expression and handle
         *       the throw exception from the writeToFile method. If there is an IOException you must throw a
         *       RuntimeException with the same event.
         */
        SampleData.getArtistList().stream().map(Artist::getName).forEach(n -> {
            try {
                writeToFile(n);
            } catch (final IOException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.printf("Longest track in %s is %s\n",
                SampleData.appetiteForDestruction.getTitle(),
                SampleData.appetiteForDestruction.searchLongestSong().isPresent() ?
                        SampleData.appetiteForDestruction.searchLongestSong().get().getTitle() :
                        "Not found");

        System.out.printf("Ordered song in %s\n",
                SampleData.appetiteForDestruction.getTitle());
        SampleData.appetiteForDestruction.orderSongByTitle()
                .forEach(System.out::println);

        printTrackStatistics(SampleData.appetiteForDestruction);
        printTrackStatistics(SampleData.randomAccessMemories);

        SampleData.appetiteForDestruction
                .getTracks().forEach(System.out::println);
        System.out.print("Tot rating for:\n" + SampleData.appetiteForDestruction.getTitle());
        System.out.printf("\ttot rating (Reduce): %d\n", sumOfRatingReduce(SampleData.appetiteForDestruction));
        System.out.printf("\ttot rating (Collection): %d\n", sumOfRatingCollection(SampleData.appetiteForDestruction));
        System.out.printf("\ttot rating (mapToInt): %d\n", sumOfRatingMap(SampleData.appetiteForDestruction));

        System.out.println("Tot occurrence in the following list are\n");
        SampleData.getRepeatedTrack()
                .forEach(System.out::println);
        Map<String, Long> titleOccurrence = countTrackOccurrence(SampleData.getRepeatedTrack());

        for (Map.Entry<String, Long> entry : titleOccurrence.entrySet()) {
            System.out.printf("Title: %s occurrence: %d\n",
                    entry.getKey(), entry.getValue());
        }

        Map<Artist, List<Album>> artistListMap = groupAlbumByArtist(SampleData.getAlbumList());

        for (Map.Entry<Artist, List<Album>> entry : artistListMap.entrySet()) {
            System.out.println("ch.usi.inf.ajp22.Artist: " + entry.getKey().getName());
            for (Album album : entry.getValue()) {
                System.out.println("\t" + album.getTitle());
            }
        }

        Map<Artist, List<Track>> artistListMap1 = groupTrackByArtist(SampleData.getAlbumList());
        for (Map.Entry<Artist, List<Track>> entry : artistListMap1.entrySet()) {
            System.out.println("ch.usi.inf.ajp22.Artist: " + entry.getKey().getName());
            for (Track track : entry.getValue()) {
                System.out.println("\t" + track.toString());
            }
        }

        List<Track> trackPOPinGuns = trackFilteredWithPredicate(SampleData.appetiteForDestruction.getTracks().stream()
                , (t -> !t.getGenre().equals(Track.Genre.HIP_HOP)));
        trackPOPinGuns
                .forEach(System.out::println);

        getAlbumWithAtLeastNTracks(SampleData.getAlbumList(), 3)
                .forEach(System.out::println);

        System.out.println("Getting artist name " + getArtistNameAndNickNameFromAlbum(SampleData.getAlbumList()));

        System.out.println("ch.usi.inf.ajp22.Album with the longest name is " + longestTitle(SampleData.getAlbumList()));

        Album[] albumsArray = createArray(SampleData.getAlbumList(), Album[]::new);

        System.out.println("ch.usi.inf.ajp22.Album array");
        for (int i = 0; i < albumsArray.length; i++) {
            System.out.println(albumsArray[i]);
        }

        Track combinedTrack = combineAllTrackInAlbum(SampleData.appetiteForDestruction.getTracks().stream());
        System.out.println(combinedTrack);

    }
}
