package com.ruoyi.web.service;

import com.alibaba.fastjson2.JSONObject;
import com.github.pagehelper.PageHelper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.dz.domain.DzSubject;
import com.ruoyi.dz.mapper.DzSubjectMapper;
import com.ruoyi.enums.PaperStatus;
import com.ruoyi.enums.PaperType;
import com.ruoyi.enums.QuestionType;
import com.ruoyi.learn.domain.*;
import com.ruoyi.learn.mapper.*;
import com.ruoyi.learn.service.ILearnQuestionsService;
import com.ruoyi.syzy.domain.BBusiWishUniversities;
import com.ruoyi.syzy.mapper.BBusiWishUniversitiesMapper;
import com.ruoyi.syzy.service.IBBusiWishUniversitiesService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 考卷服务
 */
@Service
@Slf4j
public class PaperService {
    Set<QuestionType> chooseTypes = Sets.newHashSet(QuestionType.Judgment, QuestionType.Multiple, QuestionType.Single);
    private final LearnPaperMapper paperMapper;
    private final LearnPaperQuestionMapper paperQuestionMapper;
    private final ILearnQuestionsService questionsService;
    private final ILearnQuestionsService learnQuestionsService;
    private final LearnDirectedKnowledgeMapper learnDirectedKnowledgeMapper;
    private final IBBusiWishUniversitiesService wishUniversitiesService;
    private final BBusiWishUniversitiesMapper bBusiWishUniversitiesMapper;
    private final LearnKnowledgeTreeMapper learnKnowledgeTreeMapper;
    private final LearnCultureKnowledgeMapper learnCultureKnowledgeMapper;
    private final DzSubjectMapper dzSubjectMapper;

    PaperService(LearnPaperMapper paperMapper, LearnPaperQuestionMapper paperQuestionMapper, ILearnQuestionsService questionsService, ILearnQuestionsService learnQuestionsService, LearnDirectedKnowledgeMapper learnDirectedKnowledgeMapper, IBBusiWishUniversitiesService wishUniversitiesService, BBusiWishUniversitiesMapper bBusiWishUniversitiesMapper, LearnKnowledgeTreeMapper learnKnowledgeTreeMapper, LearnCultureKnowledgeMapper learnCultureKnowledgeMapper, DzSubjectMapper dzSubjectMapper) {
        this.paperMapper = paperMapper;
        this.paperQuestionMapper = paperQuestionMapper;
        this.questionsService = questionsService;
        this.learnQuestionsService = learnQuestionsService;
        this.learnDirectedKnowledgeMapper = learnDirectedKnowledgeMapper;
        this.wishUniversitiesService = wishUniversitiesService;
        this.bBusiWishUniversitiesMapper = bBusiWishUniversitiesMapper;
        this.learnKnowledgeTreeMapper = learnKnowledgeTreeMapper;
        this.learnCultureKnowledgeMapper = learnCultureKnowledgeMapper;
        this.dzSubjectMapper = dzSubjectMapper;
        // buildAllPapers(2026, 913, false);
        // buildSimulatedPaperForUniversity(20950L, 11L, 78L, 2);
        // buildSimulatedPaperForUniversity(20962L, 11L, 156L, 2);
        // test2();
        // testCulture(2026, 912, false); // 909-911 三套
        // corrQuestions();
    }

    public static String convertImg2Html(String text) {
        String urlPattern = "!\\[\\]\\((https?://[\\w.-]+\\.[a-zA-Z]{2,}[/\\w.-]*\\??[/\\w.=&%-]*)\\)";
        Pattern pattern = Pattern.compile(urlPattern);
        Matcher matcher = pattern.matcher(text);

        StringBuffer result = new StringBuffer();
        while (matcher.find()) {
            String url = matcher.group(1);
            String replacement = String.format("<img src=\"%s\" />", url);
            matcher.appendReplacement(result, replacement);
        }
        matcher.appendTail(result);
        return result.toString();
    }

    public void corrQuestions() {
        LearnQuestions cond = new LearnQuestions();
        Long id = 0L;
        Integer pageSize = 0;
        List<LearnQuestions> questionsList;

        String newValue;
        do {
            cond.setId(id);
            PageHelper.startPage(1, pageSize, null);
            questionsList = questionsService.selectLearnQuestionsList(cond);
            for(LearnQuestions q : questionsList) {
                boolean updated = false;
                LearnQuestions up = new LearnQuestions();
                up.setId(q.getId());
                if(!q.getTitle().equals((newValue = convertImg2Html(q.getTitle())))) {
                    up.setTitle(newValue);
                    updated = true;
                }
                if(StringUtils.isNotBlank(q.getTitle0())) {
                    if(!q.getTitle0().equals((newValue = convertImg2Html(q.getTitle0())))) {
                        up.setTitle0(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getAnswer2())) {
                    if(!q.getAnswer2().equals(newValue = convertImg2Html(q.getAnswer2()))) {
                        up.setAnswer2(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getParse())) {
                    if(!q.getParse().equals(newValue = convertImg2Html(q.getParse()))) {
                        up.setParse(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionA())) {
                    if(!q.getOptionA().equals(newValue = convertImg2Html(q.getOptionA()))) {
                        up.setOptionA(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionB())) {
                    if(!q.getOptionB().equals(newValue = convertImg2Html(q.getOptionB()))) {
                        up.setOptionB(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionC())) {
                    if(!q.getOptionC().equals(newValue = convertImg2Html(q.getOptionC()))) {
                        up.setOptionC(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionD())) {
                    if(!q.getOptionD().equals(newValue = convertImg2Html(q.getOptionD()))) {
                        up.setOptionD(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionE())) {
                    if(!q.getOptionE().equals(newValue = convertImg2Html(q.getOptionE()))) {
                        up.setOptionE(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionF())) {
                    if(!q.getOptionF().equals(newValue = convertImg2Html(q.getOptionF()))) {
                        up.setOptionF(newValue);
                        updated = true;
                    }
                }
                if(StringUtils.isNotBlank(q.getOptionG())) {
                    if(!q.getOptionG().equals(newValue = convertImg2Html(q.getOptionG()))) {
                        up.setOptionG(newValue);
                        updated = true;
                    }
                }
                if(updated) {
                    up.setIsUpdate(1L);
                    questionsService.updateLearnQuestions(up);
                }
                id = q.getId();
            }
        } while(questionsList.size() != pageSize);
    }

    public void buildAllPapers(Integer year, Integer seq, boolean fillExclude) {
        LearnDirectedKnowledge dkCond = new LearnDirectedKnowledge();
        // dkCond.setMatchYear(year);
        dkCond.setYear(year);
        Map<Long, List<LearnDirectedKnowledge>> universityDirectedMap = learnDirectedKnowledgeMapper.selectLearnDirectedKnowledgeList(dkCond).stream().collect(Collectors.groupingBy(LearnDirectedKnowledge::getUniversityId));

        Map uCond = new HashMap();
        uCond.put("ids", universityDirectedMap.keySet());
        Map<Long, BBusiWishUniversities> universityMap = bBusiWishUniversitiesMapper.selectBBusiWishUniversitiesListSimpleByIds(uCond).stream().collect(Collectors.toMap(BBusiWishUniversities::getId, Function.identity()));
        List<Long> dkIdList = Lists.newArrayList();
        Set<Long> validIdSet = Sets.newHashSet(907L,908L,879L,769L,770L); // 911/912(907,908,879,769,770),913(907L,908L,879L,769L,770L:845L,846L,907L,908L,674L,879L,896L,769L,770L,709L,710L)
        for(Long universityId : universityDirectedMap.keySet()) {
            BBusiWishUniversities universities = universityMap.get(universityId);
            if(null == universities) {
                continue;
            }
            for(LearnDirectedKnowledge dk : universityDirectedMap.get(universityId)) {
                if(validIdSet.size() > 0 && !validIdSet.contains(dk.getId())) {
                    continue;
                }
                try {
                    buildSimulatedPaperForKnowledge("DK" + universityId,11L, seq, fillExclude, universities, dk);
                } catch(Exception e) {
                    dkIdList.add(dk.getId());
                    log.error(e.getMessage());
                }
            }
        }
        if(dkIdList.size() > 0) {
            System.out.println(StringUtils.join(dkIdList, ","));
        }
    }

    public void testCulture(Integer year, Integer seq, boolean fillExclude) {
        LearnCultureKnowledge ckCond = new LearnCultureKnowledge();
        ckCond.setYear(year);
        // ckCond.setUniversityId(20154L);
        List<LearnCultureKnowledge> cultureKnowledgeList = learnCultureKnowledgeMapper.selectLearnCultureKnowledgeList(ckCond).stream().filter(t -> t.getId() >= 215).collect(Collectors.toList());

        List<Long> errorIdList = Lists.newArrayList();
        Map uCond = new HashMap();
        Set<Long> validIdSet = Sets.newHashSet(372L); // 372L 910; 372L,335L,356L,362L 911
        uCond.put("ids", cultureKnowledgeList.stream().map(LearnCultureKnowledge::getUniversityId).collect(Collectors.toSet()));
        Map<Long, BBusiWishUniversities> universityMap = bBusiWishUniversitiesMapper.selectBBusiWishUniversitiesListSimpleByIds(uCond).stream().collect(Collectors.toMap(BBusiWishUniversities::getId, Function.identity()));
        Map<Long, String> subjectMap = dzSubjectMapper.selectDzSubjectList(new DzSubject()).stream().collect(Collectors.toMap(DzSubject::getSubjectId, DzSubject::getSubjectName));
        List<LearnCultureKnowledge> errList = Lists.newArrayList();
        for(LearnCultureKnowledge ck : cultureKnowledgeList) {
            if(!validIdSet.isEmpty() && !validIdSet.contains(ck.getId())) {
                continue;
            }
            TestPaperVO.PaperDef2 paperDef = new TestPaperVO.PaperDef2(ck.getConditions());
            if(ck.getScore() != (int) Math.round(paperDef.getScore())) {
                log.error("分值不对称: " + ck.getId());
                errList.add(ck);
                continue;
            }
            paperDef.setFillExclude(fillExclude);

            BBusiWishUniversities universities = universityMap.get(ck.getUniversityId());
            LearnPaper paper = new LearnPaper();
            paper.setSubjectId(Long.parseLong(ck.getSubjects()));
            paper.setRelateId(ck.getId());
            paper.setPaperName(universities.getName() + "(" + subjectMap.get(paper.getSubjectId()) + ")");
            paper.setExamineeTypes(ck.getExamineeTypes());
            paper.setPaperType(PaperType.Simulated.name());
            paper.setYear(ck.getYear());
            paper.setPaperSource(seq);
            paper.setDirectKey(universities.getId() + "_" + ck.getExamineeTypes() + "_");
            paper.setStatus(PaperStatus.Valid.getVal());
            if(CollectionUtils.isNotEmpty(paperMapper.selectLearnPaperList(paper))) {
                log.warn("已经生成: {}:{}", ck.getId(), seq);
                continue;
            }
            paper.setNumber(paperDef.getTotal());
            paper.setFenshu(paperDef.getScore().intValue());
            AnswerSheet.PaperCond info = new AnswerSheet.PaperCond();
            info.setScore(paper.getFenshu());
            info.setTime(ck.getTime().intValue() * 60);
            info.setTypes(paperDef.getTypes().stream().map(t -> new AnswerSheet.PaperCondType(t.getType().getTitle(), t.getCount(), t.getScore())).collect(Collectors.toList()));
            paper.setPaperInfo(JSONObject.toJSONString(info));
            try {
                Pair<LearnPaper, List<LearnPaperQuestion>> paperResult = buildPaper2("CK" + ck.getUniversityId(),null, paper, paperDef);
                savePaper(paperResult.getKey(), paperResult.getValue());
                continue;
            } catch(Exception e) {
                errorIdList.add(ck.getId());
                log.error(ck.getId() + ":" + e.getMessage());
            }
        }
        if(!errorIdList.isEmpty()) {
            System.out.println(StringUtils.join(errorIdList, ","));
        }
    }

    public void test2() {
        TestPaperVO.PaperDef paperDef = new TestPaperVO.PaperDef();
        paperDef.setFillExclude(true);
        paperDef.setKnowIds(Lists.newArrayList(1016L,1101L,1091L, 1103L));
        List<TestPaperVO.TypeDef> typeDefList= Lists.newArrayList();
        typeDefList.add(new TestPaperVO.TypeDef("判断题", "判断题", 20, 2));
        typeDefList.add(new TestPaperVO.TypeDef("单选题", "单选题", 10, 1));
        typeDefList.add(new TestPaperVO.TypeDef("多选题", "多选题", 10, 2));
        paperDef.setTypes(typeDefList);
        paperDef.setTotal(40L);
        List<LearnPaperQuestion> questionList = getQuestionsByType(null, paperDef);
        return;
    }

    public void test() {
//        PaperDef paperDef = new PaperDef();
//        paperDef.setFillExclude(true);
//        paperDef.setKnowIds(Lists.newArrayList(133614L,130166L,130187L));
//        List<TypeDef> typeDefList= Lists.newArrayList();
//        typeDefList.add(new TypeDef("单选题", "单选题", 80, 1));
//        typeDefList.add(new TypeDef("判断题", "判断题", 10, 2));
//        paperDef.setTypes(typeDefList);
//        getQuestions(1L, 100L, paperDef);
    }

    public PaperVO loadPaper(Long paperId) {
        LearnPaper learnPaper = paperMapper.selectLearnPaperById(paperId);
        PaperVO result = new PaperVO();
        BeanUtils.copyProperties(learnPaper, result);
        result.setQuestions(loadPaperQuestions(paperId));
        return result;
    }

    /**
     * 加载试卷
     * @param paperId
     * @return
     */
    public List<PaperVO.QuestionSeq> loadPaperQuestions(Long paperId) {
        List<LearnQuestions> questions = questionsService.selectQuestionByPaperId(paperId);
        Map<String, PaperVO.QuestionSeq> gropuMap = Maps.newHashMap();
        List<PaperVO.QuestionSeq> paperQuestionList = Lists.newArrayList();
        for(LearnQuestions lqs : questions) {
            PaperVO.QuestionSeq qs = new PaperVO.QuestionSeq();
            BeanUtils.copyProperties(lqs, qs, "options", "parse", "answer1", "answer2");
            QuestionType qt = QuestionType.of(lqs.getQtpye());
            qs.setTypeId(qt.getVal());
            qs.setType(qt.getTitle());
            qs.setQtpye(qt.getTitle()); // TODO 兼容
            qs.setTotalScore(lqs.getScore()); // 与答卷使用一致名称
            if (!chooseTypes.contains(qt)) {
                qs.setOptions(Lists.newArrayList("会", "不会"));
                qs.setParse(lqs.getParse());
                qs.setAnswer1(lqs.getAnswer1());
                qs.setAnswer2(lqs.getAnswer2());
            } else {
                qs.setOptions(StringUtils.getOptions(lqs.getOptionA(), lqs.getOptionB(), lqs.getOptionC(), lqs.getOptionD(), lqs.getOptionE(), lqs.getOptionF(), lqs.getOptionG()));
            }
            if(StringUtils.isNotBlank(lqs.getTitle0())) { // 大题
                PaperVO.QuestionSeq qg = gropuMap.get(lqs.getTitle0());
                if(qg == null) {
                    qg = new PaperVO.QuestionSeq();
                    qg.setTypeId(99);
                    qg.setSubQuestions(Lists.newArrayList());
                    qg.setTitle(lqs.getTitle0());
                    gropuMap.put(lqs.getTitle0(), qg);
                    paperQuestionList.add(qg);
                }
                qg.getSubQuestions().add(qs);
            } else {
                qs.setTypeTitle(lqs.getPaperTypeTitle());
                paperQuestionList.add(qs);
            }
        }
        return paperQuestionList;
    }

    public List<PaperVO.QuestionAnswer> loadPaperQuestionAnswers(Long userId, Long paperId, Map<Long, LearnAnswer> answerMap, boolean withParse) {
        List<LearnQuestions> questions = questionsService.selectQuestionByPaperId(paperId);
        Map<String, PaperVO.QuestionAnswer> gropuMap = Maps.newHashMap();
        List<PaperVO.QuestionAnswer> paperQuestionList = Lists.newArrayList();
        LearnAnswer answer;
        for(LearnQuestions lqs : questions) {
            PaperVO.QuestionAnswer qs = new PaperVO.QuestionAnswer();
            QuestionType qt = QuestionType.of(lqs.getQtpye());
            if(withParse || !chooseTypes.contains(qt)) {
                BeanUtils.copyProperties(lqs, qs, "title", "options");
            } else {
                BeanUtils.copyProperties(lqs, qs, "title", "options", "parse", "answer1", "answer2");
            }
            qs.setTotalScore(lqs.getScore());
            qs.setScore(null);
            qs.setTypeId(qt.getVal());
            qs.setIsFavorite(lqs.isCollect());
            if(null != answerMap && null != (answer = answerMap.get(lqs.getId()))) {
                qs.setAnswerId(answer.getAnswerId());
                qs.setAnswers(Arrays.asList(answer.getAnswer().split(",")));
                qs.setDuration(answer.getDuration());
                qs.setState(answer.getState());
                qs.setIsMark(answer.getMark());
                qs.setIsNotKnow(answer.getNotKnow());
                qs.setScore(answer.getScore());
            }
            if(StringUtils.isNotBlank(lqs.getTitle0())) { // 大题
                PaperVO.QuestionAnswer qg = gropuMap.get(lqs.getTitle0());
                if(qg == null) {
                    qg = new PaperVO.QuestionAnswer();
                    qg.setTypeId(99);
                    qg.setSubQuestions(Lists.newArrayList());
                    qg.setTitle(lqs.getTitle0());
                    gropuMap.put(lqs.getTitle0(), qg);
                    paperQuestionList.add(qg);
                }
                qg.getSubQuestions().add(qs);
            } else {
                paperQuestionList.add(qs);
            }
        }
        return paperQuestionList;
    }

    public int buildSimulatedPaperForUniversity(String filterType, boolean fillExclude, Long universityId, Long subjectId, Long directedId, Integer seq) {
        LearnDirectedKnowledge dkCond = new LearnDirectedKnowledge();
        dkCond.setUniversityId(universityId);
        List<LearnDirectedKnowledge> directedKnowledgeList = learnDirectedKnowledgeMapper.selectLearnDirectedKnowledgeList(dkCond);
        BBusiWishUniversities universities = wishUniversitiesService.selectBBusiWishUniversitiesById(universityId);
        for(LearnDirectedKnowledge dk : directedKnowledgeList) {
            if(null == directedId || directedId.equals(dk.getId())) {
                buildSimulatedPaperForKnowledge(filterType, subjectId, seq, fillExclude, universities, dk);
            }
        }
        return 0;
    }

    /**
     * 根据院校专业要求生成模拟试卷
     * @return
     */
    public int buildSimulatedPaperForKnowledge(String filterType, Long subjectId, Integer seq, boolean fillExclude, BBusiWishUniversities universities, LearnDirectedKnowledge dk) {
        if(StringUtils.isBlank(dk.getConditions())) {
            return 0;
        }
        TestPaperVO.PaperDef2 paperDef = new TestPaperVO.PaperDef2(dk.getKnowledges(), dk.getConditions());
        paperDef.setFillExclude(fillExclude);

        LearnPaper paper = new LearnPaper();
        paper.setSubjectId(subjectId);
        paper.setPaperType(PaperType.Simulated.name());
        paper.setRelateId(dk.getId()); // 定向ID
        paper.setYear(dk.getYear());
        paper.setPaperSource(seq);
        paper.setStatus(1);
        if(CollectionUtils.isNotEmpty(paperMapper.selectLearnPaperList(paper))) {
            log.warn("已经生成: {}:{}", dk.getId(), seq);
            return 0;
        }
        paper.setPaperName(StringUtils.isNotBlank(dk.getDirectKey()) ? universities.getName() + "(" + dk.getDirectKey() + ")" : universities.getName());
        paper.setDirectKey(universities.getId() + "_" + dk.getExamineeTypes() + "_" + dk.getDirectKey());
        paper.setStatus(PaperStatus.Valid.getVal());
        paper.setNumber(paperDef.getTotal());
        paper.setFenshu(paperDef.getScore().intValue());
        AnswerSheet.PaperCond info = new AnswerSheet.PaperCond();
        info.setScore(paper.getFenshu());
        info.setTime(dk.getTime() * 60);
        info.setTypes(paperDef.getTypes().stream().map(t -> new AnswerSheet.PaperCondType(t.getType().getTitle(), t.getCount(), t.getScore())).collect(Collectors.toList()));
        paper.setPaperInfo(JSONObject.toJSONString(info));
        Pair<LearnPaper, List<LearnPaperQuestion>> paperResult = buildPaper2(filterType, null, paper, paperDef);
        savePaper(paperResult.getKey(), paperResult.getValue());
        return 0;
    }

    public Pair<LearnPaper, List<LearnPaperQuestion>> buildPaper2(String filterType, Long studentId, LearnPaper paper, TestPaperVO.PaperDef2 paperDef) {
        paperDef.setFillExclude(null != studentId || StringUtils.isNotBlank(filterType));
        List<LearnPaperQuestion> pqList = getQuestions2(filterType, studentId, paperDef);
        return Pair.of(paper, pqList);
    }

    /**
     * 根据试卷定义生成试卷
     * @param studentId
     * @param paper
     * @param paperDef
     * @return
     */
    public Pair<LearnPaper, List<LearnPaperQuestion>> buildPaper(Long studentId, LearnPaper paper, TestPaperVO.PaperDef paperDef) {
        if(null == studentId){
            paperDef.setFillExclude(false);
        }
        List<LearnPaperQuestion> pqList = getQuestions(studentId, paperDef);
        return Pair.of(paper, pqList);
    }

    /**
     * 保存试卷
     * @param paper
     * @param pqList
     * @return
     */
    public LearnPaper savePaper(LearnPaper paper, List<LearnPaperQuestion> pqList) {
        paper.setNumber(pqList.size());
        paper.setFenshu((int) Math.round(pqList.stream().mapToDouble(LearnPaperQuestion::getScore).sum()));
        paperMapper.insertLearnPaper(paper);
        Long paperId = paper.getId();
        pqList.stream().forEach(t -> t.setPaperId(paperId));
        paperQuestionMapper.batchInsert(pqList);
        return paper;
    }

    /**
     * 按类型，知识点平均分配组卷
     * @param studentId
     * @param paperDef
     * @return
     */
    public List<LearnPaperQuestion> getQuestionsByType(Long studentId, TestPaperVO.PaperDef paperDef) {
        // 统计知识点+类型的有效数量 TODO 总量可以缓存
        Map<String, KnowTypeAssign> knowTypeAssignMap = buildKnowTypeAssignMap(null, studentId, paperDef);
        assignTypeFirstWithCount(paperDef, knowTypeAssignMap); // 知识优先，类型可变
        return getQuestions(studentId, paperDef, knowTypeAssignMap);
    }

    public List<LearnPaperQuestion> getQuestions2(String filterType, Long studentId, TestPaperVO.PaperDef2 paperDef) {
        List<LearnPaperQuestion> pqList = Lists.newArrayList();
        Set<String> existQuestionSet = Sets.newHashSet();
        for(TestPaperVO.KnowledgeTypeDef2 ktd : paperDef.getKnowTypes()) {
            Map<Long, List<Long>> ktSubMap = Maps.newHashMap();
            Integer maxSubCount = 6;
            List<Long> newKnownList = Lists.newArrayList();
            List<Long> tailKnownList = Lists.newArrayList();
            List<LearnKnowledgeTree> ktList = learnKnowledgeTreeMapper.selectLearnKnowledgeTreeByParentIds(ktd.getKnowledges());
            for(LearnKnowledgeTree kt : ktList) {
                List<Long> subIdList = ktSubMap.computeIfAbsent(kt.getPid(), k -> Lists.newArrayList());
                subIdList.add(kt.getId());
            }
            for(Long knownId : ktd.getKnowledges()) {
                List<Long> subList = ktSubMap.get(knownId);
                if(null == subList) {
                    newKnownList.add(knownId);
                } else if(subList.size() > maxSubCount) {
                    newKnownList.addAll(subList.subList(0, maxSubCount));
                    tailKnownList.addAll(subList.subList(maxSubCount, subList.size()));
                } else {
                    newKnownList.addAll(subList);
                }

            }
            newKnownList.addAll(tailKnownList);
            ktd.setKnowledges(newKnownList);
            String isSubType = null == ktd.getSubType() ? "0" : ktd.getSubType().toString();
            Map<String, KnowTypeAssign> knowTypeAssignMap = buildKnowTypeAssignMap(filterType, studentId, isSubType, ktd.getTypes().get(0).getCount(), ktd.getTypes().stream().map(t -> t.getType().getTitle()).collect(Collectors.toList()), newKnownList, paperDef.getFillExclude(), 0);
            assignTypeFirst(paperDef.getFillExclude(), ktd, knowTypeAssignMap);
            pqList.addAll(getQuestions2(filterType, studentId, ktd.getCount(), pqList.size(), newKnownList, isSubType, ktd.getTypes(), knowTypeAssignMap, existQuestionSet));
        }
        reSort(pqList);
        return pqList;
    }

    public List<LearnPaperQuestion> getQuestionsByRandom(Long studentId, Integer total, Collection<Long> knowledgeIds, List<String> types, Integer questionType) {
        Map<String, KnowTypeAssign> knowTypeAssignMap = buildKnowTypeAssignMap(null, studentId, "2", null, types, knowledgeIds, true, questionType);
        List<KnowTypeAssign> knowTypeAssignList = Lists.newArrayList(knowTypeAssignMap.values());
        List<LearnPaperQuestion> pqList = Lists.newArrayList();
        Set<String> existQuestionSet = Sets.newHashSet();
        Random random = ThreadLocalRandom.current();
        Map<String, List<LearnQuestions>> typeQuestionMap = Maps.newHashMap();
        LearnQuestions qCond = new LearnQuestions();
        qCond.setTypeId(questionType);
        // excl
        do {
            if(knowTypeAssignList.isEmpty()) {
                break;
            }
            int idx = random.nextInt(knowTypeAssignList.size());
            KnowTypeAssign knowTypeAssign = knowTypeAssignList.get(idx);
            if(knowTypeAssign.getExclCount() > knowTypeAssign.getExclAssign()) {
                List<LearnQuestions> questions = typeQuestionMap.get(knowTypeAssign.getType());
                if (null == questions) {
                    qCond.setKnowledgeId(knowTypeAssign.getKnowId());
                    qCond.setQtpye(knowTypeAssign.getType());
                    qCond.setId(studentId);
                    qCond.setNumber(knowTypeAssign.exclCount > 500 ? (long) random.nextInt(knowTypeAssign.exclCount.intValue() - 500) : 0L);
                    qCond.setIsSubType("2");
                    questions = questionsService.selectQuestionsForPaper(qCond);
                    typeQuestionMap.put(knowTypeAssign.getType(), questions);
                }
                if (!questions.isEmpty()) {
                    int oldSize = pqList.size();
                    addRandomList(knowTypeAssign.getKnowId(), QuestionType.of(knowTypeAssign.getType()), questions, random, total.longValue(), 1L, 1.0, existQuestionSet, 1, pqList);
                    if (oldSize != pqList.size()) {
                        knowTypeAssign.exclAssign++;
                    } else {
                        knowTypeAssignList.remove(idx);
                    }
                } else {
                    knowTypeAssignList.remove(idx);
                }
            } else {
                knowTypeAssignList.remove(idx);
            }
        } while(pqList.size() < total);

        // 补充
        qCond.setId(null);
        typeQuestionMap.clear();
        knowTypeAssignList = Lists.newArrayList(knowTypeAssignMap.values());
        while(pqList.size() < total) {
            if(knowTypeAssignList.isEmpty()) {
                break;
            }
            int idx = random.nextInt(knowTypeAssignList.size());
            KnowTypeAssign knowTypeAssign = knowTypeAssignList.get(idx);
            if(knowTypeAssign.getTotal() > knowTypeAssign.getAssign()) {
                List<LearnQuestions> questions = typeQuestionMap.get(knowTypeAssign.getType());
                if (null == questions) {
                    qCond.setKnowledgeId(knowTypeAssign.getKnowId());
                    qCond.setQtpye(knowTypeAssign.getType());
                    qCond.setNumber(knowTypeAssign.total > 500 ? (long) random.nextInt(knowTypeAssign.total.intValue() - 500) : 0L);
                    qCond.setIsSubType("2");
                    questions = questionsService.selectQuestionsForPaper(qCond);
                    typeQuestionMap.put(knowTypeAssign.getType(), questions);
                }
                if (!questions.isEmpty()) {
                    int oldSize = pqList.size();
                    addRandomList(knowTypeAssign.getKnowId(), QuestionType.of(knowTypeAssign.getType()), questions, random, total.longValue(), 1L, 1.0, existQuestionSet, 1, pqList);
                    if (oldSize != pqList.size()) {
                        knowTypeAssign.assign++;
                    } else {
                        knowTypeAssignList.remove(idx);
                    }
                } else {
                    knowTypeAssignList.remove(idx);
                }
            } else {
                knowTypeAssignList.remove(idx);
            }
        };
        reSort(pqList);
        return pqList;
    }

    /**
     * // diff(type), paperId(parentId), seq(type), questionId(id)
     * @param pqList
     */
    private void reSort(List<LearnPaperQuestion> pqList) {
        Collections.sort(pqList, new Comparator<LearnPaperQuestion>() {
            @Override
            public int compare(LearnPaperQuestion o1, LearnPaperQuestion o2) {
                int iRet;
                if(0 != (iRet = o1.getDiff().compareTo(o2.getDiff())) || o1.getPaperId().equals(0L)) {
                    return iRet;
                }
                if(0 != (iRet = o1.getPaperId().compareTo(o2.getPaperId()))) {
                    return iRet;
                }
                if(0 != (iRet = o1.getSeq().compareTo(o2.getSeq()))) {
                    return iRet;
                }
                return o1.getQuestionId().compareTo(o2.getQuestionId());
            }
        });
        Integer[] idx = {1};
        pqList.forEach(t -> {
            t.setSeq(idx[0]++);
            t.setPaperId(null);
            t.setDiff(null);
        });
    }

    /**
     * 按知识点，题型平均分配组卷
     * @param studentId
     * @param paperDef
     * @return
     */
    public List<LearnPaperQuestion> getQuestions(Long studentId, TestPaperVO.PaperDef paperDef) {
        // 题型分布定义, 知识点列表, 分值定义
        // 统计知识点+类型的有效数量 TODO 总量可以缓存
        Map<String, KnowTypeAssign> knowTypeAssignMap = buildKnowTypeAssignMap(null, studentId, paperDef);
        assignKnowFirst(paperDef, knowTypeAssignMap); // 知识优先，类型可变
        return getQuestions(studentId, paperDef, knowTypeAssignMap);
    }

    private void assignTypeFirstWithCount(TestPaperVO.PaperDef paperDef, Map<String, KnowTypeAssign> knowTypeAssignMap) {
        AtomicLong assignCount = new AtomicLong(0);
        Map<Long, Long> knownAdjMap = Maps.newHashMap();
        Map<String, Set<Long>> typeKnowIdMap = Maps.newHashMap();
        Map<String, Long> typeFillCntMap = Maps.newHashMap();
        Map<String, Long> typeMinCntMap = Maps.newHashMap();
        // 所有知识点，正常补全一次，哪个知识点差多少先记录下
        for(TestPaperVO.TypeDef typeDef : paperDef.getTypes()) {
            Long avgKnowTypeCount = typeDef.getCount().longValue() / paperDef.getKnowIds().size();
            if(avgKnowTypeCount.equals(0L)) {
                avgKnowTypeCount = 1L;
            }
            assignCount.set(0);
            for(Long knowId : paperDef.getKnowIds()) {
                Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, avgKnowTypeCount, paperDef.getFillExclude(), assignCount);
                if (tmpMinKnowTypeCount > 0) {
                    // 记录最小数
                    Long minKnowTypeCount = typeMinCntMap.get(typeDef.getType());
                    typeMinCntMap.put(typeDef.getType(), null == minKnowTypeCount ? tmpMinKnowTypeCount : Math.min(minKnowTypeCount, tmpMinKnowTypeCount));
                    // 记录有效知识点
                    Set<Long> knownIdSet = typeKnowIdMap.get(typeDef.getType());
                    if(null == knownIdSet) {
                        typeKnowIdMap.put(typeDef.getType(), Sets.newHashSet(knowId));
                    } else {
                        knownIdSet.add(knowId);
                    }
                } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型
                    // 记录知识点差额
                    Long lackCount = knownAdjMap.get(knowId);
                    knownAdjMap.put(knowId, (null != lackCount ? lackCount : 0L) - tmpMinKnowTypeCount);
                }
            }
            typeFillCntMap.put(typeDef.getType(), assignCount.get());
        }
        knownAdjMap.clear();
        // 优先补充差的知识点
        for(TestPaperVO.TypeDef typeDef : paperDef.getTypes()) {
            assignCount.set(typeFillCntMap.get(typeDef.getType()));
            Long lackTotal = typeDef.getCount().longValue();
            Long minCount = typeMinCntMap.get(typeDef.getType());
            Set<Long> knownIdSet = typeKnowIdMap.get(typeDef.getType());
            do {
                if(lackTotal <= assignCount.get()) {
                    break;
                }
                Long needCount = lackTotal - assignCount.get();
                Long avgKnowTypeCount = knownIdSet.size() > 0 ? Math.min(minCount, needCount / knownIdSet.size()) : 1;
                if (avgKnowTypeCount <= 0L) {
                    avgKnowTypeCount = 1L;
                }
                minCount = Long.MAX_VALUE;
                Set<Long> validIdSet = Sets.newHashSet(knownIdSet);
                knownIdSet.clear();
                for(Long knowId : paperDef.getKnowIds()) {
                    if(knownIdSet.size() > 0 && !validIdSet.contains(knowId)) {
                        continue;
                    }
                    // Long lastLack = null; knownAdjMap.remove(knowId); null != lastLack ? lastLack + avgKnowTypeCount :
                    Long knowTypeCount = avgKnowTypeCount;
                    if (knowTypeCount > 0) {
//                        Long oldCnt = assignCount.get();
                        Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount);
//                        Long fillCnt = oldCnt - assignCount.get();
//                        if(fillCnt < 0) {
//                            Long oldFill = knownAdjMap.get(knowId);
//                            knownAdjMap.put(knowId, null == oldFill ? fillCnt : oldFill - fillCnt);
//                        }
                        if (tmpMinKnowTypeCount > 0) {
                            minCount = Math.min(minCount, tmpMinKnowTypeCount);
                            knownIdSet.add(knowId);
                        } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型
                            // knownAdjMap.put(knowId, -tmpMinKnowTypeCount);
                        }
                    } else if(knowTypeCount < 0) {
                        // knownAdjMap.put(knowId, knowTypeCount);
                    }
                    if(lackTotal <= assignCount.get()) {
                        break;
                    }
                }
            } while(true);
        }
    }

    private void assignTypeFirstWithCount2(TestPaperVO.PaperDef paperDef, Map<String, KnowTypeAssign> knowTypeAssignMap) {
        Map<String, Set<Long>> typeKnownIdsMap = knowTypeAssignMap.values().stream().collect(Collectors.groupingBy(KnowTypeAssign::getType, Collectors.mapping(KnowTypeAssign::getKnowId, Collectors.toSet())));
        Map<Long, Long> knownAdjMap = Maps.newHashMap();
        AtomicLong assignCount = new AtomicLong(0);
        Integer needCount = paperDef.getTypes().size();
        Set<String> doneSet = Sets.newHashSet();
        do {
            for(TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { // 每个类型，让所有知识先平均补充，补充的由后面填充
                Long minKnowTypeCount = Long.MAX_VALUE;
                // 首先按平均数量填充
                assignCount.set(0L);
                Long lackTotal = typeDef.getCount().longValue();
                Set<Long> knownIdSet;
                if(knownAdjMap.size() > 0) { // 先补充之前的同类型
                    knownIdSet = Sets.newHashSet();
                    for(Long knowId : paperDef.getKnowIds()) {
                        Long lastLack = knownAdjMap.remove(knowId);
                        Long knowTypeCount = null != lastLack ? lastLack : 0;
                        if(knowTypeCount >= 0) {
                            Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount);
                            if (tmpMinKnowTypeCount > 0) {
                                minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount);
                                knownIdSet.add(knowId);
                            } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型
                                knownAdjMap.put(knowId, -tmpMinKnowTypeCount);
                            }
                        } else if(knowTypeCount < 0) {
                            knownAdjMap.put(knowId, knowTypeCount);
                        }
                    }
                } else {
                    knownIdSet = typeKnownIdsMap.get(typeDef.getType());
                }
                if(lackTotal <= assignCount.get()) {
                    doneSet.add(typeDef.getType());
                    continue;
                }
                Long avgKnowTypeCount = (lackTotal - assignCount.get()) / paperDef.getKnowIds().size();
                if (avgKnowTypeCount <= 0L) {
                    avgKnowTypeCount = 1L;
                }
                for(Long knowId : paperDef.getKnowIds()) {
                    Long lastLack = knownAdjMap.remove(knowId);
                    Long knowTypeCount = null != lastLack ? lastLack + avgKnowTypeCount : avgKnowTypeCount;
                    if(knowTypeCount > 0) {
                        Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount);
                        if (tmpMinKnowTypeCount > 0) {
                            minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount);
                            knownIdSet.add(knowId);
                        } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型
                            knownAdjMap.put(knowId, -tmpMinKnowTypeCount);
                        }
                    } else if(knowTypeCount < 0) {
                        knownAdjMap.put(knowId, knowTypeCount);
                    }
                }
                // 然后进行多级补充，多退少补
                do {
                    if(lackTotal <= assignCount.get()) {
                        doneSet.add(typeDef.getType());
                        break;
                    }
                    avgKnowTypeCount = Math.min(minKnowTypeCount, (lackTotal - assignCount.get()) / knownIdSet.size());
                    if (avgKnowTypeCount <= 0L) {
                        avgKnowTypeCount = 1L;
                    }
                    minKnowTypeCount = Long.MAX_VALUE;
                    Set<Long> validIdSet = Sets.newHashSet(knownIdSet);
                    knownIdSet.clear();
                    for(Long knowId : paperDef.getKnowIds()) {
                        if(!validIdSet.contains(knowId)) {
                            continue;
                        }
                        Long lastLack = knownAdjMap.remove(knowId);
                        Long knowTypeCount = null != lastLack ? lastLack + avgKnowTypeCount : avgKnowTypeCount;
                        if (knowTypeCount > 0) {
                            Long oldCnt = assignCount.get();
                            Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount);
                            Long fillCnt = oldCnt - assignCount.get();
                            if(fillCnt < 0) {
                                Long oldFill = knownAdjMap.get(knowId);
                                knownAdjMap.put(knowId, null == oldFill ? fillCnt : oldFill - fillCnt);
                            }
                            if (tmpMinKnowTypeCount > 0) {
                                minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount);
                                knownIdSet.add(knowId);
                            } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型
                                knownAdjMap.put(knowId, -tmpMinKnowTypeCount);
                            }
                        } else if(knowTypeCount < 0) {
                            knownAdjMap.put(knowId, knowTypeCount);
                        }
                    }
                } while(true);
            }
        } while(doneSet.size() < needCount);
    }

    private void assignTypeFirst(Boolean fillExclude, TestPaperVO.KnowledgeTypeDef2 knowledgeTypeDef, Map<String, KnowTypeAssign> knowTypeAssignMap) {
        Map<String, Set<Long>> typeKnownIdsMap = knowTypeAssignMap.values().stream().collect(Collectors.groupingBy(KnowTypeAssign::getType, Collectors.mapping(KnowTypeAssign::getKnowId, Collectors.toSet())));
        AtomicLong assignCount = new AtomicLong(0);
        for(TestPaperVO.TypeDef2 typeDef : knowledgeTypeDef.getTypes()) { // 每个类型，让所有知识先平均补充，补充的由后面填充
            Long minKnowTypeCount = Long.MAX_VALUE;
            Long lackTotal = typeDef.getCount().longValue();
            String typeTitle = typeDef.getType().getTitle();
            Set<Long> knownIdSet = typeKnownIdsMap.get(typeTitle);
            if(CollectionUtils.isEmpty(knownIdSet)) {
                log.error("Invalid knowledge type: " + typeTitle);
                return;
            }
            assignCount.set(0L);
            do {
                Long avgKnowTypeCount = (lackTotal - assignCount.get()) / knownIdSet.size();
                if (avgKnowTypeCount <= 0L) {
                    avgKnowTypeCount = 1L;
                }
                knownIdSet.clear();
                for(Long knowId : knowledgeTypeDef.getKnowledges()) {
                    if(avgKnowTypeCount > 0) {
                        Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeTitle, knowTypeAssignMap, avgKnowTypeCount, fillExclude, assignCount);
                        if (tmpMinKnowTypeCount > 0) {
                            minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount);
                            knownIdSet.add(knowId);
                        }
                        if(assignCount.get() >= lackTotal) {
                            break;
                        }
                    }
                }
            } while(lackTotal != assignCount.get() && knownIdSet.size() > 0);
            if(lackTotal < assignCount.get()) {
                throw new RuntimeException("题数不足: " + typeTitle + "差" + (lackTotal - assignCount.get()));
            }
        }
    }

    private void assignKnowFirst(TestPaperVO.PaperDef paperDef, Map<String, KnowTypeAssign> knowTypeAssignMap) {
        // 循环补充未做+已做，如果知识点总数不够时才填充其他知识点的
        Long lackTotal = paperDef.getTotal();
        AtomicLong assignCount = new AtomicLong(0);
        Map<Long, Integer> knowTypesMap = Maps.newHashMap();
        int typeCount = paperDef.getTypes().size();
        Set<Long> knowSet = knowTypeAssignMap.values().stream().map(KnowTypeAssign::getKnowId).collect(Collectors.toSet());
        for (Long knowId : paperDef.getKnowIds()) {
            knowTypesMap.put(knowId, knowSet.contains(knowId) ? typeCount : 0);
        }
        Long minKnowTypeCount = Long.MAX_VALUE;
        do {
            Integer knowCount = knowSet.size();
            knowSet.clear();
            for (Long knowId : paperDef.getKnowIds()) {
                typeCount = knowTypesMap.get(knowId);
                if (0 == typeCount) {
                    continue;
                }
                Long avgKnowTypeCount = Math.min(minKnowTypeCount, lackTotal / knowCount / typeCount);
                if(avgKnowTypeCount == 0L && lackTotal > 0L) {
                    avgKnowTypeCount = 1L;
                }
                typeCount = 0;
                for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) {
                    Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, avgKnowTypeCount, paperDef.getFillExclude(), assignCount);
                    if (tmpMinKnowTypeCount > 0L) {
                        minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount);
                        knowSet.add(knowId);
                        typeCount++;
                    }
                }
                knowTypesMap.put(knowId, typeCount);
            }
            lackTotal = paperDef.getTotal() - assignCount.get();
            if (lackTotal <= 0L || knowSet.isEmpty()) {
                break;
            }
        } while (true);
    }

    /**
     * 根据计划的分配数生成题关系
     * @param studentId
     * @param paperDef
     * @param knowTypeAssignMap
     * @return
     */
    public List<LearnPaperQuestion> getQuestions(Long studentId, TestPaperVO.PaperDef paperDef, Map<String, KnowTypeAssign> knowTypeAssignMap) {
        // 知识点已经分配，准备题型分配
        LearnQuestions qCond = new LearnQuestions();
        Random random = ThreadLocalRandom.current();
        List<LearnPaperQuestion> pqList = Lists.newArrayList();
        Set<String> existQuestionSet = Sets.newHashSet();
        int total = paperDef.getTotal().intValue();
        for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) {
            for (Long knowId : paperDef.getKnowIds()) {
                String key = knowId + "_" + typeDef.getType();
                KnowTypeAssign ktc = knowTypeAssignMap.get(key);
                if(null == ktc) {
                    continue;
                }
                qCond.setKnowledgeId(ktc.getKnowId());
                qCond.setQtpye(ktc.getType());
                qCond.setIsSubType("0");
                QuestionType qt = QuestionType.of(ktc.getType());
                if(ktc.exclAssign > 0){
                    qCond.setId(studentId);
                    qCond.setNumber(ktc.exclAssign > 500 ? (long) random.nextInt(ktc.exclAssign.intValue() - 500) :  0L);
                    List<LearnQuestions> questions = questionsService.selectQuestionsForPaper(qCond);
                    ktc.exclAssign = addRandomList(knowId, qt, questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore().doubleValue(), existQuestionSet, 1, pqList);
                    if(pqList.size() == total) {
                        break;
                    }
                }
                if(ktc.assign > 0L) {
                    qCond.setId(null);
                    qCond.setNumber(ktc.assign > 500 ? (long) random.nextInt(ktc.assign.intValue() - 500) :  0L);
                    List<LearnQuestions> questions = questionsService.selectQuestionsForPaper(qCond);
                    ktc.assign = addRandomList(knowId, qt, questions, random, paperDef.getTotal(), ktc.assign, typeDef.getScore().doubleValue(), existQuestionSet, 1, pqList);
                    if(pqList.size() == total) {
                        break;
                    }
                }
            }
            if(pqList.size() == total) {
                break;
            }
        }
        if(CollectionUtils.isEmpty(pqList)) {
            throw new RuntimeException("题数不足2");
        }
        reSort(pqList);
        return pqList;
    }

    public List<LearnPaperQuestion> getQuestions2(String filterType, Long studentId, Integer total, Integer seqId, Collection<Long> knownIds, String isSubType, List<TestPaperVO.TypeDef2> types, Map<String, KnowTypeAssign> knowTypeAssignMap, Set<String> existQuestionSet) {
        // 知识点已经分配，准备题型分配
        LearnQuestions qCond = new LearnQuestions();
        Random random = ThreadLocalRandom.current();
        List<LearnPaperQuestion> pqList = Lists.newArrayList();
        for (TestPaperVO.TypeDef2 typeDef : types) {
            String typeTitle = typeDef.getType().getTitle();
            for (Long knowId : knownIds) {
                String key = knowId + "_" + typeTitle;
                KnowTypeAssign ktc = knowTypeAssignMap.get(key);
                if(null == ktc) {
                    continue;
                }
                qCond.setKnowledgeId(ktc.getKnowId());
                qCond.setQtpye(typeTitle);
                qCond.setIsSubType(isSubType);
                if ("1".equals(isSubType)) {
                    qCond.setSubCnt(typeDef.getCount());
                }
                if(ktc.exclAssign > 0){
                    if(null != studentId) {
                        qCond.setId(studentId);
                    } else {
                        qCond.setArea(filterType);
                    }
                    qCond.setNumber(ktc.exclAssign > 500 ? (long) random.nextInt(ktc.exclAssign.intValue() - 500) :  0L);
                    List<LearnQuestions> questions = questionsService.selectQuestionsForPaper(qCond);
                    ktc.exclAssign = addRandomList(knowId, typeDef.getType(), questions, random, total.longValue(), ktc.exclAssign, typeDef.getScore(), existQuestionSet, seqId, pqList);
                    if(pqList.size() == total) {
                        break;
                    }
                }
                if(ktc.assign > 0L) {
                    qCond.setId(null);
                    qCond.setArea(null);
                    qCond.setNumber(ktc.assign > 1000 ? (long) random.nextInt(ktc.assign.intValue() - 1000) :  0L);
                    List<LearnQuestions> questions = questionsService.selectQuestionsForPaper(qCond);
                    ktc.assign = addRandomList(knowId, typeDef.getType(), questions, random, total.longValue(), ktc.assign, typeDef.getScore(), existQuestionSet, seqId, pqList);
                    if(pqList.size() == total) {
                        break;
                    }
                }
            }
            if(pqList.size() == total) {
                break;
            }
        }
        if(pqList.size() < total) {
            throw new RuntimeException("题数不足3 " + types.stream().map( t -> t.getType().getTitle()).collect(Collectors.joining(",")) + ":" + StringUtils.join(knownIds, ","));
        }
        return pqList;
    }


    /**
     * 初始化当前用户卷情况
     * @param studentId
     * @param paperDef
     * @return
     */
    private Map<String, KnowTypeAssign> buildKnowTypeAssignMap(String filterType, Long studentId, TestPaperVO.PaperDef paperDef) {
        return buildKnowTypeAssignMap(filterType, studentId, "0", null, paperDef.getTypes().stream().map(TestPaperVO.TypeDef::getType).collect(Collectors.toList()), paperDef.getKnowIds(), paperDef.getFillExclude(), 0);
    }

    /**
     * 初始化当前用户知识点的情况
     * @param studentId
     * @param types
     * @param knownIds
     * @param fillExclude
     * @return
     */
    private Map<String, KnowTypeAssign> buildKnowTypeAssignMap(String filterType, Long studentId, String isSubType, Integer subCnt, List<String> types, Collection<Long> knownIds, Boolean fillExclude, Integer questionType) {
        Map<String, KnowTypeAssign> knowTypeAssignMap = Maps.newHashMap();
        Map cond = Maps.newHashMap();
        cond.put("typeId", questionType);
        cond.put("studentId", studentId);
        cond.put("knowIds", knownIds);
        cond.put("types", types);
        cond.put("isSubType", isSubType);
        if("1".equals(isSubType)) {
            cond.put("subCnt", subCnt);
        }
        if(null == studentId) {
            cond.put("filterType", filterType);
        }
        setValue(knowTypeAssignMap, cond, 1); // 填充排除后总量
        if (fillExclude) {
            if(null != studentId) {
                cond.remove("studentId");
            } else {
                cond.remove("filterType");
            }
            setValue(knowTypeAssignMap, cond, 2); // 按需填充已做总量
        }
        return knowTypeAssignMap;
    }

    /**
     * 随机从 knowId 题池中提取需要个数的题
     * @param knowId 知识点
     * @param type 类型
     * @param questions 题池
     * @param random 随机数
     * @param totalCount 总题数
     * @param count  本池分配数
     * @param score 题分
     * @param existQuestionSet 不能使用的题
     * @param pqList 卷题关系  diff(type),paperId(parentId),seq,questionId(id)
     */
    private Long addRandomList(Long knowId, QuestionType type, List<LearnQuestions> questions, Random random, Long totalCount, Long count, Double score, Set<String> existQuestionSet, Integer baseSeq, List<LearnPaperQuestion> pqList) {
        while(count > 0L && !questions.isEmpty()) {
            LearnQuestions q = questions.size() > 1 ? questions.remove(random.nextInt(questions.size() - 1)) : questions.remove(0);
            if(existQuestionSet.add(q.getId().toString()) && existQuestionSet.add(q.getTitle())) {
                if("1".equals(q.getIsSubType())) {
                    LearnQuestions subCond = new LearnQuestions();
                    subCond.setKnowId(q.getId());
                    for(LearnQuestions sq : questionsService.selectLearnQuestionsList(subCond)) {
                        LearnPaperQuestion pq = new LearnPaperQuestion();
                        pq.setSeq(baseSeq + pqList.size());
                        pq.setKnowledgeId(knowId);
                        pq.setScore(score);
                        pq.setQuestionId(sq.getId());
                        pq.setType(type.getTitle());
                        pq.setDiff(type.getVal());
                        pq.setPaperId(q.getId());
                        pqList.add(pq);
                        count--;
                    }
                } else {
                    LearnPaperQuestion pq = new LearnPaperQuestion();
                    pq.setSeq(baseSeq + pqList.size());
                    pq.setKnowledgeId(knowId);
                    pq.setScore(score);
                    pq.setQuestionId(q.getId());
                    pq.setType(type.getTitle());
                    pq.setDiff(type.getVal());
                    pq.setPaperId(0L);
                    pqList.add(pq);
                    count--;
                }
                if(pqList.size() >= totalCount) {
                    break;
                }
            }
        }
        return count;
    }

    /**
     * 给指定类型分配
     * @param knowId
     * @param qtype
     * @param knowTypeAssignMap
     * @param knowTypeCount
     * @param fillExclude
     * @param assignCount
     */
    private Long assignKnownCount(Long knowId, String qtype, Map<String, KnowTypeAssign> knowTypeAssignMap, Long knowTypeCount, Boolean fillExclude,
                                  AtomicLong assignCount) {
        String key = knowId + "_" + qtype;
        KnowTypeAssign knowTypeAssign = knowTypeAssignMap.get(key);
        long lackCount;
        if (null != knowTypeAssign && knowTypeAssign.exclCount > 0) {
            lackCount = knowTypeCount - knowTypeAssign.exclCount;
            if (lackCount <= 0) { // 足量
                assignCount.getAndAdd(knowTypeCount);
                knowTypeAssign.exclCount -= knowTypeCount;
                knowTypeAssign.exclAssign += knowTypeCount;
            } else { // 不足时且还有时全转
                assignCount.getAndAdd(knowTypeAssign.exclCount);
                knowTypeAssign.exclAssign += knowTypeAssign.exclCount;
                knowTypeAssign.exclCount = 0L;
            }
        } else {
            lackCount = knowTypeCount;
        }
        long lack = 0;
        if (lackCount > 0) { // 差额优先补充已做过的
            if(fillExclude && null != knowTypeAssign && knowTypeAssign.total > 0) {
                lack = lackCount - knowTypeAssign.total;
                if (lack <= 0) { // 足量
                    assignCount.getAndAdd(lackCount);
                    knowTypeAssign.total -= lackCount;
                    knowTypeAssign.assign += lackCount;
                } else { // 不足时全转
                    assignCount.getAndAdd(knowTypeAssign.total);
                    knowTypeAssign.assign += knowTypeAssign.total;
                    knowTypeAssign.total = 0L;
                }
            } else {
                lack = lackCount;
            }
        }
        if(lackCount < 0) {
            return -lackCount;
        } else if(lack < 0) {
            return -lack;
        }
        return -lack;
    }

    /**
     * 合并 未用总量和已有总量, 分配数置0
     * @param knowTypeAssignMap key=knowId +"_" + qtype
     * @param cond
     * @param index 1 free 2 used
     */
    private void setValue(Map<String, KnowTypeAssign> knowTypeAssignMap, Map cond, Integer index) {
        for (LearnQuestions q : questionsService.statByKnowledgeType(cond)) {
            String key = q.getKnowledgeId() + "_" + q.getQtpye();
            KnowTypeAssign knowTypeAssign = knowTypeAssignMap.get(key);
            if (null == knowTypeAssign) {
                knowTypeAssign = new KnowTypeAssign();
                knowTypeAssign.setKnowId(q.getKnowledgeId());
                knowTypeAssign.setType(q.getQtpye());
                knowTypeAssign.total = 0L;
                knowTypeAssign.exclAssign = 0L;
                knowTypeAssign.assign = 0L;
                knowTypeAssign.exclCount = 0L;
                knowTypeAssignMap.put(key, knowTypeAssign);
            }
            if (1 == index) {
                knowTypeAssign.exclCount = q.getNumber();
            } else {
                knowTypeAssign.total = q.getNumber() - knowTypeAssign.exclCount;
            }
        }
    }

    @Data
    public static class KnowTypeAssign {
        Long knowId; // 知识点
        String type; // 题类型
        Long exclAssign; // 未用分配数
        Long assign; // 已用分配数
        Long exclCount; // 未用总量
        Long total; // 已用总量
    }
}
