package net.dirtydeeds.discordsoundboard;

import com.sun.management.OperatingSystemMXBean;
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.dirtydeeds.discordsoundboard.service.SoundPlayerImpl;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.ChannelType;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.exceptions.PermissionException;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.internal.utils.PermissionUtil;
import org.apache.commons.logging.impl.SimpleLog;

import java.io.File;
import java.io.IOException;
import java.lang.management.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author dfurrer.
 * <p>
 * This class handles listening to commands in discord text channels and responding to them.
 */
public class ChatSoundBoardListener extends ListenerAdapter {

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

    private SoundPlayerImpl soundPlayer;
    private String commandCharacter = "?";
    private Integer messageSizeLimit = 2000;
    private boolean respondToDms;
    private boolean muted;
    private static DecimalFormat df2 = new DecimalFormat("#.##");
    private static final int MAX_FILE_SIZE_IN_BYTES = 1000000; // 1 MB
    private UserRepository userRepository;
    private SoundFileRepository soundFileRepository;

    public ChatSoundBoardListener(SoundPlayerImpl soundPlayer, String commandCharacter, String messageSizeLimit,
                                  Boolean respondToDms, UserRepository userRepository,
                                  SoundFileRepository soundFileRepository) {
        this.soundPlayer = soundPlayer;
        if (commandCharacter != null && !commandCharacter.isEmpty()) {
            this.commandCharacter = commandCharacter;
        }
        if (messageSizeLimit != null && !messageSizeLimit.isEmpty() && messageSizeLimit.matches("^-?\\d+$")) {
            this.messageSizeLimit = Integer.parseInt(messageSizeLimit);
            if (this.messageSizeLimit > 1994) {
                this.messageSizeLimit = 1994;
            }
        }
        muted = false;
        this.respondToDms = respondToDms;
        this.userRepository = userRepository;
        this.soundFileRepository = soundFileRepository;
    }

    @Override
    public void onMessageReceived(MessageReceivedEvent event) {
        String requestingUser = event.getAuthor().getName();
        if (!event.getAuthor().isBot() && ((respondToDms && event.isFromType(ChannelType.PRIVATE)) ||
                !event.isFromType(ChannelType.PRIVATE))) {

            String message = event.getMessage().getContentRaw().toLowerCase();
            if (message.startsWith(commandCharacter)) {
                if (soundPlayer.isUserAllowed(requestingUser) && !soundPlayer.isUserBanned(requestingUser)) {
                    final int maxLineLength = messageSizeLimit;

                    //Respond
                    if (message.startsWith(commandCharacter + "list")) {
                        listCommand(event, requestingUser, message, maxLineLength);
                        //If the command is not list and starts with the specified command character try and play that "command" or sound file.
                    } else if (message.startsWith(commandCharacter + "help")) {
                        helpCommand(event, requestingUser);
                    } else if (message.startsWith(commandCharacter + "volume")) {
                        volumeCommand(event, requestingUser, message);
                    } else if (message.startsWith(commandCharacter + "stop")) {
                        stopCommand(event, requestingUser, message);
                    } else if (message.startsWith(commandCharacter + "info")) {
                        infoCommand(event, requestingUser);
                    } else if (message.startsWith(commandCharacter + "remove")) {
                        removeCommand(event, message);
                    } else if (message.startsWith(commandCharacter + "random")) {
                        randomCommand(event, requestingUser);
                    } else if (message.startsWith(commandCharacter + "entrance") ||
                            message.startsWith(commandCharacter + "leave")) {
                        entranceOrLeaveCommand(event, message);
                    } else if (message.startsWith(commandCharacter + "userdetails")) {
                        userDetails(event);
                    } else if (message.startsWith(commandCharacter + "url")) {
                        String[] messageSplit = event.getMessage().getContentRaw().split(" ");
                        if (messageSplit.length >= 1) {
                            String url = messageSplit[1];
                            soundPlayer.playUrlForUser(url, requestingUser);
                        }
                    } else if (message.startsWith(commandCharacter) &&
                            message.length() >= (commandCharacter.length() + 1)) {
                        soundFileCommand(event, requestingUser, message);
                    } else {
                        if (message.startsWith(commandCharacter) || event.isFromType(ChannelType.PRIVATE)) {
                            nonRecognizedCommand(event, requestingUser);
                        } else {
                            replyByPrivateMessage(event, "You seem to need help.");
                            helpCommand(event, requestingUser);
                        }
                    }
                } else {
                    if (!soundPlayer.isUserAllowed(requestingUser)) {
                        replyByPrivateMessage(event, "I don't take orders from you.");
                    }
                    if (soundPlayer.isUserBanned(requestingUser)) {
                        replyByPrivateMessage(event, "You've been banned from using this soundboard bot.");
                    }
                }
            } else {
                addAttachedSoundFile(event);
            }
        }
    }

    private void userDetails(MessageReceivedEvent event) {
        String[] messageSplit = event.getMessage().getContentRaw().split(" ");
        if (messageSplit.length >= 2) {
            String userNameOrId = messageSplit[1];
            User user = userRepository.findOneByIdOrUsernameIgnoreCase(userNameOrId, userNameOrId);
            if (user != null) {
                StringBuilder response = new StringBuilder();
                response.append("User details for ").append(userNameOrId).append("```")
                        .append("\nDiscord Id: ").append(user.getId())
                        .append("\nUsername: ").append(user.getUsername())
                        .append("\nEntrance Sound: ");
                if (user.getEntranceSound() != null) {
                    response.append(user.getEntranceSound());
                }
                response.append("\nLeave Sound: ");
                if (user.getLeaveSound() != null) {
                    response.append(user.getLeaveSound());
                }
                response.append("```");
                replyByPrivateMessage(event, response.toString());
            }
        }
    }

    private void entranceOrLeaveCommand(MessageReceivedEvent event, String message) {
        String[] messageSplit = event.getMessage().getContentRaw().split(" ");
        if (messageSplit.length >= 2) {
            String userNameOrId = messageSplit[1];
            String soundFileName = "";
            if (messageSplit.length >= 3) {
                soundFileName = messageSplit[2];
            }
            net.dv8tion.jda.api.entities.User pmUser = event.getAuthor();
            if (userIsAdmin(event) ||
                pmUser.getName().equalsIgnoreCase(userNameOrId)) {
                User user = userRepository.findOneByIdOrUsernameIgnoreCase(userNameOrId, userNameOrId);
                if (user != null) {
                    if (soundFileName.isEmpty()) {
                        if (message.startsWith(commandCharacter + "entrance")) {
                            user.setEntranceSound(null);
                            replyByPrivateMessage(event, "User: " + userNameOrId + " entrance sound cleared");
                        } else {
                            user.setLeaveSound(null);
                            replyByPrivateMessage(event, "User: " + userNameOrId + " leave sound cleared");
                        }
                        userRepository.save(user);
                    } else {
                        SoundFile soundFile = soundFileRepository.findOneBySoundFileIdIgnoreCase(soundFileName);
                        if (soundFile == null) {
                            replyByPrivateMessage(event, "Could not find sound file: " + soundFileName);
                        } else {
                            if (message.startsWith(commandCharacter + "entrance")) {
                                user.setEntranceSound(soundFileName);
                                replyByPrivateMessage(event, "User: " + userNameOrId + " entrance sound set to: " + soundFileName);
                            } else {
                                user.setLeaveSound(soundFileName);
                                replyByPrivateMessage(event, "User: " + userNameOrId + " leave sound set to: " + soundFileName);
                            }
                            userRepository.save(user);
                        }
                    }
                } else {
                    replyByPrivateMessage(event, "Could not find user with id or name: " + userNameOrId);
                }
            } else {
                replyByPrivateMessage(event, "Entrance command incorrect. Required input is " +
                        commandCharacter + "entrance <userid/username> <soundfile>");
            }
        }
    }

    private boolean userIsAdmin(MessageReceivedEvent event) {
        if (event.getMember() == null) {
            return false;
        }
        return PermissionUtil.checkPermission(event.getMember(), Permission.MANAGE_SERVER);
    }

    private void addAttachedSoundFile(MessageReceivedEvent event) {
        List<Message.Attachment> attachments = event.getMessage().getAttachments();
        if (attachments.size() > 0 && event.isFromType(ChannelType.PRIVATE)) {
            for (Message.Attachment attachment : attachments) {
                String name = attachment.getFileName();
                String extension = name.substring(name.indexOf(".") + 1);
                if (extension.equals("wav") || extension.equals("mp3")) {
                    if (attachment.getSize() < MAX_FILE_SIZE_IN_BYTES) {
                        if (!Files.exists(Paths.get(soundPlayer.getSoundsPath() + "/" + name))) {
                            File newSoundFile = new File(soundPlayer.getSoundsPath(), name);
                            attachment.downloadToFile(newSoundFile).getNow(null);
                            event.getChannel().sendMessage("Downloaded file `" + name + "` and added to list of sounds " + event.getAuthor().getAsMention() + ".").queue();
                        } else {
                            if (event.getMember() != null) {
                                boolean hasManageServerPerm = userIsAdmin(event);
                                if (event.getAuthor().getName().equalsIgnoreCase(name.substring(0, name.indexOf(".")))
                                        || hasManageServerPerm) {
                                    try {
                                        Files.deleteIfExists(Paths.get(soundPlayer.getSoundsPath() + "/" + name));
                                        File newSoundFile = new File(soundPlayer.getSoundsPath(), name);
                                        attachment.downloadToFile().getNow(newSoundFile);
                                        event.getChannel().sendMessage("Downloaded file `" + name + "` and updated list of sounds " + event.getAuthor().getAsMention() + ".").queue();
                                    } catch (IOException e1) {
                                        LOG.fatal("Problem deleting and re-adding sound file: " + name);
                                    }
                                } else {
                                    event.getChannel().sendMessage("The file '" + name + "' already exists. Only " + name.substring(0, name.indexOf(".")) + " can change update this sound.").queue();
                                }
                            }
                        }
                    } else {
                        replyByPrivateMessage(event, "File `" + name + "` is too large to add to library.");
                    }
                }
            }
        }
    }

    private void soundFileCommand(MessageReceivedEvent event, String requestingUser, String message) {
        if (!muted) {
            try {
                int repeatNumber = 1;
                String fileNameRequested = message.substring(1);

                // If there is the repeat character (~) then cut up the message string.
                int repeatIndex = message.indexOf('~');
                if (repeatIndex > -1) {
                    fileNameRequested = message.substring(1, repeatIndex - 1); // -1 to ignore the previous space
                    if (repeatIndex + 1 == message.length()) { // If there is only a ~ then repeat-infinite
                        repeatNumber = -1;
                    } else { // If there is something after the ~ then repeat for that value
                        repeatNumber = Integer.parseInt(message.substring(repeatIndex + 1)); // +1 to ignore the ~ character
                    }
                }
                LOG.info("Attempting to play file: " + fileNameRequested + " " + repeatNumber + " times. Requested by " + requestingUser + ".");

                soundPlayer.playFileForUser(fileNameRequested, event.getAuthor().getName());
                deleteMessage(event);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            replyByPrivateMessage(event, "I seem to be muted! Try " + commandCharacter + "help");
            LOG.info("Attempting to play a sound file while muted. Requested by " + requestingUser + ".");
        }
    }

    private void randomCommand(MessageReceivedEvent event, String requestingUser) {
        try {
            soundPlayer.playRandomSoundFile(requestingUser, event);
            deleteMessage(event);
        } catch (SoundPlaybackException e) {
            replyByPrivateMessage(event, "Problem playing random file:" + e);
        }
    }

    private void removeCommand(MessageReceivedEvent event, String message) {
        if (event.getMember() != null) {
            boolean hasManageServerPerm = userIsAdmin(event);
            String[] messageSplit = message.split(" ");
            String soundToRemove = messageSplit[1];
            if (event.getAuthor().getName().equalsIgnoreCase(soundToRemove)
                    || hasManageServerPerm) {
                SoundFile soundFileToRemove = soundPlayer.getAvailableSoundFiles().get(soundToRemove);
                if (soundFileToRemove != null) {
                    try {
                        boolean fileRemoved = Files.deleteIfExists(Paths.get(soundFileToRemove.getSoundFileLocation()));
                        if (fileRemoved) {
                            replyByPrivateMessage(event, "Sound file " + soundToRemove + " was removed.");
                        } else {
                            replyByPrivateMessage(event, "Could not find sound file: " + soundToRemove + ".");
                        }
                    } catch (IOException e) {
                        LOG.fatal("Could not remove sound file " + soundToRemove);
                    }
                }
            } else {
                replyByPrivateMessage(event, "You do not have permission to remove sound file: " + soundToRemove + ".");
            }
        }
    }

    private void infoCommand(MessageReceivedEvent event, String requestingUser) {
        LOG.info("Responding to info request by " + requestingUser + ".");

        OperatingSystemMXBean operatingSystemMXBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        int availableProcessors = operatingSystemMXBean.getAvailableProcessors();
        long prevUpTime = runtimeMXBean.getUptime();
        long prevProcessCpuTime = operatingSystemMXBean.getProcessCpuTime();
        double cpuUsage;
        try {
            Thread.sleep(500);
        } catch (Exception ignored) {
        }

        long upTime = runtimeMXBean.getUptime();
        long processCpuTime = operatingSystemMXBean.getProcessCpuTime();
        long elapsedCpu = processCpuTime - prevProcessCpuTime;
        long elapsedTime = upTime - prevUpTime;

        cpuUsage = Math.min(99F, elapsedCpu / (elapsedTime * 10000F * availableProcessors));

        List<MemoryPoolMXBean> memoryPools = new ArrayList<>(ManagementFactory.getMemoryPoolMXBeans());
        long usedHeapMemoryAfterLastGC = 0;
        for (MemoryPoolMXBean memoryPool : memoryPools) {
            if (memoryPool.getType().equals(MemoryType.HEAP)) {
                MemoryUsage poolCollectionMemoryUsage = memoryPool.getCollectionUsage();
                usedHeapMemoryAfterLastGC += poolCollectionMemoryUsage.getUsed();
            }
        }

        Package thisPackage = getClass().getPackage();
        String version = null;
        if (thisPackage != null) {
            version = getClass().getPackage().getImplementationVersion();
        }
        if (version == null) {
            version = "DEVELOPMENT";
        }

        long uptimeDays = TimeUnit.DAYS.convert(upTime, TimeUnit.MILLISECONDS);
        long uptimeHours = TimeUnit.HOURS.convert(upTime, TimeUnit.MILLISECONDS) - TimeUnit.DAYS.toHours(TimeUnit.MILLISECONDS.toDays(upTime));
        long uptimeMinutes = TimeUnit.MINUTES.convert(upTime, TimeUnit.MILLISECONDS) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(upTime));
        long upTimeSeconds = TimeUnit.MILLISECONDS.toSeconds(upTime) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(upTime));

        replyByPrivateMessage(event, "DiscordSoundboard info: ```" +
                "CPU: " + df2.format(cpuUsage) + "%" +
                "\nMemory: " + humanReadableByteCount(usedHeapMemoryAfterLastGC) +
                "\nUptime: Days: " + uptimeDays + " Hours: " + uptimeHours + " Minutes: " + uptimeMinutes + " Seconds: " + upTimeSeconds +
                "\nVersion: " + version +
                "\nSoundFiles: " + soundPlayer.getAvailableSoundFiles().size() +
                "\nCommand Prefix: " + commandCharacter +
                "\nSound File Path: " + soundPlayer.getSoundsPath() +
                "```");
    }

    private void stopCommand(MessageReceivedEvent event, String requestingUser, String message) {
        int fadeoutIndex = message.indexOf('~');
        int fadeoutTimeout = 0;
        if (fadeoutIndex > -1) {
            fadeoutTimeout = Integer.parseInt(message.substring(fadeoutIndex + 1));
        }
        LOG.info("Stop requested by " + requestingUser + " with a fadeout of " + fadeoutTimeout + " seconds");
        if (soundPlayer.stop()) {
            replyByPrivateMessage(event, "Playback stopped.");
        } else {
            replyByPrivateMessage(event, "Nothing was playing.");
        }
    }

    private void volumeCommand(MessageReceivedEvent event, String requestingUser, String message) {
        int fadeoutIndex = message.indexOf('~');
        int newVol = Integer.parseInt(message.substring(8, (fadeoutIndex > -1) ? fadeoutIndex - 1 : message.length()));

        if (newVol >= 1 && newVol <= 100) {
            muted = false;
            soundPlayer.setSoundPlayerVolume(newVol);
            replyByPrivateMessage(event, "*Volume set to " + newVol + "%*");
            LOG.info("Volume set to " + newVol + "% by " + requestingUser + ".");
        } else if (newVol == 0) {
            muted = true;
            soundPlayer.setSoundPlayerVolume(newVol);
            replyByPrivateMessage(event, requestingUser + " muted me.");
            LOG.info("Bot muted by " + requestingUser + ".");
        }
    }

    private void helpCommand(MessageReceivedEvent event, String requestingUser) {
        LOG.info("Responding to help command. Requested by " + requestingUser + ".");
        replyByPrivateMessage(event, "You can type any of the following commands:" +
                "\n```" + commandCharacter + "list             - Returns a list of available sound files." +
                "\n" + commandCharacter + "soundFileName    - Plays the specified sound from the list." +
                "\n" + commandCharacter + "random           - Plays a random sound from the list." +
                "\n" + commandCharacter + "volume 0-100     - Sets the playback volume." +
                "\n" + commandCharacter + "stop             - Stops the sound that is currently playing." +
                "\n" + commandCharacter + "info             - Returns info about the bot." +
                "\n" + commandCharacter + "entrance userName soundFileName - Sets entrance sound for user" +
                "\n" + commandCharacter + "leave userName soundFileName - Sets leave sound for user" +
                "\n" + commandCharacter + "userDetails userName - Get details for user```");
    }

    private void listCommand(MessageReceivedEvent event, String requestingUser, String message, int maxLineLength) {
        StringBuilder commandString = getCommandListString();
        List<String> soundList = getCommandList(commandString);

        LOG.info("Responding to list command. Requested by " + requestingUser + ".");
        if (message.equals(commandCharacter + "list")) {
            if (commandString.length() > maxLineLength) {
                replyByPrivateMessage(event, "You have " + soundList.size() + " pages of soundFiles. Reply: ```" + commandCharacter + "list pageNumber``` to request a specific page of results.");
            } else {
                replyByPrivateMessage(event, "Type any of the following into the chat to play the sound:");
                replyByPrivateMessage(event, soundList.get(0));
            }
        } else {
            String[] messageSplit = message.split(" ");
            try {
                int pageNumber = Integer.parseInt(messageSplit[1]);
                replyByPrivateMessage(event, soundList.get(pageNumber - 1));
            } catch (IndexOutOfBoundsException e) {
                replyByPrivateMessage(event, "The page number you entered is not valid.");
            } catch (NumberFormatException e) {
                replyByPrivateMessage(event, "The page number argument must be a number.");
            }
        }
    }

    private void nonRecognizedCommand(MessageReceivedEvent event, String requestingUser) {
        replyByPrivateMessage(event, "Hello @" + requestingUser + ". I don't know how to respond to this message!");
        replyByPrivateMessage(event, "You can type " + commandCharacter + "help to see a list of recognized commands.");
        LOG.info("Responding to PM of " + requestingUser + ". Unknown Command. Sending help text.");
    }

    private List<String> getCommandList(StringBuilder commandString) {
        final int maxLineLength = messageSizeLimit;
        List<String> soundFiles = new ArrayList<>();

        //if text has \n, \r or \t symbols it's better to split by \s+
        final String SPLIT_REGEXP = "(?<=[ \\n])";

        String[] tokens = commandString.toString().split(SPLIT_REGEXP);
        int lineLen = 0;
        StringBuilder output = new StringBuilder();
        output.append("```\n");
        for (int i = 0; i < tokens.length; i++) {
            String word = tokens[i];

            if (lineLen + (word).length() > maxLineLength) {
                if (i > 0) {
                    output.append("```\n");
                    soundFiles.add(output.toString());

                    output = new StringBuilder(maxLineLength);
                    output.append("```");
                }
                lineLen = 0;
            }
            output.append(word);
            lineLen += word.length();
        }
        if (output.length() > 0) {
            output.append("```");
        }
        soundFiles.add(output.toString());
        return soundFiles;
    }

    private StringBuilder getCommandListString() {
        StringBuilder sb = new StringBuilder();

        Set<Map.Entry<String, SoundFile>> entrySet = soundPlayer.getAvailableSoundFiles().entrySet();

        if (entrySet.size() > 0) {
            for (Map.Entry entry : entrySet) {
                sb.append(commandCharacter).append(entry.getKey()).append("\n");
            }
        }
        return sb;
    }

    private void replyByPrivateMessage(MessageReceivedEvent event, String message) {
        event.getAuthor().openPrivateChannel().complete().sendMessage(message).queue();
        deleteMessage(event);
    }

    private static String humanReadableByteCount(long bytes) {
        int unit = 1000;
        if (bytes < unit) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = ("kMGTPE").charAt(exp - 1) + ("");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }

    private void deleteMessage(MessageReceivedEvent event) {
        if (!event.isFromType(ChannelType.PRIVATE)) {
            try {
                event.getMessage().delete().queue();
            } catch (PermissionException e) {
                LOG.warn("Unable to delete message");
            }
        }
    }
}