// 
// Decompiled by Procyon v0.6.0
// 

package org.jline.builtins;

import org.jline.utils.StyleResolver;
import org.jline.utils.Log;
import java.io.Reader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import org.jline.utils.AttributedStyle;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.jline.utils.AttributedCharSequence;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedString;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URI;
import java.nio.file.OpenOption;
import java.nio.file.PathMatcher;
import java.nio.file.FileVisitOption;
import java.nio.file.LinkOption;
import java.util.stream.Stream;
import java.util.function.Consumer;
import java.util.Objects;
import java.io.BufferedReader;
import java.util.Iterator;
import java.util.regex.PatternSyntaxException;
import java.io.IOException;
import java.util.Arrays;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.nio.file.Path;

public class SyntaxHighlighter
{
    public static final String REGEX_TOKEN_NAME = "[A-Z_]+";
    public static final String TYPE_NANORCTHEME = ".nanorctheme";
    public static final String DEFAULT_NANORC_FILE = "jnanorc";
    protected static final String DEFAULT_LESSRC_FILE = "jlessrc";
    protected static final String COMMAND_INCLUDE = "include";
    protected static final String COMMAND_THEME = "theme";
    private static final String TOKEN_NANORC = "NANORC";
    private final Path nanorc;
    private final String syntaxName;
    private final String nanorcUrl;
    private final Map<String, List<HighlightRule>> rules;
    private Path currentTheme;
    private boolean startEndHighlight;
    private int ruleStartId;
    private Parser parser;
    
    private SyntaxHighlighter() {
        this(null, null, null);
    }
    
    private SyntaxHighlighter(final String nanorcUrl) {
        this(null, null, nanorcUrl);
    }
    
    private SyntaxHighlighter(final Path nanorc, final String syntaxName) {
        this(nanorc, syntaxName, null);
    }
    
    private SyntaxHighlighter(final Path nanorc, final String syntaxName, final String nanorcUrl) {
        this.rules = new HashMap<String, List<HighlightRule>>();
        this.ruleStartId = 0;
        this.nanorc = nanorc;
        this.syntaxName = syntaxName;
        this.nanorcUrl = nanorcUrl;
        final Map<String, List<HighlightRule>> defaultRules = new HashMap<String, List<HighlightRule>>();
        defaultRules.put("NANORC", new ArrayList<HighlightRule>());
        this.rules.putAll(defaultRules);
    }
    
    protected static SyntaxHighlighter build(final List<Path> syntaxFiles, final String file, final String syntaxName) {
        return build(syntaxFiles, file, syntaxName, false);
    }
    
    protected static SyntaxHighlighter build(final List<Path> syntaxFiles, final String file, final String syntaxName, final boolean ignoreErrors) {
        final SyntaxHighlighter out = new SyntaxHighlighter();
        final Map<String, String> colorTheme = new HashMap<String, String>();
        try {
            if (syntaxName == null || !syntaxName.equals("none")) {
                for (final Path p : syntaxFiles) {
                    try {
                        if (colorTheme.isEmpty() && p.getFileName().toString().endsWith(".nanorctheme")) {
                            out.setCurrentTheme(p);
                            try (final BufferedReader reader = Files.newBufferedReader(p)) {
                                String line;
                                while ((line = reader.readLine()) != null) {
                                    line = line.trim();
                                    if (!line.isEmpty() && !line.startsWith("#")) {
                                        final List<String> parts = Arrays.asList(line.split("\\s+", 2));
                                        colorTheme.put(parts.get(0), parts.get(1));
                                    }
                                }
                                if (reader == null) {
                                    continue;
                                }
                            }
                        }
                        else {
                            final NanorcParser nanorcParser = new NanorcParser(p, syntaxName, file, colorTheme);
                            nanorcParser.parse();
                            if (nanorcParser.matches()) {
                                out.addRules(nanorcParser.getHighlightRules());
                                out.setParser(nanorcParser.getParser());
                                return out;
                            }
                            if (!nanorcParser.isDefault()) {
                                continue;
                            }
                            out.addRules(nanorcParser.getHighlightRules());
                        }
                    }
                    catch (final IOException ex) {}
                }
            }
        }
        catch (final PatternSyntaxException e) {
            if (!ignoreErrors) {
                throw e;
            }
        }
        return out;
    }
    
    public static SyntaxHighlighter build(final Path nanorc, final String syntaxName) {
        final SyntaxHighlighter out = new SyntaxHighlighter(nanorc, syntaxName);
        final List<Path> syntaxFiles = new ArrayList<Path>();
        try {
            try (final BufferedReader reader = Files.newBufferedReader(nanorc)) {
                String line;
                while ((line = reader.readLine()) != null) {
                    line = line.trim();
                    if (!line.isEmpty() && !line.startsWith("#")) {
                        final List<String> parts = RuleSplitter.split(line);
                        if (parts.get(0).equals("include")) {
                            nanorcInclude(nanorc, parts.get(1), syntaxFiles);
                        }
                        else {
                            if (!parts.get(0).equals("theme")) {
                                continue;
                            }
                            nanorcTheme(nanorc, parts.get(1), syntaxFiles);
                        }
                    }
                }
            }
            final SyntaxHighlighter sh = build(syntaxFiles, null, syntaxName);
            out.addRules(sh.rules);
            out.setParser(sh.parser);
            out.setCurrentTheme(sh.currentTheme);
        }
        catch (final Exception ex) {}
        return out;
    }
    
    protected static void nanorcInclude(final Path nanorc, final String parameter, final List<Path> syntaxFiles) throws IOException {
        addFiles(nanorc, parameter, s -> {
            Objects.requireNonNull(syntaxFiles);
            s.forEach(syntaxFiles::add);
        });
    }
    
    protected static void nanorcTheme(final Path nanorc, final String parameter, final List<Path> syntaxFiles) throws IOException {
        addFiles(nanorc, parameter, s -> s.findFirst().ifPresent(p -> syntaxFiles.add(0, p)));
    }
    
    protected static void addFiles(final Path nanorc, final String parameter, final Consumer<Stream<Path>> consumer) throws IOException {
        final PathParts parts = extractPathParts(parameter);
        final Path searchRoot = nanorc.resolveSibling(parts.staticPrefix);
        if (Files.exists(searchRoot, new LinkOption[0])) {
            if (parts.globPattern.isEmpty()) {
                consumer.accept(Stream.of(searchRoot));
            }
            else {
                final PathMatcher pathMatcher = searchRoot.getFileSystem().getPathMatcher("glob:" + parts.globPattern);
                try (final Stream<Path> pathStream = Files.walk(searchRoot, new FileVisitOption[0])) {
                    consumer.accept(pathStream.filter(p -> pathMatcher.matches(searchRoot.relativize(p))));
                }
            }
        }
    }
    
    private static PathParts extractPathParts(final String pattern) {
        final int firstWildcard = Math.min((pattern.indexOf(42) == -1) ? Integer.MAX_VALUE : pattern.indexOf(42), (pattern.indexOf(63) == -1) ? Integer.MAX_VALUE : pattern.indexOf(63));
        if (firstWildcard == Integer.MAX_VALUE) {
            return new PathParts(pattern, "");
        }
        int lastSlashBeforeWildcard = -1;
        for (int i = firstWildcard - 1; i >= 0; --i) {
            final char c = pattern.charAt(i);
            if (c == '/' || c == '\\') {
                lastSlashBeforeWildcard = i;
                break;
            }
        }
        if (lastSlashBeforeWildcard == -1) {
            return new PathParts("", pattern);
        }
        final String staticPrefix = pattern.substring(0, lastSlashBeforeWildcard);
        final String globPattern = pattern.substring(lastSlashBeforeWildcard + 1);
        return new PathParts(staticPrefix, globPattern);
    }
    
    public static SyntaxHighlighter build(final String nanorcUrl) {
        final SyntaxHighlighter out = new SyntaxHighlighter(nanorcUrl);
        try {
            InputStream inputStream;
            if (nanorcUrl.startsWith("classpath:")) {
                final String resourcePath = nanorcUrl.substring(10);
                try {
                    final Path resourceAsPath = ClasspathResourceUtil.getResourcePath(resourcePath);
                    inputStream = Files.newInputStream(resourceAsPath, new OpenOption[0]);
                }
                catch (final Exception e) {
                    inputStream = new Source.ResourceSource(resourcePath, null).read();
                }
            }
            else {
                inputStream = new Source.URLSource(new URI(nanorcUrl).toURL(), null).read();
            }
            final NanorcParser parser = new NanorcParser(inputStream, null, null);
            parser.parse();
            out.addRules(parser.getHighlightRules());
        }
        catch (final IOException | URISyntaxException ex) {}
        return out;
    }
    
    private void addRules(final Map<String, List<HighlightRule>> rules) {
        this.rules.putAll(rules);
    }
    
    public void setCurrentTheme(final Path currentTheme) {
        this.currentTheme = currentTheme;
    }
    
    public Path getCurrentTheme() {
        return this.currentTheme;
    }
    
    public void setParser(final Parser parser) {
        this.parser = parser;
    }
    
    public SyntaxHighlighter reset() {
        this.ruleStartId = 0;
        this.startEndHighlight = false;
        if (this.parser != null) {
            this.parser.reset();
        }
        return this;
    }
    
    public void refresh() {
        SyntaxHighlighter sh;
        if (this.nanorc != null && this.syntaxName != null) {
            sh = build(this.nanorc, this.syntaxName);
        }
        else {
            if (this.nanorcUrl == null) {
                throw new IllegalStateException("Not possible to refresh highlighter!");
            }
            sh = build(this.nanorcUrl);
        }
        this.rules.clear();
        this.addRules(sh.rules);
        this.parser = sh.parser;
        this.currentTheme = sh.currentTheme;
    }
    
    public AttributedString highlight(final String string) {
        return this.splitAndHighlight(new AttributedString(string));
    }
    
    public AttributedString highlight(final AttributedStringBuilder asb) {
        return this.splitAndHighlight(asb.toAttributedString());
    }
    
    public AttributedString highlight(final AttributedString attributedString) {
        return this.splitAndHighlight(attributedString);
    }
    
    private AttributedString splitAndHighlight(final AttributedString attributedString) {
        final AttributedStringBuilder asb = new AttributedStringBuilder();
        boolean first = true;
        for (final AttributedString line : attributedString.columnSplitLength(Integer.MAX_VALUE)) {
            if (!first) {
                asb.append("\n");
            }
            List<ParsedToken> tokens = new ArrayList<ParsedToken>();
            if (this.parser != null) {
                this.parser.parse(line);
                tokens = this.parser.getTokens();
            }
            if (tokens.isEmpty()) {
                asb.append(this._highlight(line, this.rules.get("NANORC")));
            }
            else {
                int pos = 0;
                for (final ParsedToken t : tokens) {
                    if (t.getStart() > pos) {
                        final AttributedStringBuilder head = this._highlight(line.columnSubSequence(pos, t.getStart() + 1), this.rules.get("NANORC"));
                        asb.append(head.columnSubSequence(0, head.length() - 1));
                    }
                    asb.append(this._highlight(line.columnSubSequence(t.getStart(), t.getEnd()), this.rules.get(t.getName()), t.getStartWith(), line.columnSubSequence(t.getEnd(), line.length())));
                    pos = t.getEnd();
                }
                if (pos < line.length()) {
                    asb.append(this._highlight(line.columnSubSequence(pos, line.length()), this.rules.get("NANORC")));
                }
            }
            first = false;
        }
        return asb.toAttributedString();
    }
    
    private AttributedStringBuilder _highlight(final AttributedString line, final List<HighlightRule> rules) {
        return this._highlight(line, rules, null, null);
    }
    
    private AttributedStringBuilder _highlight(final AttributedString line, final List<HighlightRule> rules, final CharSequence startWith, final CharSequence continueAs) {
        AttributedStringBuilder asb = new AttributedStringBuilder();
        asb.append(line);
        if (rules.isEmpty()) {
            return asb;
        }
        final int startId = this.ruleStartId;
        final boolean endHighlight = this.startEndHighlight;
        for (int i = startId; i < (endHighlight ? (startId + 1) : rules.size()); ++i) {
            final HighlightRule rule = rules.get(i);
            switch (rule.getType().ordinal()) {
                case 0: {
                    asb.styleMatches(rule.getPattern(), rule.getStyle());
                    break;
                }
                case 1: {
                    boolean done = false;
                    final Matcher start = rule.getStart().matcher(asb.toAttributedString());
                    final Matcher end = rule.getEnd().matcher(asb.toAttributedString());
                    while (!done) {
                        final AttributedStringBuilder a = new AttributedStringBuilder();
                        if (this.startEndHighlight && this.ruleStartId == i) {
                            if (end.find()) {
                                this.ruleStartId = 0;
                                this.startEndHighlight = false;
                                a.append(asb.columnSubSequence(0, end.end()), rule.getStyle());
                                a.append(this._highlight(asb.columnSubSequence(end.end(), asb.length()).toAttributedString(), rules));
                            }
                            else {
                                a.append(asb, rule.getStyle());
                                done = true;
                            }
                            asb = a;
                        }
                        else if (start.find()) {
                            a.append(asb.columnSubSequence(0, start.start()));
                            if (end.find()) {
                                a.append(asb.columnSubSequence(start.start(), end.end()), rule.getStyle());
                                a.append(asb.columnSubSequence(end.end(), asb.length()));
                            }
                            else {
                                this.ruleStartId = i;
                                this.startEndHighlight = true;
                                a.append(asb.columnSubSequence(start.start(), asb.length()), rule.getStyle());
                                done = true;
                            }
                            asb = a;
                        }
                        else {
                            done = true;
                        }
                    }
                    break;
                }
                case 2: {
                    if (startWith != null && startWith.toString().startsWith(rule.getStartWith())) {
                        asb.styleMatches(rule.getPattern(), rule.getStyle());
                        break;
                    }
                    break;
                }
                case 3: {
                    if (continueAs != null && continueAs.toString().matches(rule.getContinueAs() + ".*")) {
                        asb.styleMatches(rule.getPattern(), rule.getStyle());
                        break;
                    }
                    break;
                }
            }
        }
        return asb;
    }
    
    private static class PathParts
    {
        final String staticPrefix;
        final String globPattern;
        
        PathParts(final String staticPrefix, final String globPattern) {
            this.staticPrefix = staticPrefix;
            this.globPattern = globPattern;
        }
    }
    
    static class HighlightRule
    {
        private final RuleType type;
        private Pattern pattern;
        private final AttributedStyle style;
        private Pattern start;
        private Pattern end;
        private String startWith;
        private String continueAs;
        
        public HighlightRule(final AttributedStyle style, final Pattern pattern) {
            this.type = RuleType.PATTERN;
            this.pattern = pattern;
            this.style = style;
        }
        
        public HighlightRule(final AttributedStyle style, final Pattern start, final Pattern end) {
            this.type = RuleType.START_END;
            this.style = style;
            this.start = start;
            this.end = end;
        }
        
        public HighlightRule(final RuleType parserRuleType, final AttributedStyle style, final String value) {
            this.type = parserRuleType;
            this.style = style;
            this.pattern = Pattern.compile(".*");
            if (parserRuleType == RuleType.PARSER_START_WITH) {
                this.startWith = value;
            }
            else {
                if (parserRuleType != RuleType.PARSER_CONTINUE_AS) {
                    throw new IllegalArgumentException("Bad RuleType: " + parserRuleType);
                }
                this.continueAs = value;
            }
        }
        
        public RuleType getType() {
            return this.type;
        }
        
        public AttributedStyle getStyle() {
            return this.style;
        }
        
        public Pattern getPattern() {
            if (this.type == RuleType.START_END) {
                throw new IllegalAccessError();
            }
            return this.pattern;
        }
        
        public Pattern getStart() {
            if (this.type == RuleType.PATTERN) {
                throw new IllegalAccessError();
            }
            return this.start;
        }
        
        public Pattern getEnd() {
            if (this.type == RuleType.PATTERN) {
                throw new IllegalAccessError();
            }
            return this.end;
        }
        
        public String getStartWith() {
            return this.startWith;
        }
        
        public String getContinueAs() {
            return this.continueAs;
        }
        
        public static RuleType evalRuleType(final List<String> colorCfg) {
            RuleType out = null;
            if (colorCfg.get(0).equals("color") || colorCfg.get(0).equals("icolor")) {
                out = RuleType.PATTERN;
                if (colorCfg.size() == 3) {
                    if (colorCfg.get(2).startsWith("startWith=")) {
                        out = RuleType.PARSER_START_WITH;
                    }
                    else if (colorCfg.get(2).startsWith("continueAs=")) {
                        out = RuleType.PARSER_CONTINUE_AS;
                    }
                }
                else if (colorCfg.size() == 4 && colorCfg.get(2).startsWith("start=") && colorCfg.get(3).startsWith("end=")) {
                    out = RuleType.START_END;
                }
            }
            return out;
        }
        
        @Override
        public String toString() {
            return "{type:" + this.type + ", startWith: " + this.startWith + ", continueAs: " + this.continueAs + ", start: " + this.start + ", end: " + this.end + ", pattern: " + this.pattern + "}";
        }
        
        public enum RuleType
        {
            PATTERN, 
            START_END, 
            PARSER_START_WITH, 
            PARSER_CONTINUE_AS;
        }
    }
    
    static class NanorcParser
    {
        private static final String DEFAULT_SYNTAX = "default";
        private final String name;
        private final String target;
        private final Map<String, List<HighlightRule>> highlightRules;
        private final BufferedReader reader;
        private Map<String, String> colorTheme;
        private boolean matches;
        private String syntaxName;
        private Parser parser;
        
        public NanorcParser(final Path file, final String name, final String target, final Map<String, String> colorTheme) throws IOException {
            this(new Source.PathSource(file, null).read(), name, target);
            this.colorTheme = colorTheme;
        }
        
        public NanorcParser(final InputStream in, final String name, final String target) {
            this.highlightRules = new HashMap<String, List<HighlightRule>>();
            this.colorTheme = new HashMap<String, String>();
            this.matches = false;
            this.syntaxName = "unknown";
            this.reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
            this.name = name;
            this.target = target;
            this.highlightRules.put("NANORC", new ArrayList<HighlightRule>());
        }
        
        public void parse() throws IOException {
            int idx = 0;
            try {
                String line;
                while ((line = this.reader.readLine()) != null) {
                    ++idx;
                    line = line.trim();
                    if (!line.isEmpty() && !line.startsWith("#")) {
                        final List<String> parts = RuleSplitter.split(line);
                        if (parts.get(0).equals("syntax")) {
                            this.syntaxName = parts.get(1);
                            final List<Pattern> filePatterns = new ArrayList<Pattern>();
                            if (this.name != null) {
                                if (!this.name.equals(this.syntaxName)) {
                                    break;
                                }
                                this.matches = true;
                            }
                            else if (this.target != null) {
                                for (int i = 2; i < parts.size(); ++i) {
                                    filePatterns.add(Pattern.compile(parts.get(i)));
                                }
                                for (final Pattern p : filePatterns) {
                                    if (p.matcher(this.target).find()) {
                                        this.matches = true;
                                        break;
                                    }
                                }
                                if (!this.matches && !this.syntaxName.equals("default")) {
                                    break;
                                }
                                continue;
                            }
                            else {
                                this.matches = true;
                            }
                        }
                        else if (parts.get(0).startsWith("$")) {
                            final String key = this.themeKey(parts.get(0));
                            if (this.colorTheme.containsKey(key)) {
                                if (this.parser == null) {
                                    this.parser = new Parser();
                                }
                                final String[] args = parts.get(1).split(",\\s*");
                                boolean validKey = true;
                                if (key.startsWith("$BLOCK_COMMENT")) {
                                    this.parser.setBlockCommentDelimiters(key, args);
                                }
                                else if (key.startsWith("$LINE_COMMENT")) {
                                    this.parser.setLineCommentDelimiters(key, args);
                                }
                                else if (key.startsWith("$BALANCED_DELIMITERS")) {
                                    this.parser.setBalancedDelimiters(key, args);
                                }
                                else {
                                    Log.warn("Unknown token type: ", key);
                                    validKey = false;
                                }
                                if (!validKey) {
                                    continue;
                                }
                                if (!this.highlightRules.containsKey(key)) {
                                    this.highlightRules.put(key, new ArrayList<HighlightRule>());
                                }
                                for (final String l : this.colorTheme.get(key).split("\\\\n")) {
                                    ++idx;
                                    this.addHighlightRule(RuleSplitter.split(l), idx, key);
                                }
                            }
                            else {
                                Log.warn("Unknown token type: ", key);
                            }
                        }
                        else {
                            if (this.addHighlightRule(parts, idx, "NANORC") || !parts.get(0).matches("\\+[A-Z_]+")) {
                                continue;
                            }
                            final String key = this.themeKey(parts.get(0));
                            final String theme = this.colorTheme.get(key);
                            if (theme != null) {
                                for (final String j : theme.split("\\\\n")) {
                                    ++idx;
                                    this.addHighlightRule(RuleSplitter.split(j), idx, "NANORC");
                                }
                            }
                            else {
                                Log.warn("Unknown token type: ", key);
                            }
                        }
                    }
                }
            }
            finally {
                this.reader.close();
            }
        }
        
        private boolean addHighlightRule(final List<String> parts, final int idx, final String tokenName) {
            boolean out = true;
            if (parts.get(0).equals("color")) {
                this.addHighlightRule(this.syntaxName + idx, parts, false, tokenName);
            }
            else if (parts.get(0).equals("icolor")) {
                this.addHighlightRule(this.syntaxName + idx, parts, true, tokenName);
            }
            else if (parts.get(0).matches("[A-Z_]+[:]?")) {
                final String key = this.themeKey(parts.get(0));
                final String theme = this.colorTheme.get(key);
                if (theme != null) {
                    parts.set(0, "color");
                    parts.add(1, theme);
                    this.addHighlightRule(this.syntaxName + idx, parts, false, tokenName);
                }
                else {
                    Log.warn("Unknown token type: ", key);
                }
            }
            else if (parts.get(0).matches("~[A-Z_]+[:]?")) {
                final String key = this.themeKey(parts.get(0));
                final String theme = this.colorTheme.get(key);
                if (theme != null) {
                    parts.set(0, "icolor");
                    parts.add(1, theme);
                    this.addHighlightRule(this.syntaxName + idx, parts, true, tokenName);
                }
                else {
                    Log.warn("Unknown token type: ", key);
                }
            }
            else {
                out = false;
            }
            return out;
        }
        
        private String themeKey(final String key) {
            if (key.startsWith("+")) {
                return key;
            }
            final int keyEnd = key.endsWith(":") ? (key.length() - 1) : key.length();
            if (key.startsWith("~")) {
                return key.substring(1, keyEnd);
            }
            return key.substring(0, keyEnd);
        }
        
        public boolean matches() {
            return this.matches;
        }
        
        public Parser getParser() {
            return this.parser;
        }
        
        public Map<String, List<HighlightRule>> getHighlightRules() {
            return this.highlightRules;
        }
        
        public boolean isDefault() {
            return this.syntaxName.equals("default");
        }
        
        private void addHighlightRule(final String reference, final List<String> parts, final boolean caseInsensitive, final String tokenName) {
            final Map<String, String> spec = new HashMap<String, String>();
            spec.put(reference, parts.get(1));
            final Styles.StyleCompiler obj;
            final Styles.StyleCompiler sh = obj = new Styles.StyleCompiler(spec, true);
            Objects.requireNonNull(obj);
            final AttributedStyle style = new StyleResolver(obj::getStyle).resolve("." + reference);
            try {
                if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PATTERN) {
                    if (parts.size() == 2) {
                        this.highlightRules.get(tokenName).add(new HighlightRule(style, this.doPattern(".*", caseInsensitive)));
                    }
                    else {
                        for (int i = 2; i < parts.size(); ++i) {
                            this.highlightRules.get(tokenName).add(new HighlightRule(style, this.doPattern(parts.get(i), caseInsensitive)));
                        }
                    }
                }
                else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.START_END) {
                    final String s = parts.get(2);
                    final String e = parts.get(3);
                    this.highlightRules.get(tokenName).add(new HighlightRule(style, this.doPattern(s.substring(7, s.length() - 1), caseInsensitive), this.doPattern(e.substring(5, e.length() - 1), caseInsensitive)));
                }
                else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_START_WITH) {
                    this.highlightRules.get(tokenName).add(new HighlightRule(HighlightRule.RuleType.PARSER_START_WITH, style, parts.get(2).substring(10)));
                }
                else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_CONTINUE_AS) {
                    this.highlightRules.get(tokenName).add(new HighlightRule(HighlightRule.RuleType.PARSER_CONTINUE_AS, style, parts.get(2).substring(11)));
                }
            }
            catch (final PatternSyntaxException e2) {
                Log.warn("Invalid highlight regex", reference, parts, e2);
            }
            catch (final Exception e3) {
                Log.warn("Failure while handling highlight regex", reference, parts, e3);
            }
        }
        
        private Pattern doPattern(String regex, final boolean caseInsensitive) {
            regex = Parser.fixRegexes(regex);
            return caseInsensitive ? Pattern.compile(regex, 2) : Pattern.compile(regex);
        }
    }
    
    protected static class RuleSplitter
    {
        protected static List<String> split(final String s) {
            final List<String> out = new ArrayList<String>();
            if (s.length() == 0) {
                return out;
            }
            int depth = 0;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < s.length(); ++i) {
                final char c = s.charAt(i);
                if (c == '\"') {
                    if (depth == 0) {
                        depth = 1;
                    }
                    else {
                        final char nextChar = (i < s.length() - 1) ? s.charAt(i + 1) : ' ';
                        if (nextChar == ' ') {
                            depth = 0;
                        }
                    }
                }
                else if (c == ' ' && depth == 0 && sb.length() > 0) {
                    out.add(stripQuotes(sb.toString()));
                    sb = new StringBuilder();
                    continue;
                }
                if (sb.length() > 0 || (c != ' ' && c != '\t')) {
                    sb.append(c);
                }
            }
            if (sb.length() > 0) {
                out.add(stripQuotes(sb.toString()));
            }
            return out;
        }
        
        private static String stripQuotes(final String s) {
            String out = s.trim();
            if (s.startsWith("\"") && s.endsWith("\"")) {
                out = s.substring(1, s.length() - 1);
            }
            return out;
        }
    }
    
    private static class BlockCommentDelimiters
    {
        private final String start;
        private final String end;
        
        public BlockCommentDelimiters(final String[] args) {
            if (args.length != 2 || args[0] == null || args[1] == null || args[0].isEmpty() || args[1].isEmpty() || args[0].equals(args[1])) {
                throw new IllegalArgumentException("Bad block comment delimiters!");
            }
            this.start = args[0];
            this.end = args[1];
        }
        
        public String getStart() {
            return this.start;
        }
        
        public String getEnd() {
            return this.end;
        }
    }
    
    private static class ParsedToken
    {
        private final String name;
        private final CharSequence startWith;
        private final int start;
        private final int end;
        
        public ParsedToken(final String name, final CharSequence startWith, final int start, final int end) {
            this.name = name;
            this.startWith = startWith;
            this.start = start;
            this.end = end;
        }
        
        public String getName() {
            return this.name;
        }
        
        public CharSequence getStartWith() {
            return this.startWith;
        }
        
        public int getStart() {
            return this.start;
        }
        
        public int getEnd() {
            return this.end;
        }
    }
    
    static class Parser
    {
        private static final char escapeChar = '\\';
        private String blockCommentTokenName;
        private BlockCommentDelimiters blockCommentDelimiters;
        private String lineCommentTokenName;
        private String[] lineCommentDelimiters;
        private String balancedDelimiterTokenName;
        private String[] balancedDelimiters;
        private String balancedDelimiter;
        private List<ParsedToken> tokens;
        private CharSequence startWith;
        private int tokenStart;
        private boolean blockComment;
        private boolean lineComment;
        private boolean balancedQuoted;
        
        public Parser() {
            this.tokenStart = 0;
        }
        
        public void setBlockCommentDelimiters(final String tokenName, final String[] args) {
            try {
                this.blockCommentTokenName = tokenName;
                this.blockCommentDelimiters = new BlockCommentDelimiters(args);
            }
            catch (final Exception e) {
                Log.warn(e.getMessage());
            }
        }
        
        public void setLineCommentDelimiters(final String tokenName, final String[] args) {
            this.lineCommentTokenName = tokenName;
            this.lineCommentDelimiters = args;
        }
        
        public void setBalancedDelimiters(final String tokenName, final String[] args) {
            this.balancedDelimiterTokenName = tokenName;
            this.balancedDelimiters = args;
        }
        
        public void reset() {
            this.startWith = null;
            this.blockComment = false;
            this.lineComment = false;
            this.balancedQuoted = false;
            this.tokenStart = 0;
        }
        
        public void parse(final CharSequence line) {
            if (line == null) {
                return;
            }
            this.tokens = new ArrayList<ParsedToken>();
            if (this.blockComment || this.balancedQuoted) {
                this.tokenStart = 0;
            }
            for (int i = 0; i < line.length(); ++i) {
                if (!this.isEscapeChar(line, i)) {
                    if (!this.isEscaped(line, i)) {
                        if (!this.blockComment && !this.lineComment && !this.balancedQuoted) {
                            if (this.blockCommentDelimiters != null && this.isDelimiter(line, i, this.blockCommentDelimiters.getStart())) {
                                this.blockComment = true;
                                this.tokenStart = i;
                                this.startWith = this.startWithSubstring(line, i);
                                i = i + this.blockCommentDelimiters.getStart().length() - 1;
                            }
                            else {
                                if (this.isLineCommentDelimiter(line, i)) {
                                    this.lineComment = true;
                                    this.tokenStart = i;
                                    this.startWith = this.startWithSubstring(line, i);
                                    break;
                                }
                                if ((this.balancedDelimiter = this.balancedDelimiter(line, i)) != null) {
                                    this.balancedQuoted = true;
                                    this.tokenStart = i;
                                    this.startWith = this.startWithSubstring(line, i);
                                    i = i + this.balancedDelimiter.length() - 1;
                                }
                            }
                        }
                        else if (this.blockComment) {
                            if (this.isDelimiter(line, i, this.blockCommentDelimiters.getEnd())) {
                                this.blockComment = false;
                                i = i + this.blockCommentDelimiters.getEnd().length() - 1;
                                this.tokens.add(new ParsedToken(this.blockCommentTokenName, this.startWith, this.tokenStart, i + 1));
                            }
                        }
                        else if (this.balancedQuoted && this.isDelimiter(line, i, this.balancedDelimiter)) {
                            this.balancedQuoted = false;
                            i = i + this.balancedDelimiter.length() - 1;
                            if (i - this.tokenStart + 1 > 2 * this.balancedDelimiter.length()) {
                                this.tokens.add(new ParsedToken(this.balancedDelimiterTokenName, this.startWith, this.tokenStart, i + 1));
                            }
                        }
                    }
                }
            }
            if (this.blockComment) {
                this.tokens.add(new ParsedToken(this.blockCommentTokenName, this.startWith, this.tokenStart, line.length()));
            }
            else if (this.lineComment) {
                this.lineComment = false;
                this.tokens.add(new ParsedToken(this.lineCommentTokenName, this.startWith, this.tokenStart, line.length()));
            }
            else if (this.balancedQuoted) {
                this.tokens.add(new ParsedToken(this.balancedDelimiterTokenName, this.startWith, this.tokenStart, line.length()));
            }
        }
        
        private CharSequence startWithSubstring(final CharSequence line, final int pos) {
            return line.subSequence(pos, Math.min(pos + 5, line.length()));
        }
        
        public List<ParsedToken> getTokens() {
            return this.tokens;
        }
        
        private String balancedDelimiter(final CharSequence buffer, final int pos) {
            if (this.balancedDelimiters != null) {
                for (final String delimiter : this.balancedDelimiters) {
                    if (this.isDelimiter(buffer, pos, delimiter)) {
                        return delimiter;
                    }
                }
            }
            return null;
        }
        
        private boolean isDelimiter(final CharSequence buffer, final int pos, final String delimiter) {
            if (pos < 0 || delimiter == null) {
                return false;
            }
            final int length = delimiter.length();
            if (length <= buffer.length() - pos) {
                for (int i = 0; i < length; ++i) {
                    if (delimiter.charAt(i) != buffer.charAt(pos + i)) {
                        return false;
                    }
                }
                return true;
            }
            return false;
        }
        
        private boolean isLineCommentDelimiter(final CharSequence buffer, final int pos) {
            if (this.lineCommentDelimiters != null) {
                for (final String delimiter : this.lineCommentDelimiters) {
                    if (this.isDelimiter(buffer, pos, delimiter)) {
                        return true;
                    }
                }
            }
            return false;
        }
        
        private boolean isEscapeChar(final char ch) {
            return '\\' == ch;
        }
        
        private boolean isEscapeChar(final CharSequence buffer, final int pos) {
            if (pos < 0) {
                return false;
            }
            final char ch = buffer.charAt(pos);
            return this.isEscapeChar(ch) && !this.isEscaped(buffer, pos);
        }
        
        private boolean isEscaped(final CharSequence buffer, final int pos) {
            return pos > 0 && this.isEscapeChar(buffer, pos - 1);
        }
        
        static String fixRegexes(final String posix) {
            final int len = posix.length();
            final StringBuilder java = new StringBuilder();
            boolean inBracketExpression = false;
            int i = 0;
            try {
                while (i < len) {
                    final char c = posix.charAt(i);
                    switch (c) {
                        case '\\': {
                            char next = posix.charAt(++i);
                            if (inBracketExpression && next == ']') {
                                inBracketExpression = false;
                                java.append("\\\\").append(next);
                                break;
                            }
                            if (next == '<' || next == '>') {
                                next = 'b';
                            }
                            java.append(c).append(next);
                            break;
                        }
                        case '[': {
                            if (i == len - 1) {
                                throw new IllegalArgumentException("Lone [ at the end of (index " + i + "): " + posix);
                            }
                            if (posix.charAt(i + 1) == ':') {
                                final int afterClass = nextAfterClass(posix, i + 2);
                                if (!posix.regionMatches(afterClass, ":]", 0, 2)) {
                                    java.append("[:");
                                    ++i;
                                    inBracketExpression = true;
                                    break;
                                }
                                final String className = posix.substring(i + 2, afterClass);
                                java.append(replaceClass(className));
                                i = afterClass + 1;
                                break;
                            }
                            else {
                                if (inBracketExpression) {
                                    java.append('\\').append(c);
                                    break;
                                }
                                inBracketExpression = true;
                                java.append(c);
                                final char next = posix.charAt(i + 1);
                                if (next == ']') {
                                    ++i;
                                    java.append("\\]");
                                    break;
                                }
                                if (next == '^' && posix.charAt(i + 2) == ']') {
                                    i += 2;
                                    java.append("^\\]");
                                    break;
                                }
                                break;
                            }
                            break;
                        }
                        case ']': {
                            inBracketExpression = false;
                            java.append(c);
                            break;
                        }
                        default: {
                            java.append(c);
                            break;
                        }
                    }
                    ++i;
                }
            }
            catch (final Exception e) {
                throw new IllegalArgumentException("Posix-to-Java regex translation failed around index " + i + " of: " + posix, e);
            }
            return java.toString();
        }
        
        private static String replaceClass(final String className) {
            switch (className) {
                case "alnum": {
                    return "\\p{Alnum}";
                }
                case "alpha": {
                    return "\\p{Alpha}";
                }
                case "blank": {
                    return "\\p{Blank}";
                }
                case "cntrl": {
                    return "\\p{Cntrl}";
                }
                case "digit": {
                    return "\\p{Digit}";
                }
                case "graph": {
                    return "\\p{Graph}";
                }
                case "lower": {
                    return "\\p{Lower}";
                }
                case "print": {
                    return "\\p{Print}";
                }
                case "punct": {
                    return "\\p{Punct}";
                }
                case "space": {
                    return "\\s";
                }
                case "upper": {
                    return "\\p{Upper}";
                }
                case "xdigit": {
                    return "\\p{XDigit}";
                }
                default: {
                    throw new IllegalArgumentException("Unknown class '" + className + "'");
                }
            }
        }
        
        private static int nextAfterClass(final String s, int idx) {
            if (s.charAt(idx) == ':') {
                ++idx;
            }
            while (true) {
                final char c = s.charAt(idx);
                if (!Character.isLetterOrDigit(c)) {
                    break;
                }
                ++idx;
            }
            return idx;
        }
    }
}
