package net.dirtydeeds.discordsoundboard.service;

import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.FunctionalResultHandler;
import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import net.dirtydeeds.discordsoundboard.*;
import net.dirtydeeds.discordsoundboard.beans.SoundFile;
import net.dirtydeeds.discordsoundboard.beans.User;
import net.dirtydeeds.discordsoundboard.repository.SoundFileRepository;
import net.dirtydeeds.discordsoundboard.repository.UserRepository;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.managers.AudioManager;
import org.apache.commons.logging.impl.SimpleLog;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.security.auth.login.LoginException;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * @author dfurrer.
 * <p>
 * This class handles moving into channels and playing sounds. Also, it loads the available sound files
 * and the configuration properties.
 */
@Service
public class SoundPlayerImpl implements Observer {

    private static final SimpleLog LOG = new SimpleLog("SoundPlayerImpl");

    private Properties appProperties;
    private JDA bot;
    private final MainWatch mainWatch;
    private boolean initialized = false;
    private DefaultAudioPlayerManager playerManager;
    private AudioPlayer musicPlayer;
    private String soundFileDir;
    private List<String> allowedUsers;
    private List<String> bannedUsers;
    private SoundFileRepository soundFileRepository;
    private UserRepository userRepository;
    private boolean leaveAfterPlayback = false;
    private String leaveSuffix = "_leave";

    @Inject
    public SoundPlayerImpl(MainWatch mainWatch, SoundFileRepository soundFileRepository,
                           UserRepository userRepository) {
        this.mainWatch = mainWatch;
        this.mainWatch.addObserver(this);
        this.soundFileRepository = soundFileRepository;
        this.userRepository = userRepository;

        init();
    }

    private void init() {
        loadProperties();
        initializeDiscordBot();
        updateFileList();
        getUsers();

        playerManager = new DefaultAudioPlayerManager();
        playerManager.registerSourceManager(new LocalAudioSourceManager());
        playerManager.registerSourceManager(new YoutubeAudioSourceManager());
        playerManager.registerSourceManager(new VimeoAudioSourceManager());
        playerManager.registerSourceManager(new SoundCloudAudioSourceManager());

        musicPlayer = playerManager.createPlayer();
        musicPlayer.setVolume(75);

        leaveAfterPlayback = Boolean.parseBoolean(appProperties.getProperty("leaveAfterPlayback"));

        ConnectorNativeLibLoader.loadConnectorLibrary();

        initialized = true;
    }

    /**
     * Logs the discord bot in and adds the ChatSoundBoardListener if the user configured it to be used
     */
    private void initializeDiscordBot() {
        try {
            if (bot != null) {
                bot.shutdown();
            }

            String botToken = appProperties.getProperty("bot_token");
            bot = new JDABuilder()
                    .setAutoReconnect(true)
                    .setToken(botToken)
                    .build()
                    .awaitReady();

            if (Boolean.parseBoolean(appProperties.getProperty("respond_to_chat_commands"))) {
                String commandCharacter = appProperties.getProperty("command_character");
                String messageSizeLimit = appProperties.getProperty("message_size_limit");
                leaveSuffix = appProperties.getProperty("leave_suffix");
                String respondToDmsString = appProperties.getProperty("respond_to_dm");
                Boolean respondToDms = true;
                if (respondToDmsString != null) {
                    respondToDms = Boolean.valueOf(respondToDmsString);
                }
                ChatSoundBoardListener chatListener = new ChatSoundBoardListener(this, commandCharacter,
                        messageSizeLimit, respondToDms, userRepository, soundFileRepository);
                this.addBotListener(chatListener);
                EntranceSoundBoardListener entranceSoundBoardListener = new EntranceSoundBoardListener(this, userRepository);
                LeaveSoundBoardListener leaveSoundBoardListener = new LeaveSoundBoardListener(this, userRepository);
                MovedChannelListener movedChannelListener = new MovedChannelListener(this, userRepository);
                this.addBotListener(entranceSoundBoardListener);
                this.addBotListener(leaveSoundBoardListener);
                this.addBotListener(movedChannelListener);
            }

            String allowedUsersString = appProperties.getProperty("allowedUsers");
            if (allowedUsersString != null) {
                String[] allowedUsersArray = allowedUsersString.trim().split(",");
                if (allowedUsersArray.length > 0) {
                    allowedUsers = Arrays.asList(allowedUsersArray);
                }
            }

            String bannedUsersString = appProperties.getProperty("bannedUsers");
            if (bannedUsersString != null) {
                String[] bannedUsersArray = bannedUsersString.split(",");
                if (bannedUsersArray.length > 0) {
                    bannedUsers = Arrays.asList(bannedUsersArray);
                }
            }

            String activityString = appProperties.getProperty("activityString");
            if (StringUtils.isEmpty(activityString)) {
                bot.getPresence().setActivity(Activity.of(Activity.ActivityType.DEFAULT,
                        "Type " + appProperties.getProperty("command_character") + "help for a list of commands."));
            } else {
                bot.getPresence().setActivity(Activity.of(Activity.ActivityType.DEFAULT, activityString));
            }

        } catch (IllegalArgumentException e) {
            LOG.warn("The config was not populated. Please enter an email and password.");
        } catch (LoginException e) {
            LOG.warn("The provided bot token was incorrect. Please provide valid details.");
        } catch (InterruptedException e) {
            LOG.fatal("Login Interrupted.");
        }
    }

    @Override
    public void update(Observable o, Object arg) {
        updateFileList();
    }

    /**
     * Gets a Map of the loaded sound files.
     *
     * @return Map of sound files that have been loaded.
     */
    public Map<String, SoundFile> getAvailableSoundFiles() {
        Map<String, SoundFile> returnFiles = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (SoundFile soundFile : soundFileRepository.findAll()) {
            returnFiles.put(soundFile.getSoundFileId(), soundFile);
        }
        return returnFiles;
    }

    /**
     * Sets volume of the player.
     *
     * @param volume - The volume value to set.
     */
    public void setSoundPlayerVolume(int volume) {
        musicPlayer.setVolume(volume);
    }

    /**
     * Returns the current volume
     *
     * @return float representing the current volume.
     */
    public float getSoundPlayerVolume() {
        return musicPlayer.getVolume();
    }

    @SuppressWarnings("unchecked")
    public void playRandomSoundFile(String requestingUser, MessageReceivedEvent event) throws SoundPlaybackException {
        try {
            Map<String, SoundFile> sounds = getAvailableSoundFiles();
            List<String> keysAsArray = new ArrayList(sounds.keySet());
            Random r = new Random();
            SoundFile randomValue = sounds.get(keysAsArray.get(r.nextInt(keysAsArray.size())));

            LOG.info("Attempting to play random file: " + randomValue.getSoundFileId() + ", requested by : " + requestingUser);
            try {
                if (event != null) {
                    if (event.getChannelType().equals(ChannelType.PRIVATE)) {
                        playFileForUser(randomValue.getSoundFileId(), requestingUser);
                    } else {
                        playFileForEvent(randomValue.getSoundFileId(), event);
                    }
                } else {
                    playFileForUser(randomValue.getSoundFileId(), requestingUser);
                }

                if (leaveAfterPlayback) {
                    if (event != null) {
                        disconnectFromChannel(event.getGuild());
                    }
                }
            } catch (Exception e) {
                LOG.fatal("Could not play random file: " + randomValue.getSoundFileId());
            }
        } catch (Exception e) {
            throw new SoundPlaybackException("Problem playing random file.");
        }
    }

    /**
     * Joins the channel of the user provided and then plays a file.
     *
     * @param fileName - The name of the file to play.
     * @param userName - The name of the user to lookup what VoiceChannel they are in.
     */
    public void playFileForUser(String fileName, String userName) {
        if (userName == null || userName.isEmpty()) {
            userName = appProperties.getProperty("username_to_join_channel");
        }
        try {
            Guild guild = getUsersGuild(userName);
            joinUsersCurrentChannel(userName);

            playFile(fileName, guild);

            if (leaveAfterPlayback) {
                disconnectFromChannel(guild);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void playUrlForUser(String url, String userName) {
        if (userName == null || userName.isEmpty()) {
            userName = appProperties.getProperty("username_to_join_channel");
        }
        try {
            Guild guild = getUsersGuild(userName);
            joinUsersCurrentChannel(userName);

            playUrl(url, guild);

            if (leaveAfterPlayback) {
                disconnectFromChannel(guild);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Plays the fileName requested.
     *
     * @param fileName - The name of the file to play.
     * @param event    -  The event that triggered the sound playing request. The event is used to find the channel to play
     *                 the sound back in.
     */
    private void playFileForEvent(String fileName, MessageReceivedEvent event) {
        playFileForEvent(fileName, event, 1);
    }

    /**
     * Plays the fileName requested.
     *
     * @param fileName     - The name of the file to play.
     * @param event        -  The event that triggered the sound playing request. The event is used to find the channel to play
     *                     the sound back in.
     * @param repeatNumber - the number of times to repeat the sound file
     */
    private void playFileForEvent(String fileName, MessageReceivedEvent event, int repeatNumber) {
        SoundFile fileToPlay = getSoundFileById(fileName);
        if (event != null) {
            Guild guild = event.getGuild();
            if (fileToPlay != null) {
                moveToUserIdsChannel(event, guild);

                File soundFile = new File(fileToPlay.getSoundFileLocation());
                playFile(soundFile, guild, repeatNumber);

                if (leaveAfterPlayback) {
                    disconnectFromChannel(event.getGuild());
                }
            } else {
                event.getAuthor().openPrivateChannel().complete().sendMessage("Could not find sound to play. Requested sound: " + fileName + ".").queue();
            }
        }
    }

    /**
     * Plays the fileName requested in the requested channel.
     *
     * @param fileName - The name of the file to play.
     * @param channel  -  The channel to play the file in
     */
    public void playFileInChannel(String fileName, VoiceChannel channel) {
        if (channel == null) return;
        moveToChannel(channel, channel.getGuild());
        LOG.info("Playing file for user: " + fileName + " in channel: " + channel.getName());
        try {
            playFile(fileName, channel.getGuild());
        } catch (SoundPlaybackException e) {
            LOG.info("Could not find any sound to play for channel movement of user: " + fileName);
        }
        if (leaveAfterPlayback) {
            disconnectFromChannel(channel.getGuild());
        }
    }

    /**
     * Stops sound playback and returns true or false depending on if playback was stopped.
     *
     * @return boolean representing whether playback was stopped.
     */
    public boolean stop() {
        musicPlayer.stopTrack();

        return true;
    }

    /**
     * Get a list of users
     *
     * @return List of soundboard users.
     */
    public List<net.dirtydeeds.discordsoundboard.beans.User> getUsers() {
        String userNameToSelect = appProperties.getProperty("username_to_join_channel");
        List<User> users = new ArrayList<>();
        for (net.dv8tion.jda.api.entities.User discordUser : bot.getUsers()) {
            if (discordUser.getJDA().getStatus().equals(JDA.Status.CONNECTED)) {
                boolean selected = false;
                String username = discordUser.getName();
                if (userNameToSelect.equals(username)) {
                    selected = true;
                }
                Optional<User> optionalUser = userRepository.findById(discordUser.getId());
                if (optionalUser.isPresent()) {
                    User user = optionalUser.get();
                    user.setSelected(selected);
                    users.add(user);
                } else {
                    users.add(new net.dirtydeeds.discordsoundboard.beans.User(discordUser.getId(), username, selected));
                }
            }
        }
        users.sort(Comparator.comparing(User::getUsername));
        userRepository.saveAll(users);
        return users;
    }

    public boolean isUserAllowed(String username) {
        if (allowedUsers == null) {
            return true;
        } else if (allowedUsers.isEmpty()) {
            return true;
        } else return !allowedUsers.contains(username);
    }

    public boolean isUserBanned(String username) {
        return bannedUsers != null && !bannedUsers.isEmpty() && bannedUsers.contains(username);
    }

    /**
     * Get the path the application is using for sound files.
     *
     * @return String representation of the sound file path.
     */
    public String getSoundsPath() {
        return soundFileDir;
    }

    private SoundFile getSoundFileById(String soundFileId) {
        return soundFileRepository.findOneBySoundFileIdIgnoreCase(soundFileId);
    }

    /**
     * Find the "author" of the event and join the voice channel they are in.
     *
     * @param event - The event
     */
    private void moveToUserIdsChannel(MessageReceivedEvent event, Guild guild) {
        VoiceChannel channel = findUsersChannel(event, guild);

        if (channel == null) {
            event.getAuthor().openPrivateChannel().complete()
                    .sendMessage("Hello @" + event.getAuthor().getName() + "! I can not find you in any Voice Channel. Are you sure you are connected to voice?.").queue();
            LOG.warn("Problem moving to requested users channel. Maybe user, " + event.getAuthor().getName() + " is not connected to Voice?");
        } else {
            moveToChannel(channel, guild);
        }
    }

    /**
     * Moves to the specified voice channel.
     *
     * @param channel - The channel specified.
     */
    private void moveToChannel(VoiceChannel channel, Guild guild) {
//        boolean hasPermissionToSpeak = PermissionUtil.checkPermission(bot.getSelfUser(), Permission.VOICE_SPEAK);
//        if (hasPermissionToSpeak) {
        AudioManager audioManager = guild.getAudioManager();
        if (audioManager.isConnected()) {
            if (audioManager.isAttemptingToConnect()) {
                audioManager.closeAudioConnection();
            }
            audioManager.openAudioConnection(channel);
        } else {
            audioManager.openAudioConnection(channel);
        }

        int i = 0;
        int waitTime = 100;
        int maxIterations = 40;
        //Wait for the audio connection to be ready before proceeding.
        synchronized (this) {
            while (!audioManager.isConnected()) {
                try {
                    wait(waitTime);
                    i++;
                    if (i >= maxIterations) {
                        break; //break out if after 1 second it doesn't get a connection;
                    }
                } catch (InterruptedException e) {
                    LOG.warn("Waiting for audio connection was interrupted.");
                }
            }
        }
//        } else {
//            throw new SoundPlaybackException("The bot does not have permission to speak in the requested channel: " + channel.getName() + ".");
//        }
    }

    /**
     * Finds a users voice channel based on event and what guild to look in.
     *
     * @param event - The event that triggered this search. This is used to get th events author.
     * @param guild - The guild (discord server) to look in for the author.
     * @return The VoiceChannel if one is found. Otherwise return null.
     */
    private VoiceChannel findUsersChannel(MessageReceivedEvent event, Guild guild) {
        VoiceChannel channel = null;

        outerloop:
        for (VoiceChannel channel1 : guild.getVoiceChannels()) {
            for (Member user : channel1.getMembers()) {
                if (user.getId().equals(event.getAuthor().getId())) {
                    channel = channel1;
                    break outerloop;
                }
            }
        }

        return channel;
    }

    /**
     * Join the users current channel.
     */
    private void joinUsersCurrentChannel(String userName) {
        for (Guild guild : bot.getGuilds()) {
            for (VoiceChannel channel : guild.getVoiceChannels()) {
                for (Member user : channel.getMembers()) {
                    if (user.getEffectiveName().equalsIgnoreCase(userName)
                            || user.getUser().getName().equalsIgnoreCase(userName)) {
                        moveToChannel(channel, guild);
                    }
                }
            }
        }
    }

    /**
     * Looks through all the guilds the bot has access to and returns the VoiceChannel the requested user is connected to.
     *
     * @param userName - The username to look for.
     * @return The voice channel the user is connected to. If user is not connected to a voice channel will return null.
     */
    private Guild getUsersGuild(String userName) {
        for (Guild guild : bot.getGuilds()) {
            for (VoiceChannel channel : guild.getVoiceChannels()) {
                for (Member user : channel.getMembers()) {
                    if (user.getEffectiveName().equalsIgnoreCase(userName)
                            || user.getUser().getName().equalsIgnoreCase(userName)) {
                        return guild;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Play file name requested. Will first try to load the file from the map of available sounds.
     *
     * @param fileName - fileName to play.
     */
    private void playFile(String fileName, Guild guild) throws SoundPlaybackException {
        SoundFile fileToPlay = getSoundFileById(fileName);
        if (fileToPlay != null) {
            File soundFile = new File(fileToPlay.getSoundFileLocation());
            playFile(soundFile, guild);
        } else {
            throw new SoundPlaybackException("Could not find sound file that was requested.");
        }
    }

    /**
     * Play the provided File object
     *
     * @param audioFile - The File object to play.
     * @param guild     - The guild (discord server) the playback is going to happen in.
     */
    private void playFile(File audioFile, Guild guild) {
        playFile(audioFile, guild, 1);
    }

    /**
     * Play the provided File object
     *
     * @param audioFile    - The File object to play.
     * @param guild        - The guild (discord server) the playback is going to happen in.
     * @param repeatNumber - The number of times to repeat the audio file.
     */
    private void playFile(File audioFile, Guild guild, int repeatNumber) {
        if (guild == null) {
            LOG.fatal("Guild is null or you're not in a voice channel the bot has permission to access. Have you added your bot to a guild? https://discordapp.com/developers/docs/topics/oauth2");
        } else {
            AudioManager audioManager = guild.getAudioManager();
            AudioSendHandler audioSendHandler = new MyAudioSendHandler(musicPlayer);
            audioManager.setSendingHandler(audioSendHandler);

            playFileString(audioFile.getAbsolutePath());
        }
    }

    private void playUrl(String url, Guild guild) {
        playFileString(url);
    }

    private void playFileString(String whatToPlay) {
        playerManager.loadItem(whatToPlay,
                new FunctionalResultHandler(track -> musicPlayer.playTrack(track),
                        null, null, null));
    }

    public String getFileForUser(String userName, boolean entrance) {
        Set<Map.Entry<String, SoundFile>> entrySet = getAvailableSoundFiles().entrySet();
        String fileToPlay = "";
        if (entrySet.size() > 0) {
            for (Map.Entry entry : entrySet) {
                String fileEntry = (String) entry.getKey();
                if (entrance) {
                    if (userName.toLowerCase().startsWith(fileEntry.toLowerCase())
                            && fileEntry.length() > fileToPlay.length())
                        fileToPlay = fileEntry;
                } else {
                    if (fileEntry.toLowerCase().startsWith(userName.toLowerCase()) &&
                            fileEntry.toLowerCase().endsWith(leaveSuffix.toLowerCase())
                            && fileEntry.length() > fileToPlay.length()) {
                        fileToPlay = fileEntry;
                    }
                }
            }
        }
        return fileToPlay;
    }

    /**
     * This method loads the files. This checks if you are running from a .jar file and loads from the /sounds dir relative
     * to the jar file. If not it assumes you are running from code and loads relative to your resource dir.
     */
    private void updateFileList() {
        try {

            soundFileDir = appProperties.getProperty("sounds_directory");
            if (soundFileDir == null || soundFileDir.isEmpty()) {
                soundFileDir = System.getProperty("user.dir") + "/sounds";
            }
            LOG.info("Loading from " + soundFileDir);
            Path soundFilePath = Paths.get(soundFileDir);

            if (!initialized) {
                mainWatch.watchDirectoryPath(soundFilePath);
            }

            if (!soundFilePath.toFile().exists()) {
                System.out.println("creating directory: " + soundFilePath.toFile().toString());
                boolean result = false;

                try {
                    result = soundFilePath.toFile().mkdir();
                } catch (SecurityException se) {
                    LOG.fatal("Could not create directory: " + soundFilePath.toFile().toString());
                }
                if (result) {
                    LOG.info("DIR: " + soundFilePath.toFile().toString() + " created.");
                }
            }

            soundFileRepository.deleteAll();

            Files.walk(soundFilePath).forEach(filePath -> {
                if (Files.isRegularFile(filePath)) {
                    String fileName = filePath.getFileName().toString();
                    fileName = fileName.substring(fileName.indexOf("/") + 1);
                    fileName = fileName.substring(0, fileName.indexOf("."));
                    LOG.info(fileName);
                    File file = filePath.toFile();
                    String parent = file.getParentFile().getName();
                    if (!soundFileRepository.existsById(fileName)) {
                        SoundFile soundFile = new SoundFile(fileName, filePath.toString(), parent);
                        soundFileRepository.save(soundFile);
                    }
                }
            });
        } catch (IOException e) {
            LOG.fatal(e.toString());
            e.printStackTrace();
        }
    }


    private void disconnectFromChannel(Guild guild) {
        if (guild != null) {
            guild.getAudioManager().closeAudioConnection();
            LOG.info("Disconnecting from channel.");
        }
    }

    /**
     * Loads in the properties from the application.properties file
     */
    private void loadProperties() {
        appProperties = new Properties();
        InputStream stream = null;
        try {
            stream = new FileInputStream(System.getProperty("user.dir") + "/application.properties");
            appProperties.load(stream);
            stream.close();
            return;
        } catch (FileNotFoundException e) {
            LOG.warn("Could not find application.properties file.");
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (stream == null) {
            LOG.warn("Loading application.properties file from resources folder");
            try {
                stream = this.getClass().getResourceAsStream("/application.properties");
                if (stream != null) {
                    appProperties.load(stream);
                    stream.close();
                } else {
                    //TODO: Would be nice if we could auto create a default application.properties here.
                    LOG.fatal("You do not have an application.properties file. Please create one.");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @PreDestroy
    @SuppressWarnings("unused")
    public void cleanUp() {
        System.out.println("SoundPlayer is shutting down. Cleaning up.");
        bot.shutdown();
    }

    /**
     * Sets listeners
     *
     * @param listener - The listener object to set.
     */
    private void addBotListener(Object listener) {
        bot.addEventListener(listener);
    }
}