package com.jakubu9333.grammarManager.service;

import com.jakubu9333.grammarManager.dao.FileContentRepository;
import com.jakubu9333.grammarManager.dao.FileMetaDataRepo;
import com.jakubu9333.grammarManager.dao.GrammarRepository;
import com.jakubu9333.grammarManager.dao.RuleRepository;
import com.jakubu9333.grammarManager.model.FileMetadata;
import com.jakubu9333.grammarManager.model.Grammar;
import com.jakubu9333.grammarManager.model.RuleModel;
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.util.HashSet;
import java.util.List;

import java.util.Random;
import java.util.Set;

import static java.lang.Math.abs;


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

    private final GrammarRepository grammarRepository;
    private final RuleRepository ruleRepository;

    private final FileMetaDataRepo fileMetaDataRepo;

    private final FileContentRepository fileRepository;

    private final RuleService ruleService;

    @Autowired
    public GrammarService(GrammarRepository grammarRepository,
                          RuleRepository ruleRepository, FileMetaDataRepo fileMetaDataRepo, FileContentRepository fileRepository, RuleService ruleService) {
        this.grammarRepository = grammarRepository;
        this.ruleRepository = ruleRepository;
        this.fileMetaDataRepo = fileMetaDataRepo;
        this.fileRepository = fileRepository;
        this.ruleService = ruleService;
    }

    public List<Grammar> getGrammars() {
        return grammarRepository.findAll();
    }


    @Transactional
    public void deleteAll() {
        ruleService.deleteRules();
        grammarRepository.findAll().forEach(grammar -> grammar.setFiles(null));
        fileMetaDataRepo.findAll().forEach(this::deleteSavedContents);
        fileMetaDataRepo.deleteAll();
        grammarRepository.deleteAll();
    }
    private void deleteSavedContents(FileMetadata fileMetadata){
        fileRepository.delete(fileMetadata.getFileContent());
    }


    @Transactional
    public void deleteById(Long id) {
        Grammar grammar = getGrammarById(id);
        ruleRepository.deleteAll(grammar.getRules());
        Set<FileMetadata> files = grammar.getFiles();
        files.forEach(this::deleteSavedContents);
        fileMetaDataRepo.deleteAll(grammar.getFiles());
        grammarRepository.deleteById(id);
    }

    public Grammar preProcessGrammar(Grammar grammar) {
        grammar.setId(null);
        if (grammar.getUri() == null) {
            grammar.setUri("null");
        }

        RuleModel rootRule = grammar.getRootRule();
        Set<RuleModel> rules = grammar.getRules();
        if (rootRule != null) {
            rootRule.setRoot(true);
            //ads root rule to rules  if it was forgotten in request
            rules.add(rootRule);
        }
        rules = ruleService.commonLeftMerge(rules);
        boolean root = false;
        for (RuleModel rule : rules) {
            if (root && rule.getRoot()) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "multiple rootrules in one grammar");
            }
            root |= rule.getRoot();
        }
        for (RuleModel rule : rules) {
            ruleService.rulePreProcess(rule);
        }
        if (root) {
            grammar.setRootRule(rules.stream().filter(RuleModel::getRoot).toList().get(0));
        }

        //new set to reset modified hashcodes
        grammar.setRules(new HashSet<>(rules));
        return grammar;
    }

    @Transactional
    public Grammar processAddGrammar(Grammar grammar) {
        return addGrammar(preProcessGrammar(grammar));
    }

    @Transactional
    public Grammar addGrammar(Grammar grammar) {
        RuleModel rootRule = grammar.getRootRule();
        Set<RuleModel> rules = grammar.getRules();

        grammar.setRules(new HashSet<>());
        grammar.setRootRule(null);
        grammar = grammarRepository.save(grammar);
        Boolean exists_uri = grammarRepository.existsByUriAndIdNot(grammar.getUri(), grammar.getId());

        //uri modification

        if (exists_uri) {
            grammar.setUri(generateUniqueUri(grammar.getUri(), grammar.getId()));
        }

        if (rootRule != null) {
            rules.remove(rootRule);
            ruleService.addRule(rootRule, grammar);
        }
        for (RuleModel rule : rules) {
            ruleService.addRule(rule, grammar);
        }


        return grammar;
    }

    public Grammar getGrammarById(Long id) {
        return grammarRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "grammar id: " + id + " not found"));
    }

    @Transactional
    public Grammar duplicateGrammar(Long id) {
        Grammar grammar = getGrammarById(id);
        Grammar newGrammar = new Grammar();
        newGrammar.setUri(grammar.getUri());
        RuleModel oldRule = grammar.getRootRule();
        if (oldRule != null) {
            newGrammar.setRootRule(new RuleModel(oldRule));
        }
        Set<RuleModel> rules = grammar.getRules();
        Set<RuleModel> newRules = new HashSet<>();
        for (RuleModel rule : rules) {
            newRules.add(new RuleModel(rule));
        }
        newGrammar.setRules(newRules);
        newGrammar = addGrammar(newGrammar);

        return newGrammar;
    }

    public List<Grammar> getGrammarByRootRule(String rootSymbol) {
        List<RuleModel> rulesWithSymbol = ruleRepository.findByLeftSymbol(rootSymbol);
        return rulesWithSymbol.stream().filter((RuleModel::getRoot)).map((RuleModel::getGrammar)).toList();

    }

    public List<Grammar> getGrammarsByUriStart(String uri) {
        return grammarRepository.findByUriStartingWith(uri);
    }


    private String generateUniqueUri(String uri, Long id) {
        String result = uri + id;
        Random random = new Random(System.currentTimeMillis());
        while (grammarRepository.existsByUriAndIdNot(result, id)) {
            result = uri + id;
            result += "_" +abs(random.nextInt());
        }
        return result;
    }

    @Transactional
    public Grammar changeUri(Long id, Grammar grammar) {
        Grammar oldGrammar = this.getGrammarById(id);
        if (grammar.getUri() != null && grammar.getUri().trim().length()!=0) {
            oldGrammar.setUri(grammar.getUri());

            if (grammarRepository.existsByUriAndIdNot(grammar.getUri(), id)) {
                oldGrammar.setUri(generateUniqueUri(grammar.getUri(), oldGrammar.getId()));
                return oldGrammar;
            }
        }
        return oldGrammar;
    }
}
