package com.jakubu9333.grammarManager.service;

import com.jakubu9333.grammarManager.dao.GrammarRepository;
import com.jakubu9333.grammarManager.dao.RightSidePartRepo;
import com.jakubu9333.grammarManager.dao.RuleRepository;
import com.jakubu9333.grammarManager.model.Grammar;
import com.jakubu9333.grammarManager.model.RightSidePart;
import com.jakubu9333.grammarManager.model.RuleModel;
import edu.cmu.sphinx.jsgf.rule.JSGFRule;
import org.javatuples.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Jakub Uhlarik
 */
@Service
public class RuleService {

    private final RuleRepository ruleRepository;
    private final GrammarRepository grammarRepository;
    private final RightSidePartRepo rightSidePartRepo;

    @Autowired
    public RuleService(RuleRepository ruleRepository, GrammarRepository grammarRepository, RightSidePartRepo rightSidePartRepo) {
        this.ruleRepository = ruleRepository;
        this.grammarRepository = grammarRepository;
        this.rightSidePartRepo = rightSidePartRepo;

    }

    public List<RuleModel> getAllRules() {
        return ruleRepository.findAll();
    }

    private RuleModel saveRule(RuleModel rule) {
        return ruleRepository.save(rule);
    }

    public String stringEncapsulation(String string) {
        if (string == null) {
            return null;
        }
        StringBuilder stringBuilder = new StringBuilder();
        if (!string.startsWith("<")) {
            stringBuilder.append('<');
        }
        stringBuilder.append(string);
        if (!string.endsWith(">")) {
            stringBuilder.append('>');
        }
        return stringBuilder.toString();

    }
    private Pair<Integer,Boolean> findRightBracketAndCheckInsideSplit(int leftIndex, StringBuilder sb) {
        int brackets = 0;
        int rightIndex = -1;
        boolean split = false;
        for (int i = leftIndex; i < sb.length(); i++) {
            if (brackets==1 &&sb.charAt(i)=='|'){
                split=true;
            }
            if (sb.charAt(i) == '(') {
                brackets++;
            }
            if (sb.charAt(i) == ')') {
                brackets--;
                if (brackets == 0) {
                    rightIndex = i;
                    return new Pair<>(rightIndex,split);
                }
            }

        }

        return new Pair<>(rightIndex,split);
    }

    private int findLeftEnd(int leftIndex, StringBuilder sb) {
        int outsideLeft = 0;

        for (int i = leftIndex; i >= 0; i--) {
            outsideLeft = i;
            if (sb.charAt(i) == '|') {
                return outsideLeft;
            }
        }
        return outsideLeft;
    }

    private int findRightEnd(int rightIndex, StringBuilder sb) {
        int outSideRight = rightIndex;
        int brackets = 0;
        for (int i = rightIndex; i < sb.length(); i++) {
            outSideRight = i;
            if (sb.charAt(i) == '(') {
                brackets++;
            }
            if (sb.charAt(i) == ')' && i != rightIndex) {
                brackets--;
            }
            if (sb.charAt(i) == '|' && brackets == 0) {
                return outSideRight;
            }
        }
        return outSideRight;

    }

    private String normalBrackets(String string) {
        StringBuilder sb = new StringBuilder(string.trim());

        int leftIndex = 0;
        int rightIndex;
        //While there are still opening brackets "(", find the matching closing bracket ")".
        while ((leftIndex = sb.indexOf("(", leftIndex)) != -1) {
            Pair<Integer,Boolean> rightIndexSplit = findRightBracketAndCheckInsideSplit(leftIndex, sb);
             rightIndex=rightIndexSplit.getValue0();
            if (rightIndex == -1) {
                break;
            }
            if (!rightIndexSplit.getValue1()){
                sb.replace(leftIndex,leftIndex+1,"");
                sb.replace(rightIndex-1,rightIndex,"");
                continue;
            }

            //Extract the text inside the brackets and the text outside the brackets on the left and right sides.
            String inside = sb.substring(leftIndex + 1, rightIndex);

            int startOfLeftOutside = findLeftEnd(leftIndex, sb);
            String leftOutside = sb.substring(startOfLeftOutside, leftIndex);

            int endOfRightOutside = findRightEnd(rightIndex, sb);
            String rightOutside = sb.substring(rightIndex + 1, endOfRightOutside + 1);

            if (startOfLeftOutside != 0 && leftIndex != 0) {
                startOfLeftOutside++;
            }

            sb.replace(startOfLeftOutside, endOfRightOutside + 1, "|");
            for (String stringInSplit : inside.split("\\|")) {
                if (stringInSplit.equals("<NULL>")&&(leftOutside.length()>0||rightOutside.length()>0)){
                    sb.append("|").append(leftOutside).append(rightOutside);
                }else{
                    sb.append("|").append(leftOutside).append(stringInSplit).append(rightOutside);
                }

            }

        }
        return sb.toString();
    }
    private int findRightStraightBracket(int leftIndex, StringBuilder sb) {
        int brackets = 0;
        int rightIndex = -1;
        for (int i = leftIndex; i < sb.length(); i++) {
            if (sb.charAt(i) == '[') {
                brackets++;
            }
            if (sb.charAt(i) == ']') {
                brackets--;
                if (brackets == 0) {
                    rightIndex = i;
                    return rightIndex;
                }
            }

        }
        return rightIndex;
    }

    private String straightBrackets(String string) {
        StringBuilder sb = new StringBuilder(string);

        int leftIndex;
        int rightIndex;
        while ((leftIndex = sb.indexOf("[")) != -1 && (rightIndex =findRightStraightBracket(leftIndex,sb)) != -1) {

            sb.replace(leftIndex, leftIndex + 1, "(" );
            sb.replace(rightIndex,rightIndex+1,"|<NULL>)");

        }
        return sb.toString();

    }

    private List<Pair<Integer, Integer>> getIgnoreListCheckRuleRef(String addedRightSide) {
        List<Pair<Integer, Integer>> result = new ArrayList<>();
        Integer start = null;
        Integer end = null;
        Integer startAngleBracket = null;
        Integer endAngleBracket = null;
        for (int i = 0; i < addedRightSide.length(); i++) {
            boolean slashNotBefore = ((i == 0 || addedRightSide.charAt(i - 1) != '\\') || (i > 1 && addedRightSide.charAt(i - 1) == '\\' && addedRightSide.charAt(i - 2) == '\\'));
            if (addedRightSide.charAt(i) == '"' && slashNotBefore) {
                if (start == null) {
                    end = null;
                    start = i;
                } else {
                    result.add(new Pair<>(start, i));
                    start = null;
                    end = i;
                }
            }
            if (addedRightSide.charAt(i) == '<' && slashNotBefore) {
                endAngleBracket = null;
                startAngleBracket = i;
            }
            if (addedRightSide.charAt(i) == '>' && slashNotBefore) {
                if (startAngleBracket == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ending angle bracket without starting bracket");
                }
                endAngleBracket = i;
                result.add(new Pair<>(startAngleBracket, endAngleBracket));
                startAngleBracket = null;

            }
        }
        if (end == null && start != null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "last quote not ended");
        }
        if (endAngleBracket == null && startAngleBracket != null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ruleref not ended");
        }
        return result;
    }


    public Set<RightSidePart> stringToRightPart(String string) {
        String addedRightSide = string == null ? "<NULL>" : string;
        List<Pair<Integer, Integer>> ignoreList = getIgnoreListCheckRuleRef(addedRightSide);
        boolean straightBrackets = (addedRightSide.contains("[")) && (addedRightSide.contains("]"));
        if (straightBrackets) {
            addedRightSide = straightBrackets(addedRightSide);
        }
        boolean normalBrackets = (addedRightSide.contains("(")) && (addedRightSide.contains(")"));
        if (normalBrackets) {
            addedRightSide = normalBrackets(addedRightSide);
        }
        return Arrays.stream(addedRightSide.split("\\||\\\\\\\\\\|", 0)).map(String::trim).filter(p -> p.length() > 0).map(RightSidePart::new).collect(Collectors.toSet());
    }


    public Set<RuleModel> commonLeftMerge(Collection<RuleModel> rules) {
        Map<String, RuleModel> calcMap = new HashMap<>();
        for (RuleModel rule : rules) {
            RuleModel oldValue = calcMap.get(rule.getLeftSymbol());
            if (oldValue == null) {
                calcMap.put(rule.getLeftSymbol(), rule);
            } else {
                Set<RightSidePart> leftSide = new HashSet<>();
                leftSide.addAll(oldValue.getRightSide());
                leftSide.addAll(rule.getRightSide());
                RuleModel mergeRule = new RuleModel(null, rule.getLeftSymbol(), leftSide, rule.getRoot() || oldValue.getRoot());
                calcMap.replace(rule.getLeftSymbol(), mergeRule);
            }
        }
        if (calcMap.size() == rules.size()) {
            return new HashSet<>(rules);
        }
        return new HashSet<>(calcMap.values());
    }

    public void rulePreProcess(RuleModel rule, boolean ignoreLeft) {
        if (rule.getRoot()==null){
            rule.setRoot(false);
        }
        if (ignoreLeft) {
            Set<RightSidePart> newRightSide = new HashSet<>();
            for (RightSidePart rightSidePart : rule.getRightSide()) {
                newRightSide.addAll(stringToRightPart(rightSidePart.getPart()));
            }
            rule.setRightSide(newRightSide);
            return;
        }
        rulePreProcess(rule);

    }

    public void rulePreProcess(RuleModel rule) {
        if (rule.getRoot()==null){
            rule.setRoot(false);
        }
        String leftSymbol = rule.getLeftSymbol();
        if (leftSymbol == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "no left symbol");
        }
        if (leftSymbol.trim().length() == 0) {
            rule.setLeftSymbol("empty");
        }
        rule.setLeftSymbol(stringEncapsulation(rule.getLeftSymbol().trim()));
        Set<RightSidePart> newRightSide = new HashSet<>();

        for (RightSidePart rightSidePart : rule.getRightSide()) {
           // JSGFRule sfs = JSGFParser.ruleForJSGF(rightSidePart.getPart());
            newRightSide.addAll(stringToRightPart(rightSidePart.getPart()));
        }
        new JSGFRule();
        rule.setRightSide(newRightSide);

    }

    private Set<RightSidePart> newRightSide(RuleModel oldRule, RuleModel newRule) {
        Set<RightSidePart> oldRightSide = oldRule.getRightSide() == null ? new HashSet<>() : oldRule.getRightSide();
        Set<RightSidePart> newRightSide = newRule.getRightSide() == null ? new HashSet<>() : newRule.getRightSide();
        newRightSide.removeAll(oldRightSide);
        newRule.setRightSide(null);
        rightSidePartRepo.deleteAll(newRightSide.stream().filter(p -> p.getId() != null).collect(Collectors.toSet()));
        return newRightSide.stream().map(p -> p.getId() != null ? new RightSidePart(p.getPart()) : p).collect(Collectors.toSet());
    }

    @Transactional
    public Pair<RuleModel, Boolean> findMegreOlderRule(RuleModel rule, Grammar grammar) {

        boolean isRoot = rule.getRoot();

        //checks if any in this grammar rule have same left side
        List<RuleModel> oldRules = getByLeftSide(rule.getLeftSymbol(), grammar.getId());
        if (!oldRules.isEmpty() && !Objects.equals(oldRules.get(0).getId(), rule.getId())) {
            RuleModel oldRule = oldRules.get(0);
            Set<RightSidePart> newRightSide = newRightSide(oldRule, rule);
            rightSidePartRepo.saveAll(newRightSide);
            oldRule.getRightSide().addAll(newRightSide);
            if (grammar.getRootRule() == null && isRoot) {
                oldRule.setRoot(true);
                grammar.setRootRule(oldRule);
            }
            return new Pair<>(oldRule, false);
        }

        if (rule.getRightSide().size() == 0) {
            rule.getRightSide().add(new RightSidePart("<NULL>"));
        }
        return new Pair<>(rule, true);
    }

    @Transactional
    public Pair<RuleModel, Boolean> processAddRule(RuleModel rule, Grammar grammar) {
        rulePreProcess(rule);
        return addRule(rule, grammar);
    }

    @Transactional
    public Pair<RuleModel, Boolean> addRule(RuleModel rule, Grammar grammar) {
        Pair<RuleModel, Boolean> result = findMegreOlderRule(rule, grammar);
        rule = result.getValue0();
        //if no new rule will be created
        if (!result.getValue1()) {
            return result;
        }

        if (grammar.getRootRule() != null && rule.getRoot()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, grammar.getId() + " already have root");
        }
        rightSidePartRepo.saveAll(rule.getRightSide());
        rule = saveRule(rule);
        if (rule.getRoot()) {
            grammar.setRootRule(rule);
        }
        grammar.getRules().add(rule);
        rule.setGrammar(grammar);
        grammar.setUpdateTime(LocalDateTime.now());
        return new Pair<>(rule, true);
    }

    @Transactional
    public void deleteRules() {
        grammarRepository.findAll().forEach(((grammar) -> {
            grammar.setRules(null);
            grammar.setRootRule(null);
        }));
        ruleRepository.deleteAll();
        rightSidePartRepo.deleteAll();
    }

    @Transactional
    public void deleteById(Long id) {
        RuleModel rule = ruleRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "rule id: " + id + " not found"));
        Grammar rulesGrammar = rule.getGrammar();
        rulesGrammar.getRules().remove(rule);
        if (rule.equals(rulesGrammar.getRootRule())) {
            rulesGrammar.setRootRule(null);
        }
        ruleRepository.delete(rule);
        rightSidePartRepo.deleteAll(rule.getRightSide());
    }


    public List<RuleModel> getByLeftSide(String leftSymbol) {
        leftSymbol = stringEncapsulation(leftSymbol);
        return ruleRepository.findByLeftSymbol(leftSymbol);
    }

    public List<RuleModel> getByLeftSide(String leftSymbol, Long grammarId) {
        leftSymbol = stringEncapsulation(leftSymbol);
        return ruleRepository.findByLeftSymbolAndGrammar_Id(leftSymbol, grammarId);
    }

    public RuleModel getById(Long id) {
        return ruleRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @Transactional
    public RuleModel updateRule(Long id, RuleModel rule) {
        RuleModel oldRule = this.getById(id);
        if (rule.getRoot() != null && oldRule.getRoot() != rule.getRoot()) {
            oldRule = changeRootStatus(id);
        }
        boolean changes = false;
        boolean preprocess = false;
        String leftSymbol = rule.getLeftSymbol();
        if (leftSymbol != null && leftSymbol.trim().length() != 0) {
            preprocess = true;
            rulePreProcess(rule);
            leftSymbol = rule.getLeftSymbol();
            if (!Objects.equals(leftSymbol, oldRule.getLeftSymbol())) {
                oldRule.setLeftSymbol(leftSymbol);
                changes = true;
            }
        }
        Set<RightSidePart> rightSide = rule.getRightSide();
        if (rightSide != null && rightSide.size() != 0 && !(rightSide.size() == 1 && rightSide.stream().toList().get(0).getPart().trim().length() == 0)) {
            if (!preprocess) {
                rulePreProcess(rule, true);
                rightSide = rule.getRightSide();
            }
            if (!Objects.equals(rightSide, oldRule.getRightSide())) {
                rightSidePartRepo.deleteAll(oldRule.getRightSide());
                rightSidePartRepo.saveAll(rightSide);
                oldRule.setRightSide(rightSide);
                changes = true;
            }
        }
        if (changes) {
            oldRule.getGrammar().setUpdateTime(LocalDateTime.now());
            return this.findMegreOlderRule(oldRule, oldRule.getGrammar()).getValue0();
        }
        return oldRule;
    }

    @Transactional
    public RuleModel changeRootStatus(Long id) {
        RuleModel rule = this.getById(id);
        Grammar grammar = rule.getGrammar();
        if (rule.getRoot()) {
            grammar.setRootRule(null);
            rule.setRoot(false);
            return rule;
        }
        if (grammar.getRootRule() != null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "grammar have root terminal");
        }
        grammar.setRootRule(rule);
        rule.setRoot(true);
        return rule;

    }


    public Pair<Set<RuleModel>, Boolean> addRuleMultiple(Collection<RuleModel> rules, Grammar grammar) {
        for (RuleModel rule : rules) {
            rulePreProcess(rule);
        }
        rules = commonLeftMerge(rules);


        Boolean change = false;
        Set<RuleModel> result = new HashSet<>();
        for (RuleModel rule : rules) {
            rulePreProcess(rule);
            Pair<RuleModel, Boolean> partResult = addRule(rule, grammar);
            change |= partResult.getValue1();
            result.add(partResult.getValue0());
        }
        return new Pair<>(result, change);

    }

    public RuleModel getRuleByRuleIdGrammarID(long ruleId, long grammarId) {
        return ruleRepository.findByIdAndGrammar_Id(ruleId, grammarId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "rule " + ruleId + " not found in" + grammarId));
    }
}
