module dfmt.globmatch_editorconfig;

import std.path : CaseSensitive;
import std.range : isForwardRange, ElementEncodingType;
import std.traits : isSomeChar, isSomeString;
import std.range.primitives :  empty, save, front, popFront;
import std.traits : Unqual;
import std.conv : to;
import std.path : filenameCharCmp, isDirSeparator;

// From std.path with changes:
// * changes meaning to match all characters except '/'
// ** added to take over the old meaning of *
bool globMatchEditorConfig(CaseSensitive cs = CaseSensitive.osDefault, C, Range)(
        Range path, const(C)[] pattern) @safe pure
        if (isForwardRange!Range && isSomeChar!(ElementEncodingType!Range)
            && isSomeChar!C && is(Unqual!C == Unqual!(ElementEncodingType!Range)))
in
{
    // Verify that pattern[] is valid
    import std.algorithm : balancedParens;

    assert(balancedParens(pattern, '[', ']', 0));
    assert(balancedParens(pattern, '{', '}', 0));
}
do
{
    alias RC = Unqual!(ElementEncodingType!Range);

    static if (RC.sizeof == 1 && isSomeString!Range)
    {
        import std.utf : byChar;

        return globMatchEditorConfig!cs(path.byChar, pattern);
    }
    else static if (RC.sizeof == 2 && isSomeString!Range)
    {
        import std.utf : byWchar;

        return globMatchEditorConfig!cs(path.byWchar, pattern);
    }
    else
    {
        C[] pattmp;
        foreach (ref pi; 0 .. pattern.length)
        {
            const pc = pattern[pi];
            switch (pc)
            {
            case '*':
                if (pi < pattern.length - 1 && pattern[pi + 1] == '*')
                {
                    if (pi + 2 == pattern.length)
                        return true;
                    for (; !path.empty; path.popFront())
                    {
                        auto p = path.save;
                        if (globMatchEditorConfig!(cs, C)(p, pattern[pi + 2 .. pattern.length]))
                            return true;
                    }
                    return false;
                }
                else
                {
                    if (pi + 1 == pattern.length)
                        return true;
                    for (; !path.empty; path.popFront())
                    {
                        auto p = path.save;
                        //if (p[0].to!dchar.isDirSeparator() && !pattern[pi+1].isDirSeparator())
                        //    return false;
                        if (globMatchEditorConfig!(cs, C)(p, pattern[pi + 1 .. pattern.length]))
                            return true;
                        if (p[0].to!dchar.isDirSeparator())
                            return false;
                    }
                    return false;
                }
            case '?':
                if (path.empty)
                    return false;
                path.popFront();
                break;

            case '[':
                if (path.empty)
                    return false;
                auto nc = path.front;
                path.popFront();
                auto not = false;
                ++pi;
                if (pattern[pi] == '!')
                {
                    not = true;
                    ++pi;
                }
                auto anymatch = false;
                while (1)
                {
                    const pc2 = pattern[pi];
                    if (pc2 == ']')
                        break;
                    if (!anymatch && (filenameCharCmp!cs(nc, pc2) == 0))
                        anymatch = true;
                    ++pi;
                }
                if (anymatch == not)
                    return false;
                break;

            case '{':
                // find end of {} section
                auto piRemain = pi;
                for (; piRemain < pattern.length && pattern[piRemain] != '}'; ++piRemain)
                {
                }

                if (piRemain < pattern.length)
                    ++piRemain;
                ++pi;

                while (pi < pattern.length)
                {
                    const pi0 = pi;
                    C pc3 = pattern[pi];
                    // find end of current alternative
                    for (; pi < pattern.length && pc3 != '}' && pc3 != ','; ++pi)
                    {
                        pc3 = pattern[pi];
                    }

                    auto p = path.save;
                    if (pi0 == pi)
                    {
                        if (globMatchEditorConfig!(cs, C)(p, pattern[piRemain .. $]))
                        {
                            return true;
                        }
                        ++pi;
                    }
                    else
                    {
                        /* Match for:
                            *   pattern[pi0..pi-1] ~ pattern[piRemain..$]
                            */
                        if (pattmp.ptr == null) // Allocate this only once per function invocation.
                            // Should do it with malloc/free, but that would make it impure.
                            pattmp = new C[pattern.length];

                        const len1 = pi - 1 - pi0;
                        pattmp[0 .. len1] = pattern[pi0 .. pi - 1];

                        const len2 = pattern.length - piRemain;
                        pattmp[len1 .. len1 + len2] = pattern[piRemain .. $];

                        if (globMatchEditorConfig!(cs, C)(p, pattmp[0 .. len1 + len2]))
                        {
                            return true;
                        }
                    }
                    if (pc3 == '}')
                    {
                        break;
                    }
                }
                return false;

            default:
                if (path.empty)
                    return false;
                if (filenameCharCmp!cs(pc, path.front) != 0)
                    return false;
                path.popFront();
                break;
            }
        }
        return path.empty;
    }
}

unittest
{
    assert(globMatchEditorConfig!(CaseSensitive.no)("foo", "Foo"));
    assert(!globMatchEditorConfig!(CaseSensitive.yes)("foo", "Foo"));

    assert(globMatchEditorConfig("foo", "*"));
    assert(globMatchEditorConfig("foo.bar"w, "*"w));
    assert(globMatchEditorConfig("foo.bar"d, "*.*"d));
    assert(globMatchEditorConfig("foo.bar", "foo*"));
    assert(globMatchEditorConfig("foo.bar"w, "f*bar"w));
    assert(globMatchEditorConfig("foo.bar"d, "f*b*r"d));
    assert(globMatchEditorConfig("foo.bar", "f???bar"));
    assert(globMatchEditorConfig("foo.bar"w, "[fg]???bar"w));
    assert(globMatchEditorConfig("foo.bar"d, "[!gh]*bar"d));

    assert(!globMatchEditorConfig("foo", "bar"));
    assert(!globMatchEditorConfig("foo"w, "*.*"w));
    assert(!globMatchEditorConfig("foo.bar"d, "f*baz"d));
    assert(!globMatchEditorConfig("foo.bar", "f*b*x"));
    assert(!globMatchEditorConfig("foo.bar", "[gh]???bar"));
    assert(!globMatchEditorConfig("foo.bar"w, "[!fg]*bar"w));
    assert(!globMatchEditorConfig("foo.bar"d, "[fg]???baz"d));
    assert(!globMatchEditorConfig("foo.di", "*.d")); // test issue 6634: triggered bad assertion

    assert(globMatchEditorConfig("foo.bar", "{foo,bif}.bar"));
    assert(globMatchEditorConfig("bif.bar"w, "{foo,bif}.bar"w));

    assert(globMatchEditorConfig("bar.foo"d, "bar.{foo,bif}"d));
    assert(globMatchEditorConfig("bar.bif", "bar.{foo,bif}"));

    assert(globMatchEditorConfig("bar.fooz"w, "bar.{foo,bif}z"w));
    assert(globMatchEditorConfig("bar.bifz"d, "bar.{foo,bif}z"d));

    assert(globMatchEditorConfig("bar.foo", "bar.{biz,,baz}foo"));
    assert(globMatchEditorConfig("bar.foo"w, "bar.{biz,}foo"w));
    assert(globMatchEditorConfig("bar.foo"d, "bar.{,biz}foo"d));
    assert(globMatchEditorConfig("bar.foo", "bar.{}foo"));

    assert(globMatchEditorConfig("bar.foo"w, "bar.{ar,,fo}o"w));
    assert(globMatchEditorConfig("bar.foo"d, "bar.{,ar,fo}o"d));
    assert(globMatchEditorConfig("bar.o", "bar.{,ar,fo}o"));

    assert(!globMatchEditorConfig("foo", "foo?"));
    assert(!globMatchEditorConfig("foo", "foo[]"));
    assert(!globMatchEditorConfig("foo", "foob"));
    assert(!globMatchEditorConfig("foo", "foo{b}"));

    assert(globMatchEditorConfig(`foo/foo\bar`, "f**b**r"));
    assert(globMatchEditorConfig("foo", "**"));
    assert(globMatchEditorConfig("foo/bar", "foo/bar"));
    assert(globMatchEditorConfig("foo/bar", "foo/*"));
    assert(globMatchEditorConfig("foo/bar", "*/bar"));
    assert(globMatchEditorConfig("/foo/bar/gluu/sar.png", "**/sar.png"));
    assert(globMatchEditorConfig("/foo/bar/gluu/sar.png", "**/*.png"));
    assert(!globMatchEditorConfig("/foo/bar/gluu/sar.png", "*/sar.png"));
    assert(!globMatchEditorConfig("/foo/bar/gluu/sar.png", "*/*.png"));

    static assert(globMatchEditorConfig("foo.bar", "[!gh]*bar"));
}
