import static org.gradle.api.tasks.PathSensitivity.RELATIVE;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.TaskAction;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/** Task to merge several word-list files into one */
@CacheableTask
public class MergeWordsListTask extends DefaultTask {
    @TaskAction
    public void mergeWordsLists() throws IOException, ParserConfigurationException, SAXException {
        if (inputWordsListFiles == null || inputWordsListFiles.length == 0) {
            throw new IllegalArgumentException("Must specify at least one inputWordsListFiles");
        }
        if (outputWordsListFile == null) {
            throw new IllegalArgumentException("Must supply outputWordsListFile");
        }

        System.out.printf(
                Locale.ENGLISH,
                "Merging %d files for maximum %d words, and writing into '%s'. Discarding %d words.%n",
                inputWordsListFiles.length,
                maxWordsInList,
                outputWordsListFile.getName(),
                wordsToDiscard.length);
        final HashMap<String, Integer> allWords = new HashMap<>();
        final List<String> inputFilesWithDuplicates = new ArrayList<>();
        for (File inputFile : inputWordsListFiles) {
            System.out.printf(Locale.ENGLISH, "Reading %s...%n", inputFile.getName());
            if (!inputFile.exists()) throw new FileNotFoundException(inputFile.getAbsolutePath());
            SAXParserFactory parserFactor = SAXParserFactory.newInstance();
            SAXParser parser = parserFactor.newSAXParser();

            Set<String> duplicateWords = new HashSet<>();
            try (final InputStreamReader inputStream =
                    new InputStreamReader(new FileInputStream(inputFile), StandardCharsets.UTF_8)) {
                InputSource inputSource = new InputSource(inputStream);
                parser.parse(inputSource, new MySaxHandler(allWords, duplicateWords));
                System.out.printf(Locale.ENGLISH, "Loaded %d words in total...%n", allWords.size());
            }

            boolean isBuildDir =
                    inputFile
                            .getAbsolutePath()
                            .contains((File.separator).concat("build").concat(File.separator));
            if (duplicateWords.size() > 0 && !isBuildDir) {
                inputFilesWithDuplicates.add(inputFile.getAbsolutePath());
                filterWordsFromInputFile(inputFile, duplicateWords);
            }
        }

        // discarding unwanted words
        if (wordsToDiscard.length > 0) {
            System.out.print("Discarding words...");
            Arrays.stream(wordsToDiscard)
                    .forEach(
                            word -> {
                                if (allWords.remove(word) != null) System.out.print(".");
                            });
            System.out.println();
        }

        System.out.println("Creating output XML file...");
        try (WordListWriter writer = new WordListWriter(outputWordsListFile)) {
            for (Map.Entry<String, Integer> entry : allWords.entrySet()) {
                WordListWriter.writeWordWithRuntimeException(
                        writer, entry.getKey(), entry.getValue());
            }
            System.out.println("Done.");
        }

        if (inputFilesWithDuplicates.size() > 0) {
            throw new RuntimeException(
                    "Found duplicate words in: " + String.join(",", inputFilesWithDuplicates));
        }
    }

    private static final Pattern WORD_LIST_ENTRY =
            Pattern.compile("\\s*<w\\s+f=\"\\d+\">(.+)</w>\\s*");

    private static void filterWordsFromInputFile(File inputFile, Set<String> duplicateWords)
            throws IOException {
        File tempFile = Files.createTempFile("filtered_word_list", "tmp").toFile();

        System.out.printf(
                Locale.ENGLISH,
                "Removing duplicate words from '%s': ",
                inputFile.getAbsolutePath());
        try (BufferedReader reader = new BufferedReader(new FileReader(inputFile))) {
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) {
                String currentLine;
                while ((currentLine = reader.readLine()) != null) {
                    Matcher matcher = WORD_LIST_ENTRY.matcher(currentLine);
                    if (matcher.find()) {
                        String word = matcher.group(1);
                        if (duplicateWords.contains(word)) {
                            System.out.printf(Locale.ENGLISH, "%s, ", word);
                            continue;
                        }
                    }

                    writer.write(currentLine);
                    writer.newLine();
                }
            }
        }

        Files.copy(tempFile.toPath(), inputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
        System.out.println("Done!");
    }

    @InputFiles
    @PathSensitive(RELATIVE)
    public File[] getInputWordsListFiles() {
        return inputWordsListFiles;
    }

    public void setInputWordsListFiles(File[] inputWordsListFiles) {
        this.inputWordsListFiles = inputWordsListFiles;
    }

    @OutputFile
    public File getOutputWordsListFile() {
        return outputWordsListFile;
    }

    public void setOutputWordsListFile(File outputWordsListFile) {
        this.outputWordsListFile = outputWordsListFile;
    }

    @Input
    public String[] getWordsToDiscard() {
        return wordsToDiscard;
    }

    public void setWordsToDiscard(String[] wordsToDiscard) {
        this.wordsToDiscard = wordsToDiscard;
    }

    @Input
    public int getMaxWordsInList() {
        return maxWordsInList;
    }

    public void setMaxWordsInList(int maxWordsInList) {
        this.maxWordsInList = maxWordsInList;
    }

    private File[] inputWordsListFiles;
    private File outputWordsListFile;
    private String[] wordsToDiscard = new String[0];
    private int maxWordsInList = Integer.MAX_VALUE;

    private static class MySaxHandler extends DefaultHandler {

        private final HashMap<String, Integer> allWords;
        private final Set<String> seenBeforeWords;
        private final Set<String> duplicateWords;
        private boolean inWord;
        private final StringBuilder word = new StringBuilder();
        private int freq;

        public MySaxHandler(HashMap<String, Integer> allWords, Set<String> duplicateWords) {
            this.allWords = allWords;
            this.seenBeforeWords = Set.copyOf(allWords.keySet());
            this.duplicateWords = duplicateWords;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException {
            super.startElement(uri, localName, qName, attributes);
            if (qName.equals("w")) {
                inWord = true;
                freq = Integer.parseInt(attributes.getValue("f"));
                word.setLength(0);
            } else {
                inWord = false;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            super.characters(ch, start, length);
            if (inWord) {
                word.append(ch, start, length);
            }
        }

        @Override
        public void skippedEntity(String name) throws SAXException {
            System.out.print("Skipped " + name);
            super.skippedEntity(name);
        }

        @Override
        public void warning(SAXParseException e) throws SAXException {
            System.out.print("Warning! " + e);
            super.warning(e);
        }

        @Override
        public void error(SAXParseException e) throws SAXException {
            System.out.print("Error! " + e);
            super.error(e);
        }

        @Override
        public void fatalError(SAXParseException e) throws SAXException {
            System.out.print("Fatal-Error! " + e);
            super.fatalError(e);
        }

        @Override
        public void unparsedEntityDecl(
                String name, String publicId, String systemId, String notationName)
                throws SAXException {
            System.out.print("unparsed-Entity-Decl! " + name);
            super.unparsedEntityDecl(name, publicId, systemId, notationName);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);
            if (qName.equals("w") && inWord) {
                String word = this.word.toString();
                if (seenBeforeWords.contains(word)) {
                    duplicateWords.add(word);
                }
                allWords.compute(word, (key, value) -> Math.max(value == null ? 0 : value, freq));
            }

            inWord = false;
        }
    }
}
