package net.ranides.assira.grammar.impl;

import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.grammar.G4ErrorHandler;
import net.ranides.assira.grammar.G4Node;
import net.ranides.assira.grammar.G4Parser;
import net.ranides.assira.grammar.G4ParserCoverage;
import net.ranides.assira.grammar.impl.CoverageSupport.SimpleBranchCoverage;
import net.ranides.assira.grammar.impl.CoverageSupport.SimpleRuleCoverage;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IMethod;
import net.ranides.assira.reflection.IReflectiveException;
import net.ranides.assira.text.StringUtils;
import net.ranides.assira.trace.ExceptionUtils;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Vocabulary;
import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

class G4ParserGenerated implements G4Parser {

    protected final IClass<? extends Lexer> lexer;

    protected final IClass<? extends Parser> parser;

    protected final G4ErrorHandler handler;

    private final Vocabulary vocabulary;

    private final Set<String> lexerRules;

    private final String[] parserRules;

    private final Set<String> branchValidator;

    public G4ParserGenerated(IClass<? extends Lexer> lexer, IClass<? extends Parser> parser, G4ErrorHandler handler) {
        this.lexer = lexer;
        this.parser = parser;
        this.handler = handler;

        this.vocabulary = (Vocabulary)parser.field("VOCABULARY").get().getStatic();
        this.parserRules = (String[])parser.field("ruleNames").get().getStatic();
        this.lexerRules = new OpenSet<>();

        for(int i = 0, n = vocabulary.getMaxTokenType(); i<=n; i++) {
            lexerRules.add(vocabulary.getSymbolicName(i));
        }

        this.branchValidator = new OpenSet<>();

        parser.inner().forEach(type -> {
            String someName = ruleName(type.raw());
            if(type.parent().outer() == IClass.NULL) {
                branchValidator.add(someName);
            } else {
                String ruleName = ruleName(type.parent().raw());
                branchValidator.add(NodeSupport.QUALIFIER_NAME + someName);
                branchValidator.add(ruleName + NodeSupport.QUALIFIER_NAME + someName);
            }
        });
    }

    @Override
    public G4Node parse(String rule, String text) {
        return new MyRoot(rule, text).tree();
    }

    @Override
    public boolean validateRuleName(String ruleName) {
        if(ruleName.contains("#")) {
            return branchValidator.contains(ruleName);
        } else {
            return Arrays.asList(parserRules).contains(ruleName);
        }
    }

    @Override
    public boolean validateTokenName(String tokenName) {
        return lexerRules.contains(tokenName);
    }

    @Override
    public String ruleName(int ruleIndex) {
        return parserRules[ruleIndex];
    }

    @Override
    public String ruleName(Class<?> ruleType) {
        return StringUtils.uncapitalize(ruleType.getSimpleName().replaceAll("Context$", ""));
    }

    @Override
    public String tokenName(int tokenIndex) {
        return vocabulary.getSymbolicName(tokenIndex);
    }

    @Override
    public G4ParserCoverage rules() {
        return new MyCoverage();
    }

    private class MyCoverage extends G4ParserCoverage {

        private final Map<String, RuleCoverage> rules;
        private final Map<String, BranchCoverage> branches;

        public MyCoverage() {
            // @todo fix coverage
            // false ALIAS detection: borderSizeComponent <= numberUnit
            // this rule has few alternatives and only one of them links to "numberUnit"
            // other alternatives link to TOKENS (not grammar rules)

            rules = new TreeMap<>();
            branches = new TreeMap<>();

            parser.inner().forEach(type -> {
                String someName = ruleName(type.raw());
                if(type.parent().outer() == IClass.NULL) {
                    rules.put(someName, new SimpleRuleCoverage(someName, getAlias(type)));

                } else {
                    String ruleName = ruleName(type.parent().raw());
                    String qualifier = NodeSupport.QUALIFIER_NAME + someName;
                    branches.put(qualifier, new SimpleBranchCoverage(ruleName, qualifier, getAlias(type)));
                }
            });

            branches.values().forEach(branch -> {
                rules.get(branch.rule()).branches().add(branch);
            });

            String[] symbols = (String[]) lexer.field("_SYMBOLIC_NAMES").get().accessible().getStatic();
            for(int i=1, n=symbols.length; i<n; i++) {
                String token = symbols[i];
                rules.put(token, new SimpleRuleCoverage(token, Ref.empty()));
            }
        }

        private Ref<String> getAlias(IClass<?> type) {
            boolean hasList = type.methods()
                .require(IAttribute.DECLARED)
                .require(0)
                .require(m -> m.returns().isSubclass(List.class))
                .count() > 0;

            if(hasList) {
                return Ref.empty();
            }

            List<IMethod> aliasRule = type
                .methods()
                .require(IAttribute.DECLARED)
                .require(0)
                .require(m -> m.returns().isSubclass(ParserRuleContext.class))
                .list();

            List<IMethod> aliasToken = type
                .methods()
                .require(IAttribute.DECLARED)
                .require(0)
                .require(m -> m.returns().isSubclass(TerminalNode.class))
                .list();


            if(!aliasRule.isEmpty() && !aliasToken.isEmpty()) {
                return Ref.empty();
            }

            if(aliasToken.size() == 1) {
                String name = aliasToken.iterator().next().name();
                return Ref.of(name);
            }

            if(aliasRule.size() == 1) {
                String name = ruleName(aliasRule.iterator().next().returns().raw());
                return Ref.of(name);
            }

            return Ref.empty();
        }

        @Override
        public Collection<G4ParserCoverage.RuleCoverage> rules() {
            return rules.values();
        }

        @Override
        public Collection<BranchCoverage> branches() {
            return branches.values();
        }

        @Override
        public Ref<G4ParserCoverage.RuleCoverage> rule(String ruleName) {
            return Ref.ofNullable(rules.get(ruleName));
        }

        @Override
        public Ref<G4ParserCoverage.BranchCoverage> branch(String ruleName) {
            return Ref.ofNullable(branches.get(ruleName));
        }
    }

    private class MyRoot extends RootAbstract {

        private final NodeForRule tree;

        public MyRoot(String ruleName, String text) {
            Lexer lexer = G4ParserGenerated.this.lexer.construct(CharStreams.fromString(text));

            lexer.removeErrorListeners();

            lexer.addErrorListener(new NodeSupport.DelegateListener(handler));

            CommonTokenStream tokens = new CommonTokenStream(lexer);
            Parser interpreter = G4ParserGenerated.this.parser.construct(tokens);

            interpreter.removeErrorListeners();
            interpreter.addErrorListener(new NodeSupport.DelegateListener(handler));

            IMethod rule = G4ParserGenerated.this.parser
                .method(ruleName)
                .orElseThrow(() -> new IllegalArgumentException("Unknown rule: " + ruleName));

            try {
                this.tree = new NodeForRule(this, null, (RuleNode) rule.call(interpreter));
            } catch(IReflectiveException.CallException e) {
                throw ExceptionUtils.rethrow(e.getCause());
            }
        }

        @Override
        public G4Parser parser() {
            return G4ParserGenerated.this;
        }

        @Override
        public G4Node tree() {
            return tree;
        }

        @Override
        public String branchName(G4Node g4node) {
            IClass<?> type = g4node.type();
            if(type.parent().outer() == IClass.NULL) {
                return NodeSupport.QUALIFIER_INDEX_DEFAULT;
            } else {
                return NodeSupport.QUALIFIER_NAME + ruleName(type.raw());
            }
        }

        @Override
        public Ref<G4Node> findLabel(G4Node g4node, String labelName) {
            Object node = g4node.unwrap();
            return IClass.typefor(node)
                .field(labelName)
                .map(f -> f.get(node))
                .map(t -> wrap(g4node, t));
        }

    }

}
