import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import javax.persistence.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Map;

/**
 * Created by zen on 11/15/14.
 */
@Entity
@Table(name = "analyzed_book")
public class AnalyzedBookModel {

    private AnalyzedBookModelId analyzedBookModelId;
//    private Sentiment positiveness; // +
    private float positiveness; // +
    private int wordsNum; // +
    private int charactersNum; // +
    private int sentencesNum; // +
    private float averSentenceLength; // +
    private float averWordLength; // +
    private float relNouns; // +
    private float relAdjectives; // +
    private float relVerbs; // +
    private double vocabulary; // ?
    private int locations; // +
    private int dates; // +
    private int names; // +

    @EmbeddedId
    public AnalyzedBookModelId getAnalyzedBookModelId() {
        return analyzedBookModelId;
    }

    public void setAnalyzedBookModelId(AnalyzedBookModelId analyzedBookModelId) {
        this.analyzedBookModelId = analyzedBookModelId;
    }


    //    @Column(name = "positiveness")
//    @Enumerated(EnumType.STRING)
//    public Sentiment getPositiveness() {
//        return positiveness;
//    }
//
//    public void setPositiveness(Sentiment positiveness) {
//        this.positiveness = positiveness;
//    }

    @Column (name = "positiveness")
    public float getPositiveness() {
        return positiveness;
    }

    public void setPositiveness(float positiveness) {
        this.positiveness = positiveness;
    }

    @Column(name = "words")
    public int getWordsNum() {
        return wordsNum;
    }

    public void setWordsNum(int wordsNum) {
        this.wordsNum = wordsNum;
    }

    @Column(name = "characters")
    public int getCharactersNum() {
        return charactersNum;
    }

    public void setCharactersNum(int charactersNum) {
        this.charactersNum = charactersNum;
    }

    @Column(name = "sentences")
    public int getSentencesNum() {
        return sentencesNum;
    }

    public void setSentencesNum(int sentencesNum) {
        this.sentencesNum = sentencesNum;
    }

    @Column(name = "averSentenceLength")
    public float getAverSentenceLength() {
        return averSentenceLength;
    }

    public void setAverSentenceLength(float averSentenceLength) {
        this.averSentenceLength = averSentenceLength;
    }

    @Column(name = "relNouns")
    public float getRelNouns() {
        return relNouns;
    }

    public void setRelNouns(float relNouns) {
        this.relNouns = relNouns;
    }

    @Column(name = "relAdjectives")
    public float getRelAdjectives() {
        return relAdjectives;
    }

    public void setRelAdjectives(float relAdjectives) {
        this.relAdjectives = relAdjectives;
    }

    @Column(name = "relVerbs")
    public float getRelVerbs() {
        return relVerbs;
    }

    public void setRelVerbs(float relVerbs) {
        this.relVerbs = relVerbs;
    }

    @Column(name = "averWordLength")
    public float getAverWordLength() {
        return averWordLength;
    }

    public void setAverWordLength(float averWordLength) {
        this.averWordLength = averWordLength;
    }

    @Transient
    public double getVocabulary() {
        return vocabulary;
    }

    public void setVocabulary(double vocabulary) {
        this.vocabulary = vocabulary;
    }

    @Column(name = "locations")
    public int getLocations() {
        return locations;
    }

    public void setLocations(int locations) {
        this.locations = locations;
    }

    @Column(name = "dates")
    public int getDates() {
        return dates;
    }

    public void setDates(int dates) {
        this.dates = dates;
    }

    @Column(name = "names")
    public int getNames() {
        return names;
    }

    public void setNames(int names) {
        this.names = names;
    }

    public static AnalyzedBookModel analyseBook(String text) {
        AnalyzedBookModel book = new AnalyzedBookModel();

        BookAnalysator bookAnalysator = new BookAnalysator();

        //This breaks text into sentences
        String sentences[] = bookAnalysator.detectSentences("models/en-sent.bin", text);
        book.setSentencesNum(sentences.length);

        //This breaks each sentence down into tokens so we get tokenized sentences array
        ArrayList<String[]> tokenizedSentences = bookAnalysator.tokenize("models/en-token.bin", sentences);

        //This counts number of words in each tokenized sentence
        int wordsNum = bookAnalysator.countWords(tokenizedSentences);
        book.setWordsNum(wordsNum);

        //This searches for different parts of speech (adjectives, nouns etc.)
        Map<String, Integer> partsOfSpeech = bookAnalysator.getPOSwordsNumber("models/en-pos-maxent.bin", tokenizedSentences);
        book.setRelAdjectives((float)partsOfSpeech.get("adjectivesNum") / book.getWordsNum() * 100);
        book.setRelNouns((float)partsOfSpeech.get("nounsNum") / book.getWordsNum() * 100);
        book.setRelVerbs((float)partsOfSpeech.get("verbsNum") / book.getWordsNum() * 100);

        //This counts number of characters in text
        int charactersNum = bookAnalysator.countCharacters(text);
        book.setCharactersNum(charactersNum);

        //This calculates average sentence length over whole book
        float averSentenceLength = bookAnalysator.calcAverSentenceLength(book.getWordsNum(), book.getSentencesNum());
        book.setAverSentenceLength(averSentenceLength);

        //This calculates aver word length over whole text
        float averWordLength = bookAnalysator.calcAverWordLength(book.getCharactersNum(), book.getWordsNum());
        book.setAverWordLength(averWordLength);

        //This searches for people's names
        int namesNum = bookAnalysator.countPersonsNames(tokenizedSentences);
        book.setNames(namesNum);

        //This searches for dates
        int datesSum = bookAnalysator.countDates(tokenizedSentences);
        book.setDates(datesSum);

        //This searches for different locations
        int locationsSum = bookAnalysator.countLocationsNames(tokenizedSentences);
        book.setLocations(locationsSum);

//        This is positiveness analysator work that I intended to use at start, but then leaned towards using valence of words
//        SentimentAnalysisTraining sentAnal = new SentimentAnalysisTraining();
//        sentAnal.setBasic();
//        sentAnal.trainOnSentences();
//        sentAnal.trainOnReviews();
//        sentAnal.exportTrainedModel();
//        SentimentAnalysisClassifier sentAnalReady = new SentimentAnalysisClassifier();
//        sentAnalReady.loadTrainedModel();
//        sentAnalReady.evaluateSentence("This is more than poetic insight; it is hallucination.");

        float positiveness = bookAnalysator.calcPositiveness(tokenizedSentences);
        book.setPositiveness(positiveness);

        return book;
    }

    /**
     * Stems valences file and writes them into new file
     */
    public static void stemValencedWords() {
        //Read ratings file to use it on books, to use stemmer word must be lowercase btw
        CSVReader csvReader = new CSVReader();

        //Stem words in valences datafile and save them to the new file
        csvReader.readStemAndWriteCSV("affectiveRatings/Ratings_Warriner_et_al.csv", "affectiveRatings/newAffectiveRatings.csv");
    }

    /**
     * Looks through the file and if it's a book reads it in and analyses
     * @param file
     * @param persistenceManager
     * @return
     */
    public static AnalyzedBookModel processFile(Path file, PersistenceManager persistenceManager) {
        if (FilenameUtils.getExtension(file.toString()).equals("zip")) {
            //Open the file, read in the book
            //Make new directory where to extract the contents of the book zip file
            File destDir = new File(FilenameUtils.removeExtension(file.toString()));
            destDir.mkdir();

            ZipFile zf = null;
            try {
                System.out.println("extracting");
                //Extract contents of the zip file to dest directory
                zf = new ZipFile(file.toString());
                zf.extractAll(destDir.getPath());

                //If zip file contained directory that contained book with other files then change
                //place where we look for the book to this directory
                File bookDir = destDir;
                if (bookDir.listFiles().length == 1 & bookDir.listFiles()[0].isDirectory()) {
                    bookDir = bookDir.listFiles()[0];
                }

                //Search for the book, since the gutenberg books don't have unified format,
                //in this work I will assume that first encountered .html or .htm file is the book
                for (File extractedFile : bookDir.listFiles()) {
                    if ((FilenameUtils.getExtension(extractedFile.getName()).equals("html") ||
                            FilenameUtils.getExtension(extractedFile.getName()).equals("htm")) &&
                            !FilenameUtils.removeExtension(extractedFile.getName()).equals("title")) {
                        org.jsoup.nodes.Document bookDoc = ReadBookFile.readBookIn(extractedFile);
                        System.out.println("READING TITLE AND AUTHOR");
                        String bookTitle = ReadBookFile.readBookTitle(bookDoc);
                        String bookAuthor = ReadBookFile.readBookAuthor(bookDoc);
                        System.out.println("TITLE: " + bookTitle);
                        System.out.println("AUTHOR: " + bookAuthor);

                        //If we don't have this book in catalogue after data filtering or we already
                        //analysed such book, then skip it
                        if ((persistenceManager.getGutenbergBook(bookTitle, bookAuthor) == null) ||
                                (persistenceManager.getAnalyzedBookByTitleAndAuthor(bookTitle, bookAuthor) != null)) {
                            System.out.println("IS ALREADY ANALYZED OR NOT IN CATALOGUE");
                            break;
                        }
                        //Lets put it this way, if I can't find book author and title, then I assume this is corrupted
                        //file and I have book author and title as composite primary key anyway
                        if (!bookTitle.isEmpty() && !bookAuthor.isEmpty()) {
                            //Check in the database if we have this book left in catalogue after filtering, if not, skip
                            if (persistenceManager.getGutenbergBook(bookTitle, bookAuthor) != null) {
                                //System.out.println(bookTitle);
                                //System.out.println(bookAuthor);
                                //Read the book
                                System.out.println("READING BOOK CONTENT");
                                String bookContent = ReadBookFile.readBookContent(bookDoc);
                                //                            System.out.println(bookContent);
                                if (!bookContent.isEmpty()) {
                                    //Analyze the book
                                    System.out.println("ANALYZING THE BOOK");
                                    AnalyzedBookModel analyzedBook = AnalyzedBookModel.analyseBook(bookContent);
                                    AnalyzedBookModelId analyzedBookModelId = new AnalyzedBookModelId(bookTitle, bookAuthor);
                                    analyzedBook.setAnalyzedBookModelId(analyzedBookModelId);
                                    System.out.println("SAVING ANALYZED BOOK");
                                    persistenceManager.saveAnalyzedBook(analyzedBook);
                                    System.out.println("RETURN");
                                    return analyzedBook;
                                }
                                break;
                            }
                        }
                    }
                }
                //Delete dir we created after we analyzed all we wanted, otherwise my computer will run out of space
                try {
                    FileUtils.deleteDirectory(destDir);
                    System.out.println("EXTRACTED DIRECTORY DELETED");
                } catch (IOException e) {
                    e.printStackTrace();
                }

            } catch (ZipException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * This class traverses all directories starting with main directory of gutenberg books and analyzez all of them
     */
    public static class TraverseAndAnalyzeBooks extends SimpleFileVisitor<Path> {

        private PersistenceManager persistenceManager;

        @Override
        public FileVisitResult visitFile(Path file,
                                         BasicFileAttributes attr) {
            //Copy here file management code from main class, maybe create separate method for it
            System.out.println("File: " + file);
            AnalyzedBookModel processedBook = processFile(file, persistenceManager);
            if (processedBook != null) {
                System.out.println("***** PROCESSED *****");
                System.out.println("TITLE: " + processedBook.getAnalyzedBookModelId().getTitle());
                System.out.println("AUTHOR: " + processedBook.getAnalyzedBookModelId().getAuthor());
                System.out.println("***** PROCESSED *****");
            }
            return FileVisitResult.CONTINUE;
        }

        // Print each directory visited.
        @Override
        public FileVisitResult postVisitDirectory(Path dir,
                                                  IOException exc) {
            System.out.format("Directory: %s%n", dir);
            return FileVisitResult.CONTINUE;
        }

        // If there is some error accessing
        // the file, let the user know.
        // If you don't override this method
        // and an error occurs, an IOException
        // is thrown.
        @Override
        public FileVisitResult visitFileFailed(Path file,
                                               IOException exc) {
            System.err.println(exc);
            return FileVisitResult.CONTINUE;
        }

        public PersistenceManager getPersistenceManager() {
            return persistenceManager;
        }

        public void setPersistenceManager(PersistenceManager persistenceManager) {
            this.persistenceManager = persistenceManager;
        }
    }

    /**
     * Run this method to trigger books analysis
     */
    public static void startTraverseAndAnalysis() {
        Path startingDir = Paths.get("gutenbergHtmlBooks/www.gutenberg.lib.md.us");
        TraverseAndAnalyzeBooks rb = new TraverseAndAnalyzeBooks();
        rb.setPersistenceManager(new PersistenceManager());
        try {
            Files.walkFileTree(startingDir, rb);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
