package de.duehl.basics.regex;

/*
 * Copyright 2019 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import de.duehl.basics.text.NumberString;

/**
 * Diese Klasse erstellt aus einem nichtnegativem Bereich einen möglichst kompakten regulären
 * Ausdruck.
 *
 * @version 1.01     2019-01-10
 * @author Christian Dühl
 */

public class RangeToRegex {


    private static boolean BRACKETS = true; // die sind zwar meist überflüssig, aber nicht immer...
                                            // daher muss das auf true stehen bleiben!

    private static final String OPENING_BRACKET = BRACKETS ? "(?:" : "";
    private static final String OPENING_BRACKET_WITHOUT_QUESTIONMARK = BRACKETS ? "(" : "";
    private static final String CLOSING_BRACKET = BRACKETS ? ")" : "";

    /** Untere Grenze des Bereichs (einschließlich). */
    private final int from;

    /** Obere Grenze des Bereichs (einschließlich). */
    private final int to;

    /** Regulärer Ausdruck der aufgebaut wird. */
    private StringBuilder regex;

    private String openingBracket;
    private String closingBracket;

    /**
     * Konstruktor.
     *
     * @param from
     *            Untere Grenze des Bereichs (einschließlich).
     * @param to
     *            Obere Grenze des Bereichs (einschließlich).
     */
    public RangeToRegex(String from, String to) {
        this(NumberString.parseInt(from, "Die untere Grenze '" + from
                        + "' des Bereiches lässt sich nicht als Integer parsen."),
                NumberString.parseInt(to, "Die obere Grenze '" + to
                        + "' des Bereiches lässt sich nicht als Integer parsen."));
    }

    /**
     * Konstruktor.
     *
     * @param from
     *            Untere Grenze des Bereichs (einschließlich).
     * @param to
     *            Obere Grenze des Bereichs (einschließlich).
     */
    public RangeToRegex(int from, int to) {
        this.from = from;
        this.to = to;

        if (from < 0) {
            throw new IllegalArgumentException("Die untere Grenze '" + from
                        + "' des Bereiches ist kleiner als 0.");
        }
        if (to < 0) {
            throw new IllegalArgumentException("Die obere Grenze '" + to
                        + "' des Bereiches ist kleiner als 0.");
        }
        if (to < from) {
            throw new IllegalArgumentException("Die obere Grenze '" + to
                        + "' ist kleiner als die untere Grenze '" + from
                        + "' des Bereiches .");
        }

        openingBracket = OPENING_BRACKET;
        closingBracket = CLOSING_BRACKET;
    }

    public void useNormalBreaks() {
        openingBracket = OPENING_BRACKET_WITHOUT_QUESTIONMARK;
        closingBracket = CLOSING_BRACKET;
    }

    /**
     * Erzeugt einen regulären Ausdruck für die ganze Zeile.
     *
     * Anwendung mittels:                                                                 <pre>
     *
     *     int from = 9;
     *     int to = 12;
     *     RangeToRegex rangeToRegex = new RangeToRegex(from, to);
     *     String regex = rangeToRegex.createRegexFullMatch();
     *     Pattern pattern = Pattern.compile(regex);
     *     boolean found = pattern.matcher("11").find();                                 </pre>
     */
    public String createRegexFullMatch() {
        regex = new StringBuilder();

        regex.append("^");
        createRegexInnerMatch(regex);
        regex.append("$");

        return regex.toString();
    }

    /**
     * Erzeugt einen regulären Ausdruck für die Wort innerhalb der Zeile.
     *
     * Anwendung mittels:                                                                 <pre>
     *
     *     int from = 9;
     *     int to = 12;
     *     RangeToRegex rangeToRegex = new RangeToRegex(from, to);
     *     String regex = rangeToRegex.createRegexWholeWordMatch();
     *     Pattern pattern = Pattern.compile(regex);
     *     boolean found = pattern.matcher("8 9 10 11 12 13").find();                     </pre>
     */
    public String createRegexWholeWordMatch() {
        regex = new StringBuilder();

        regex.append("(?:^|\\b)");
        createRegexInnerMatch(regex);
        regex.append("(?:$|\\b)");

        return regex.toString();
    }

    /**
     * Erzeugt einen regulären Ausdruck für einen beliebigen Teil innerhalb der Zeile.
     *
     * Anwendung mittels:                                                                 <pre>
     *
     *     int from = 9;
     *     int to = 12;
     *     RangeToRegex rangeToRegex = new RangeToRegex(from, to);
     *     String regex = rangeToRegex.createRegexPartMatch();
     *     Pattern pattern = Pattern.compile(regex);
     *     boolean found = pattern.matcher("8910111213").find();                          </pre>
     */
    public String createRegexPartMatch() {
        regex = new StringBuilder();

        createRegexInnerMatch(regex);

        return regex.toString();
    }

    private void createRegexInnerMatch(StringBuilder regex) {
        regex.append(openingBracket);
        createRangeRegex();
        regex.append(closingBracket);
    }

    @SuppressWarnings("unused")
    private void createRangeRegexSimple() {
        boolean first = true;
        for (int number = from; number <= to; ++number) {
            if (first) {
                first = false;
            }
            else {
                regex.append("|");
            }
            regex.append(number);
        }
    }

    private void createRangeRegex() {
        regex.append(rangeRegex(from, to));
    }




    /*
     * Der folgende Code stammt aus:
     * https://stackoverflow.com/questions/6349161/
     *     how-to-generate-a-regular-expression-at-runtime-to-match-a-numeric-range
     */

    String rangeRegex(int n, int m) {
        return rangeRegex("" + n, "" + m);
    }

    String rangeRegex(String n, String m) {
        return n.length() == m.length() ? eqLengths(n, m) : nonEqLengths(n, m);
    }

    private String eqLengths(String from, String to) {
        char fc = from.charAt(0), tc = to.charAt(0);

        if (from.length() == 1 && to.length() == 1) {
            return charClass(fc, tc);
        }

        if (fc == tc) {
            return fc + openingBracket + rangeRegex(from.substring(1), to.substring(1))
                    + closingBracket;
        }

        String re = fc + openingBracket + baseRange(from.substring(1), true, false)
                + closingBracket + "|" + tc + openingBracket
                + baseRange(to.substring(1), false, false) + closingBracket;

        if (++fc <= --tc) {
            re += "|" + charClass(fc, tc) + nDigits(from.length() - 1);
        }

        return re;
    }

    private String nonEqLengths(String from, String to) {
        String re = baseRange(from, true, false) + "|" + baseRange(to, false, true);
        if (to.length() - from.length() > 1)
            re += "|[1-9]" + nDigits(from.length(), to.length() - 2);
        return re;
    }

    private String baseRange(String num, boolean up, boolean leading1) {
        char c = num.charAt(0);
        char low = up ? c : leading1 ? '1' : '0';
        char high = up ? '9' : c;

        if (num.length() == 1) {
            return charClass(low, high);
        }

        String re = c + openingBracket + baseRange(num.substring(1), up, false) + closingBracket;

        if (up) {
            low++;
        }
        else {
            high--;
        }

        if (low <= high) {
            re += "|" + charClass(low, high) + nDigits(num.length() - 1);
        }

        return re;
    }

    private String charClass(char b, char e) {
        return String.format(b == e ? "%c" : e - b > 1 ? "[%c-%c]" : "[%c%c]", b, e);
    }

    private String nDigits(int n) {
        return nDigits(n, n);
    }

    private String nDigits(int n, int m) {
        return "[0-9]" + String.format(n == m ? n == 1 ? "" : "{%d}" : "{%d,%d}", n, m);
    }

}
