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.model.FileContent;
import com.jakubu9333.grammarManager.model.FileMetadata;
import com.jakubu9333.grammarManager.model.Grammar;

import com.jakubu9333.grammarManager.model.RightSidePart;
import com.jakubu9333.grammarManager.model.RuleModel;
import edu.cmu.sphinx.jsgf.JSGFGrammarParseException;
import edu.cmu.sphinx.jsgf.JSGFRuleGrammar;
import edu.cmu.sphinx.jsgf.JSGFRuleGrammarFactory;
import edu.cmu.sphinx.jsgf.JSGFRuleGrammarManager;
import edu.cmu.sphinx.jsgf.parser.JSGFParser;
import edu.cmu.sphinx.jsgf.parser.TokenMgrError;
import edu.cmu.sphinx.jsgf.rule.JSGFRule;
import edu.cmu.sphinx.jsgf.rule.JSGFRuleAlternatives;
import org.javatuples.Pair;
import org.jvoicexml.processor.grammar.Rule;
import org.jvoicexml.processor.srgs.GrammarException;
import org.jvoicexml.processor.srgs.xml.SrgsRuleGrammarParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.transaction.Transactional;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.StringReader;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

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

    private final FileMetaDataRepo fileMetaDataRepo;

    private final GrammarService grammarService;
    private final GrammarRepository grammarRepository;
    private final FileContentRepository fileRepository;

    @Autowired
    public FileService(FileMetaDataRepo fileMetaDataRepo, GrammarService grammarService, GrammarRepository grammarRepository, FileContentRepository fileRepository) {
        this.fileMetaDataRepo = fileMetaDataRepo;
        this.grammarService = grammarService;
        this.grammarRepository = grammarRepository;
        this.fileRepository = fileRepository;
    }


    public static class XmlGrammarHandler extends DefaultHandler {

    }

    public List<FileMetadata> getFilesFromGrammar(Long id) {
        return fileMetaDataRepo.findAllByGrammarId(id);
    }

    private void deleteSavedContents(FileMetadata fileMetadata) {
        fileRepository.delete(fileMetadata.getFileContent());
    }

    @Transactional
    public void deleteFilesFromGrammar(Long grammarId) {
        Grammar grammar = grammarService.getGrammarById(grammarId);
        Set<FileMetadata> files = grammar.getFiles();
        grammar.setFiles(new HashSet<>());
        fileMetaDataRepo.deleteAll(files);
        files.forEach(this::deleteSavedContents);

    }

    @Transactional
    public void deleteFileById(Long id) {
        FileMetadata file = fileMetaDataRepo.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "file:" + id + "not found"));
        file.getGrammar().getFiles().remove(file);
        fileMetaDataRepo.delete(file);
        fileRepository.delete(file.getFileContent());
    }

    public List<FileMetadata> getAllFiles() {
        return fileMetaDataRepo.findAll();
    }


    public Pair<FileMetadata, String> getFileHolderAndStringById(Long id) {
        FileMetadata fileMetadata = fileMetaDataRepo.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "file id:" + id + " not found"));
        return new Pair<>(fileMetadata, getStringFromFileHolder(fileMetadata));
    }
    private  RuleModel ruleModelFromRule(Rule rule){
        String x=rule.toStringABNF();
        String y = rule.toStringXML();
        Class<? > fdgd =rule.getRuleComponent().getClass();
        assert JSGFParser.ruleForJSGF(rule.toStringJSGF())!=null;
       /* RightSideAlternatives gd= new RightSideAlternatives("dfsf", Type.Alternatives);*/
        return  new RuleModel(rule.getRuleName(),new HashSet<>(List.of(new RightSidePart(rule.getRuleComponent().toStringJSGF())/*,gd*/)));
    }
    public Set<RuleModel> getRules(List<Rule> rules){
        return rules.stream().map(this::ruleModelFromRule).collect(Collectors.toSet());
    }


    private Grammar parseFile(String file, boolean xml) {
        Grammar grammar;


        if (xml) {
            try {
                SrgsRuleGrammarParser xmlParser = new SrgsRuleGrammarParser();
                SAXParserFactory factory = SAXParserFactory.newInstance();
                SAXParser saxParser = factory.newSAXParser();

                XmlGrammarHandler handler = new XmlGrammarHandler();
                InputSource is = new InputSource(new StringReader(file));
                saxParser.parse(is, handler);

                List<Rule> ruleList =xmlParser.load(new StringReader(file));
                System.out.println(ruleList);
                Map<String,Object> attributes =xmlParser.getAttributes();
                RuleModel rootRule= null;
                Object rootName= attributes.get("root") ;
                if (rootName!=null){
                   rootRule= new RuleModel(null,rootName.toString(),null,true);
                }
                getRules(ruleList);

                grammar = new Grammar(attributes.get("xml:base").toString(),  getRules(ruleList),rootRule) ;

            } catch (ParserConfigurationException | IOException | SAXException | GrammarException | URISyntaxException e) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "xml parse error\n" + e.getMessage(), e);
            }

        } else {
            try {
                grammar = jsgfParse(file);
            } catch (JSGFGrammarParseException | TokenMgrError e) {
                if (e instanceof JSGFGrammarParseException) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "jsgf parse error\n" + ((JSGFGrammarParseException) e).details);
                }
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "jsgf parse error\n" + e.getMessage(), e);
            }

        }
        return grammarService.preProcessGrammar(grammar);
    }

    private FileContent saveFile(String content) {
        FileContent file = new FileContent(content);
        return fileRepository.save(file);
    }

    private boolean getFileExtension(String file) {
        Pattern xml = Pattern.compile("^\\W*<\\?xml.*\\?>");
        Matcher mXml = xml.matcher(file);
        if (mXml.find()) {
            return true;
        }
        Pattern jsgf = Pattern.compile("^\\W*#JSGF.*;");
        Matcher mJsgf = jsgf.matcher(file);
        if (mJsgf.find()) {
            return false;
        }
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "format cant be found from file");
    }

    @Transactional
    public void saveFileToGrammar(Grammar grammar, boolean xml, String file) {
        FileMetadata fileHolder = new FileMetadata(xml, false, null);
        fileMetaDataRepo.save(fileHolder);
        fileHolder.setSize(file.length());
        fileHolder.setFileContent(saveFile(file));
        grammar.getFiles().add(fileHolder);
        fileHolder.setGrammar(grammar);
    }

    @Transactional
    public Pair<Grammar, Boolean> addGrammarFromFile(String file, Boolean xml, Long id, boolean generateNewOnConflict, boolean saveFile) {
        if (xml == null) {
            xml = getFileExtension(file);
        }
        Grammar grammar = parseFile(file, xml);
        grammar = grammarService.preProcessGrammar(grammar);
        //if selected creating new grammar or adding to non exist grammar
        if (id == null || !grammarRepository.existsById(id)) {
            if (saveFile) {
                saveFileToGrammar(grammar, xml, file);
            }
            return new Pair<>(grammarService.addGrammar(grammar), true);
        }
        Grammar oldGrammar = grammarService.getGrammarById(id);
        //if grammar generated from file equals existing grammar added to existing grammar
        if (oldGrammar.equals(grammar)) {
            if (saveFile) {
                saveFileToGrammar(oldGrammar, xml, file);
            }
            return new Pair<>(oldGrammar, false);
        }
        if (generateNewOnConflict) {
            if (saveFile) {
                saveFileToGrammar(grammar, xml, file);
            }
            return new Pair<>(grammarService.addGrammar(grammar), true);
        }

        return null;
    }


    private Grammar jsgfParse(String file) throws JSGFGrammarParseException {
        JSGFRuleGrammar grammarRaw = JSGFParser.newGrammarFromJSGF(new StringReader(file), new JSGFRuleGrammarFactory(new JSGFRuleGrammarManager()));
        Set<RuleModel> rules = new HashSet<>();
        for (String ruleName : grammarRaw.getRuleNames()) {
            JSGFRule rule = grammarRaw.getRule(ruleName);
            if (rule instanceof JSGFRuleAlternatives) {
                List<JSGFRule> alterRules = ((JSGFRuleAlternatives) rule).getRules();
                rules.add(new RuleModel(null, ruleName, alterRules.stream().map(ruleInside -> (new RightSidePart(ruleInside.toString()))).collect(Collectors.toSet()), false));
            } else {
                rules.add(new RuleModel(null, ruleName, new HashSet<>(List.of(new RightSidePart(rule.toString()))), false));
            }

        }

        return new Grammar(grammarRaw.getName(), rules, null);
    }

    private void printRightSide(StringBuilder sb, String rightSide) {
        if (rightSide.contains("<") && rightSide.contains(">")) {

            int leftindex = 0;
            int rightindex = 0;

            int begin = 0;
            while (leftindex >= 0 && rightindex >= 0) {
                leftindex = rightSide.indexOf('<', rightindex);
                rightindex = rightSide.indexOf('>', leftindex);
                //between two rules or begin
                if (leftindex - begin > 0) {

                    sb.append(rightSide, begin, leftindex);
                }
                //after rules
                if (leftindex == -1 && rightSide.length() > begin) {
                    sb.append(rightSide.substring(begin));
                }
                if (leftindex >= 0 && rightindex >= 0) {
                    String ruleInside = rightSide.substring(leftindex + 1, rightindex);
                    if (ruleInside.equals("NULL")) {
                        sb.append("<ruleref special=\"NULL\"/>");
                    } else if (ruleInside.equals("VOID")) {
                        sb.append("<ruleref special=\"VOID\"/>");
                    } else {
                        sb.append("<ruleref uri=\"#").append(ruleInside).append("\"/>");
                    }

                }
                begin = rightindex + 1;

            }
            return;
        }
        if (!rightSide.contains("<") && !rightSide.contains(">")) {
            sb.append(rightSide).append("\n");
            return;
        }
        throw new ResponseStatusException(HttpStatus.EXPECTATION_FAILED, "bad rule format");
    }

    private void writeRule(StringBuilder sb, RuleModel rule, boolean xml) {
        if (!xml) {
            if (rule.getRoot()) {
                sb.append("//root;\n");
            }
            int i = 0;
            StringBuilder rightSide = new StringBuilder();
            Set<RightSidePart> setRightSide = rule.getRightSide();
            for (RightSidePart part : setRightSide) {
                rightSide.append(part.getPart());
                if (i + 1 < setRightSide.size()) {
                    rightSide.append(" | ");
                }
                i++;
            }
            sb.append(rule.getLeftSymbol()).append(" = ").append(rightSide).append(";\n");
            return;
        }
        sb.append("<rule id=\"");
        sb.append(nameFromLeftSymbol(rule)).append("\">\n");
        Set<RightSidePart> rightSide = rule.getRightSide();
        if (rightSide.size() > 1) {
            sb.append("<one-of>\n");
            for (RightSidePart part : rightSide) {
                sb.append("<item>\n");
                printRightSide(sb, part.getPart());
                sb.append("</item>\n");
            }
            sb.append("</one-of>\n");
        } else {
            sb.append("<item>\n");
            printRightSide(sb, rightSide.stream().map(RightSidePart::getPart).toList().get(0));
            sb.append("</item>\n");
        }
        sb.append("</rule>\n");
    }

    private String nameFromLeftSymbol(RuleModel rule) {
        String oldLeftSymbol = rule.getLeftSymbol();
        return rule.getLeftSymbol().substring(1, oldLeftSymbol.length() - 1);
    }

    private void writeHeader(StringBuilder sb, Grammar grammar, boolean xml) {
        //jsgf
        if (!xml) {
            sb.append("#JSGF  V1.0;\n");
            sb.append("grammar ").append(grammar.getUri()).append(";\n\n");
            return;
        }
        //xml
        sb.append("<?xml version=\"1.0\"?>\n");
        sb.append("<grammar version=\"1.0\"\n");
        RuleModel rootRule;
        String uri;
        if ((uri = grammar.getUri()) != null) {
            sb.append("xml:base=\"").append(uri).append("\"\n");
        }
        if ((rootRule = grammar.getRootRule()) != null) {
            sb.append("root=\"").append(nameFromLeftSymbol(rootRule)).append("\"\n");
        }

        sb.append(" xmlns=\"http://www.w3.org/2001/06/grammar\">\n");
    }

    private String generateFile(Grammar grammar, boolean xml) {
        StringBuilder sb = new StringBuilder();
        writeHeader(sb, grammar, xml);
        RuleModel rootRule;
        if (null != (rootRule = grammar.getRootRule())) {
            writeRule(sb, rootRule, xml);
        }
        for (RuleModel rule : grammar.getRules()) {
            if (!rule.getRoot()) {
                writeRule(sb, rule, xml);
            }
        }
        if (xml) {
            sb.append("</grammar>");
        }
        return sb.toString();
    }

    public String getStringFromFileHolder(FileMetadata fileData) {
        return fileData.getFileContent().getContent();
    }

    @Transactional
    public Pair<String, Integer> newFileFromGrammar(Long id, boolean xml, boolean save) {
        Grammar grammar = grammarService.getGrammarById(id);
        FileMetadata fileHolder = fileMetaDataRepo.getFileHolderByGeneratedTrueAndGrammar_IdAndXml(id, xml);

        if (fileHolder != null && (grammar.getUpdateTime() == null || fileHolder.getUpdateTime().compareTo(grammar.getUpdateTime()) > 0)) {
            String content = getStringFromFileHolder(fileHolder);
            return new Pair<>(content, 0);
        }


        String fileContent = generateFile(grammar, xml);
        if (!save) {
            return new Pair<>(fileContent, 2);
        }
        if (fileHolder != null) {
            fileHolder.setSize(fileContent.length());
            fileHolder.getFileContent().setContent(fileContent);
            return new Pair<>(fileContent, 2);
        }
        fileHolder = new FileMetadata(xml, true, grammar);

        fileMetaDataRepo.save(fileHolder);
        grammar.getFiles().add(fileHolder);

        fileHolder.setSize(fileContent.length());
        fileHolder.setFileContent(saveFile(fileContent));
        return new Pair<>(fileContent, 1);
    }


}

