package lc.mecha.audio;

import lc.mecha.lang.CountingList;
import lc.mecha.log.MechaLogger;
import lc.mecha.log.MechaLoggerFactory;
import lc.mecha.util.UniversalJob;
import lc.mecha.util.VelocityWatch;
import org.apache.commons.io.FilenameUtils;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.audio.AudioHeader;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.TagException;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.HashSet;
import java.util.Locale;

/**
 * Scan a media path to build a database of audio files present within the path.
 *
 * @author Alex Leigh
 */
public class AudioScanner {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(AudioScanner.class);
    private final CountingList<String> extensions = new CountingList<>();
    private final CountingList<String> rejectedExtensions = new CountingList<>();
    private final HashSet<Path> corruptFiles = new HashSet<>();
    private final HashSet<String> supported = new HashSet<>();
    private long totalTime;

    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            logger.info("AudioScanner: dir ...");
            System.exit(UniversalJob.RET_BADARGS);
        }

        AudioScanner scanner = new AudioScanner();

        scanner.defaultSupported();
        for (String arg : args) {
            scanner.scan(Paths.get(arg), null);
        }

        logger.info("Rejected files: [total: {}] {}", scanner.getRejectedExtensions().total(),
                scanner.getRejectedExtensions());
        logger.info("Accepted files: [total: {}] {}", scanner.getExtensions().total(),
                scanner.getExtensions());
        logger.info("Corrupted files: [cnt: {}]", scanner.getCorruptFiles().size());
        logger.info("Total duration: {}", scanner.totalTime());
    }

    public CountingList<String> getExtensions() {
        return extensions;
    }

    public HashSet<Path> getCorruptFiles() {
        return corruptFiles;
    }

    public Duration totalTime() {
        synchronized (this) {
            return Duration.ofSeconds(totalTime);
        }
    }

    public void defaultSupported() {
        supported.add("mp3");
        supported.add("mp4");
        supported.add("m4p");
        supported.add("m4a");
        supported.add("wav");
        supported.add("m4v");
        supported.add("aif");
        supported.add("aiff");
        supported.add("mpga");
        supported.add("mp2");
    }

    public CountingList<String> getRejectedExtensions() {
        return rejectedExtensions;
    }

    public HashSet<String> getSupported() {
        return supported;
    }

    /**
     * Scan the given path recursively looking for audio files.
     */
    public void scan(Path root, AudioFileProcessor processor) throws Exception {
        VelocityWatch watch = new VelocityWatch(logger);

        Files.walkFileTree(root, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                logger.debug("Visiting file. [file: {}]", file);
                if (!Files.exists(file)) {
                    logger.warn("File exists, but could not open: {}", file);
                    return FileVisitResult.CONTINUE;
                }

                String extension = FilenameUtils.getExtension(file.toString());
                if (extension != null) extension = extension.toLowerCase(Locale.US);

                watch.event(extension);

                if (extension == null || !supported.contains(extension)) {
                    logger.warn("Skipping file. [fn: {}]", file);
                    rejectedExtensions.increment(extension, 1);
                    return FileVisitResult.CONTINUE;
                }

                extensions.increment(extension, 1);

                try {
                    processAudio(file, processor);
                } catch (Exception e) {
                    e.printStackTrace();
                    corruptFiles.add(file);
                }

                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * Import the audio file metadata into the scanner.
     */
    public void processAudio(Path file, AudioFileProcessor processor) throws TagException, ReadOnlyFileException,
            CannotReadException, InvalidAudioFrameException, IOException {
        AudioFile audio = AudioFileIO.read(file.toFile());
        AudioHeader hdr = audio.getAudioHeader();
        int seconds = hdr.getTrackLength();
        long bitrate = hdr.getBitRateAsNumber();

        synchronized (this) {
            totalTime += seconds;
        }

        String artist = audio.getTag().getFirst(FieldKey.ARTIST);
        String title = audio.getTag().getFirst(FieldKey.TITLE);
        String album = audio.getTag().getFirst(FieldKey.ALBUM);

        if (processor != null)
            processor.process(file, artist, album, title, seconds, bitrate);
    }
}
