QuizService.java

package sk.iway.iwcm.components.quiz.rest;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import sk.iway.iwcm.Tools;
import sk.iway.iwcm.common.CloudToolsForCore;
import sk.iway.iwcm.common.EditorToolsForCore;
import sk.iway.iwcm.components.quiz.jpa.NameValueBean;
import sk.iway.iwcm.components.quiz.jpa.QuizAnswerEntity;
import sk.iway.iwcm.components.quiz.jpa.QuizAnswerRespository;
import sk.iway.iwcm.components.quiz.jpa.QuizEntity;
import sk.iway.iwcm.components.quiz.jpa.QuizQuestionEntity;
import sk.iway.iwcm.components.quiz.jpa.QuizQuestionRepository;
import sk.iway.iwcm.components.quiz.jpa.QuizRepository;
import sk.iway.iwcm.components.quiz.jpa.QuizStatDTO;
import sk.iway.iwcm.components.quiz.jpa.QuizType;
import sk.iway.iwcm.database.SimpleQuery;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.stat.ChartType;
import sk.iway.iwcm.stat.rest.StatService;
import sk.iway.iwcm.system.jpa.JpaTools;
import sk.iway.iwcm.utils.Pair;

public class QuizService {

    static final String rightKey = "components.quiz.stat.right_answers";
    static final String wrongKey = "components.quiz.stat.wrong_answers";
    static final String allKey = "components.quiz.stat.all_answers";
    
    /**************************** GET data methods ****************************/
    public static QuizEntity getById(int id) {
        if(id < 1) return new QuizEntity();
        QuizRepository qr = Tools.getSpringBean("quizRepository", QuizRepository.class);
        Optional<QuizEntity> opt =  qr.findById((long) id);
        if(opt.isPresent()) return opt.get();
        else return new QuizEntity();
    }

    public static List<QuizEntity> getAll() {
        QuizRepository qr = Tools.getSpringBean("quizRepository", QuizRepository.class);
        return qr.findAll();
    }

    public static List<QuizQuestionEntity> getQuizQuestions(int quizId) {
        QuizQuestionRepository qqr = Tools.getSpringBean("quizQuestionRepository", QuizQuestionRepository.class);
        List<QuizQuestionEntity> quizQuestions = qqr.findAllByQuizId(quizId);
        if(quizQuestions == null || quizQuestions.size() < 1) return new ArrayList<>();
        return quizQuestions;
    }

    public static Map<Integer, QuizQuestionEntity> getQuizQuestionsMap(int quizId) { 
        List<QuizQuestionEntity> quizQuestions = getQuizQuestions(quizId);
        
        Map<Integer, QuizQuestionEntity> quizQuestionsByQuestionId = new HashMap<>();
        for (QuizQuestionEntity item : quizQuestions) {
            quizQuestionsByQuestionId.put(item.getId().intValue(), item);
        }

        return quizQuestionsByQuestionId;
    }
    
    /**************************** SAVE data method ****************************/
    public static List<QuizAnswerEntity> saveAnswersAndGetResult(int quizId, String formId, List<NameValueBean> answers) {
        //Get quiz questions
        QuizAnswerRespository qar = Tools.getSpringBean("quizAnswerRespository", QuizAnswerRespository.class);
        Map<Integer, QuizQuestionEntity> quizQuestionsByQuestionId = getQuizQuestionsMap(quizId);

        QuizType quizType = QuizType.getQuizType( (new SimpleQuery()).forString("SELECT quiz_type FROM quiz WHERE id=? AND domain_id=?", quizId, CloudToolsForCore.getDomainId()) );
        for (NameValueBean answer : answers) {
            QuizAnswerEntity existingAnswer = JpaTools.findFirstByProperties(QuizAnswerEntity.class,
                    new Pair<String, Integer>("quizId", quizId), new Pair<String, String>("formId", formId),
                    new Pair<String, Integer>("quizQuestionId", answer.getName()));
            if (existingAnswer != null) {
                break;
            }

            QuizAnswerEntity newAnswer = new QuizAnswerEntity();
            newAnswer.setQuizId(quizId);
            newAnswer.setFormId(formId);
            newAnswer.setQuizQuestionId(answer.getName());
            newAnswer.setAnswer(answer.getValue());

            if(quizType == QuizType.RIGHT_ANSWER) {
                int rightAnswer = quizQuestionsByQuestionId.get(answer.getName()).getRightAnswer();
                boolean isCorrect = rightAnswer == answer.getValue();
                newAnswer.setIsCorrect(isCorrect);
                newAnswer.setRightAnswer(rightAnswer);

                //By default, if answer is correct, set rate to 1
                if(isCorrect) newAnswer.setRate(1);
                else newAnswer.setRate(0);

            } else if(quizType == QuizType.RATED_ANSWER) {
                int rate = quizQuestionsByQuestionId.get( answer.getName() ).getRate( answer.getValue() );
                newAnswer.setRate(rate);

                //If rate is greater than 0, answer is correct
                if(rate > 0) { 
                    newAnswer.setIsCorrect(true);
                    newAnswer.setRightAnswer( answer.getValue() ); //If our answer is correct, set it as right answer
                } else {
                    newAnswer.setIsCorrect(false);
                    newAnswer.setRightAnswer( -1 ); //If our answer is wrong, set -1 as right answer
                }
            }
            newAnswer.setCreated(new Date());
            qar.save(newAnswer);
        }
        return qar.findAllByFormId(formId);
    }

    /**************************** STAT methods ****************************/

    /**
     * Prepare list of QuizStatDTO entities (for chart), where every entity represents one question and has compute number of right/wrong answers for selected date range via "stringRange".
     * 
     * @param quizId - QuizEntity id
     * @param stringRange - clasic string range
     * @param chartType
     * @param quizAnswerRespository
     * @return if any error occur, return empty list
     */    
    public static List<QuizStatDTO> statTableData(Integer quizId, String stringRange, ChartType chartType, QuizAnswerRespository quizAnswerRespository) {
        if(quizId == null || quizId < 1) return new ArrayList<>();
        Date[] dateRangeArr = StatService.processDateRangeString(stringRange);

        //Sort data into map -> based on questionId
        Map<Integer, List<QuizAnswerEntity>> questionAnswers = new HashMap<>();
        for(QuizAnswerEntity answer : quizAnswerRespository.findAllByQuizIdAndCreatedBetweenOrderByQuizQuestionId(quizId, dateRangeArr[0], dateRangeArr[1])) {
            Integer answerQuestionId = answer.getQuizQuestionId();

            if(questionAnswers.get(answerQuestionId) == null) {
                questionAnswers.put(answerQuestionId, new ArrayList<>());
                questionAnswers.get(answerQuestionId).add(answer);
            } else { questionAnswers.get(answerQuestionId).add(answer); }
        }

        //Loop map and compute right/wrong answers + all answers for specific date
        List<QuizStatDTO> stats = new ArrayList<>();
        for (Map.Entry<Integer, List<QuizAnswerEntity>> entry : questionAnswers.entrySet()) {
            QuizStatDTO newStat = null;
            int rightAnswers = 0;
            int wrongAnswers = 0;
            int gainedPoints = 0;
            QuizType quizType = QuizType.RIGHT_ANSWER;

            for(QuizAnswerEntity answer : entry.getValue()) {
                if(newStat == null) {
                    newStat = new QuizStatDTO();
                    newStat.setQuestion(answer.getQuizQuestion().getQuestion());
                    newStat.setImageUrl(answer.getQuizQuestion().getImageUrl());
                    quizType = answer.getQuiz().getQuizTypeEnum();
                    if(quizType == QuizType.RATED_ANSWER) newStat.setQuestionMaxPoints( answer.getQuizQuestion().getMaxRate() );
                }

                if(quizType == QuizType.RATED_ANSWER) gainedPoints += answer.getQuizQuestion().getRate( answer.getAnswer() );

                if(answer.getIsCorrect()) rightAnswers++;
                else wrongAnswers++;
            }

            //If entry is not null, prepare it and add it to list
            if(newStat != null) {
                newStat.setNumberOfRightAnswers(rightAnswers);
                newStat.setNumberOfWrongAnswers(wrongAnswers);
                
                if(chartType == ChartType.NOT_CHART) newStat.setPercentageOfRightAnswers( (float) rightAnswers / (float) (rightAnswers + wrongAnswers) * 100 );
                else newStat.setPercentageOfRightAnswers( (float) Math.round((float) rightAnswers / (float) (rightAnswers + wrongAnswers) * 100) );

                if(quizType == QuizType.RATED_ANSWER) newStat.setAverageGainedPoints( (float) gainedPoints / (float) entry.getValue().size() );
                stats.add(newStat);
            }
        }

        //sort it based on number of right answers
        Collections.sort(stats, Comparator.comparingInt(QuizStatDTO::getNumberOfRightAnswers).reversed());

        //Prepare valid question format
        if(chartType != ChartType.NOT_CHART) {
            for(QuizStatDTO stat : stats) {
                stat.setQuestion( prepareQuestionString( stat.getQuestion() ) );
            }
        }

        return stats;
    }

    /**
     * Nulify hours, minutes, seconds and miliseconds in date. (whole time part)
     * @param date
     * @return
     */
    private static Date prepareDate(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.HOUR, 0);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    public static Map<String, List<QuizStatDTO>> statLineData(Integer quizId, String dateRange, String quizType, QuizAnswerRespository quizAnswerRespository, Prop prop) { 
        if(quizId == null || quizId < 1) return new HashMap<>();
        Date[] dateRangeArr = StatService.processDateRangeString(dateRange);

        if(Tools.isAnyEmpty(quizType) || "0".equals(quizType)) 
            return statLineDataAllAnswers(quizId, dateRangeArr, quizType, quizAnswerRespository, prop);
        else if("1".equals(quizType)) 
            return statLineDataAnswers(quizId, dateRangeArr, quizType, quizAnswerRespository, prop);
        else 
            return new HashMap<>();
    }

    private static Map<String, List<QuizStatDTO>> statLineDataAllAnswers(Integer quizId, Date[] dateRangeArr, String quizType, QuizAnswerRespository quizAnswerRespository, Prop prop) {
        //Create combination of dates and number of right/wrong answers (separe maps)
        Map<Date, Integer> rightAnswPerDay = new HashMap<>();
        Map<Date, Integer> wrongAnswPerDay = new HashMap<>();
        for(QuizAnswerEntity answer : quizAnswerRespository.findAllByQuizIdAndCreatedBetweenOrderByQuizQuestionId(quizId, dateRangeArr[0], dateRangeArr[1])) { 
            Date created = prepareDate( answer.getCreated() );

            if(answer.getIsCorrect()) {
                if(rightAnswPerDay.get(created) == null) rightAnswPerDay.put(created, 1);
                else rightAnswPerDay.put(created, rightAnswPerDay.get(created) + 1);
            } else {
                if(wrongAnswPerDay.get(created) == null) wrongAnswPerDay.put(created, 1);
                else wrongAnswPerDay.put(created, wrongAnswPerDay.get(created) + 1);
            }
        }

        List<QuizStatDTO> rightAnsw = new ArrayList<>();
        for (Map.Entry<Date, Integer> entry : rightAnswPerDay.entrySet()) {
            QuizStatDTO newStatAnsw = new QuizStatDTO();
            newStatAnsw.setDayDate(entry.getKey());
            newStatAnsw.setChartValue(entry.getValue());
            rightAnsw.add(newStatAnsw);
        }

        List<QuizStatDTO> wrongAnsw = new ArrayList<>();
        for (Map.Entry<Date, Integer> entry : wrongAnswPerDay.entrySet()) {
            QuizStatDTO newStatAnsw = new QuizStatDTO();
            newStatAnsw.setDayDate(entry.getKey());
            newStatAnsw.setChartValue(entry.getValue());
            wrongAnsw.add(newStatAnsw);
        }

        //Combine days value (combine maps) -> put/sum right map into wrong map
        for (Map.Entry<Date, Integer> right : rightAnswPerDay.entrySet()) {

            Integer wrongValue = wrongAnswPerDay.get( right.getKey() );
            if(wrongValue == null) {
                wrongAnswPerDay.put(right.getKey(), right.getValue());
            } else {
                wrongAnswPerDay.put(right.getKey(), right.getValue() + wrongValue);
            }
        }
        //Loop combined map and create list of QuizStatDTO
        List<QuizStatDTO> allAnsw = new ArrayList<>();
        for (Map.Entry<Date, Integer> combined : wrongAnswPerDay.entrySet()) { 
            QuizStatDTO newStat = new QuizStatDTO();
            newStat.setDayDate(combined.getKey());
            newStat.setChartValue(combined.getValue());
            allAnsw.add(newStat);
        }

        Map<String, List<QuizStatDTO>> lineChartData = new HashMap<>();
        lineChartData.put(prop.getText(rightKey), rightAnsw);
        lineChartData.put(prop.getText(wrongKey), wrongAnsw);
        lineChartData.put(prop.getText(allKey), allAnsw);

        return lineChartData;
    }

    private static Map<String, List<QuizStatDTO>> statLineDataAnswers(Integer quizId, Date[] dateRangeArr, String quizType, QuizAnswerRespository quizAnswerRespository, Prop prop) {
        //Compute points sum for every question for every day (time part of date is nulified)
        Map<String, QuizStatDTO> valueByDateAndQuestion = new HashMap<>();
        for(QuizAnswerEntity answer : quizAnswerRespository.findAllByQuizIdAndCreatedBetweenOrderByQuizQuestionId(quizId, dateRangeArr[0], dateRangeArr[1])) { 
            Date created = prepareDate( answer.getCreated() );
            String mapKey = "" + created.getTime() + "_" + prepareQuestionString( answer.getQuizQuestion().getQuestion() );
            
            if(valueByDateAndQuestion.get(mapKey) == null) {
                QuizStatDTO newStat = new QuizStatDTO();
                newStat.setDayDate(created);
                newStat.setChartValue(answer.getRate());
                
                if(answer.getIsCorrect()) {
                    newStat.setNumberOfRightAnswers(1);
                    newStat.setNumberOfWrongAnswers(0);
                } else {
                    newStat.setNumberOfRightAnswers(0);
                    newStat.setNumberOfWrongAnswers(1);
                }
                valueByDateAndQuestion.put(mapKey, newStat);
            } else {
                valueByDateAndQuestion.get(mapKey).setChartValue( valueByDateAndQuestion.get(mapKey).getChartValue() + answer.getRate() );
                if(answer.getIsCorrect()) 
                    valueByDateAndQuestion.get(mapKey).setNumberOfRightAnswers( valueByDateAndQuestion.get(mapKey).getNumberOfRightAnswers() + 1 );
                else
                    valueByDateAndQuestion.get(mapKey).setNumberOfWrongAnswers( valueByDateAndQuestion.get(mapKey).getNumberOfWrongAnswers() + 1 );
            }
        }

        //Tranform data to line chart format
        Map<String, List<QuizStatDTO>> lineChartData = new HashMap<>();
        for (Map.Entry<String, QuizStatDTO> entry : valueByDateAndQuestion.entrySet()) { 
            String[] keyParts = entry.getKey().split("_");

            if(lineChartData.get(keyParts[1]) == null) lineChartData.put(keyParts[1], new ArrayList<>());

            lineChartData.get(keyParts[1]).add(entry.getValue());
        }

        return lineChartData;
    }

    /**
     * Remove Html tags from string.
     * If string is longer than 64 characters, cut it and add "..." at the end.
     * else return string.
     * 
     * If string is null, return empty string.
     * 
     * @param question
     * @return
     */
    private static String prepareQuestionString (String question) {
        if(question == null)  return "";

        String formatted = EditorToolsForCore.removeHtmlTagsKeepLength( question );
        if(formatted.length() < 64) return formatted;
        else return formatted.substring(0, 64) + "...";
    }
}