/**
 * Read a file from disk and store it in memory.
 *
 * Copyright: Copyright (C) 1999-2024 by The D Language Foundation, All Rights Reserved
 * License:   $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:    $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/file_manager.d, _file_manager.d)
 * Documentation:  https://dlang.org/phobos/dmd_file_manager.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/file_manager.d
 */

module dmd.file_manager;

import core.stdc.stdio;
import dmd.root.stringtable : StringTable;
import dmd.root.file : File, Buffer;
import dmd.root.filename : FileName, isDirSeparator;
import dmd.root.string : toDString;
import dmd.globals;
import dmd.identifier;
import dmd.location;

enum package_d  = "package." ~ mars_ext;
enum package_di = "package." ~ hdr_ext;

/// Returns: whether a file with `name` is a special "package.d" module
bool isPackageFileName(scope FileName fileName) nothrow
{
    return FileName.equals(fileName.name, package_d) || FileName.equals(fileName.name, package_di);
}

// A path stack that allows one to go up and down the path using directory
// separators. `cur` is the current path, `up` goes up one path, `down` goes
// down one path. if `up` or `down` return false, there are no further paths.
private struct PathStack
{
    private const(char)[] path;
    private size_t pos;

    @safe @nogc nothrow pure:

    this(const(char)[] p)
    {
        path = p;
        pos = p.length;
    }

    const(char)[] cur()
    {
        return path[0 .. pos];
    }

    bool up()
    {
        if (pos == 0)
            return false;
        while (--pos != 0)
            if (isDirSeparator(path[pos]))
                return true;
        return false;
    }

    bool down()
    {
        if (pos == path.length)
            return false;
        while (++pos != path.length)
            if (isDirSeparator(path[pos]))
                return true;
        return false;
    }
}

final class FileManager
{
    private StringTable!(const(ubyte)[]) files;
    private StringTable!(bool) packageStatus;

    // check if the package path of the given path exists. The input path is
    // expected to contain the full path to the module, so the parent
    // directories of that path are checked.
    private bool packageExists(const(char)[] p) nothrow
    {
        // step 1, look for the closest parent path that is cached
        bool exists = true;
        auto st = PathStack(p);
        while (st.up) {
            if (auto cached = packageStatus.lookup(st.cur)) {
                exists = cached.value;
                break;
            }
        }
        // found a parent that is cached (or reached the end of the stack).
        // step 2, traverse back up the stack storing either false if the
        // parent doesn't exist, or the result of the `exists` call if it does.
        while (st.down) {
            if (!exists)
                packageStatus.insert(st.cur, false);
            else
                exists = packageStatus.insert(st.cur, FileName.exists(st.cur) == 2).value;
        }

        // at this point, exists should be the answer.
        return exists;
    }

    ///
    public this () nothrow
    {
        this.files._init();
        this.packageStatus._init();
    }

nothrow:
    /********************************************
    * Look for the source file if it's different from filename.
    * Look for .di, .d, directory, and along global.path.
    * Does not open the file.
    * Params:
    *      filename = as supplied by the user
    *      path = path to look for filename
    * Returns:
    *      the found file name or
    *      `null` if it is not different from filename.
    */
    const(char)[] lookForSourceFile(const char[] filename, const char*[] path)
    {
        //printf("lookForSourceFile(`%.*s`)\n", cast(int)filename.length, filename.ptr);
        /* Search along path[] for .di file, then .d file.
        */
        // see if we should check for the module locally.
        bool checkLocal = packageExists(filename);
        const sdi = FileName.forceExt(filename, hdr_ext);
        if (checkLocal && FileName.exists(sdi) == 1)
            return sdi;
        scope(exit) FileName.free(sdi.ptr);

        const sd = FileName.forceExt(filename, mars_ext);
        // Special file name representing `stdin`, always assume its presence
        if (sd == "__stdin.d")
            return sd;
        if (checkLocal && FileName.exists(sd) == 1)
            return sd;
        scope(exit) FileName.free(sd.ptr);

        if (checkLocal)
        {
            auto cached = packageStatus.lookup(filename);
            if (!cached)
                cached = packageStatus.insert(filename, FileName.exists(filename) == 2);
            if (cached.value)
            {
                /* The filename exists and it's a directory.
                 * Therefore, the result should be: filename/package.d
                 * iff filename/package.d is a file
                 */
                const ni = FileName.combine(filename, package_di);
                if (FileName.exists(ni) == 1)
                    return ni;
                FileName.free(ni.ptr);

                const n = FileName.combine(filename, package_d);
                if (FileName.exists(n) == 1)
                    return n;
                FileName.free(n.ptr);
            }
        }

        if (FileName.absolute(filename))
            return null;
        if (!path.length)
            return null;
        foreach (entry; path)
        {
            const p = entry.toDString();

            const(char)[] n = FileName.combine(p, sdi);

            if (!packageExists(n)) {
                FileName.free(n.ptr);
                continue; // no need to check for anything else.
            }
            if (FileName.exists(n) == 1) {
                return n;
            }
            FileName.free(n.ptr);

            n = FileName.combine(p, sd);
            if (FileName.exists(n) == 1) {
                return n;
            }
            FileName.free(n.ptr);

            const b = FileName.removeExt(filename);
            n = FileName.combine(p, b);
            FileName.free(b.ptr);

            scope(exit) FileName.free(n.ptr);

            // also cache this if we are looking for package.d[i]
            auto cached = packageStatus.lookup(n);
            if (!cached) {
                cached = packageStatus.insert(n, FileName.exists(n) == 2);
            }

            if (cached.value)
            {
                const n2i = FileName.combine(n, package_di);
                if (FileName.exists(n2i) == 1)
                    return n2i;
                FileName.free(n2i.ptr);
                const n2 = FileName.combine(n, package_d);
                if (FileName.exists(n2) == 1) {
                    return n2;
                }
                FileName.free(n2.ptr);
            }
        }

        /* ImportC: No D modules found, now search along path[] for .i file, then .c file.
         */
        const si = FileName.forceExt(filename, i_ext);
        if (FileName.exists(si) == 1)
            return si;
        scope(exit) FileName.free(si.ptr);

        const sc = FileName.forceExt(filename, c_ext);
        if (FileName.exists(sc) == 1)
            return sc;
        scope(exit) FileName.free(sc.ptr);
        foreach (entry; path)
        {
            const p = entry.toDString();

            const(char)[] n = FileName.combine(p, si);
            if (FileName.exists(n) == 1) {
                return n;
            }
            FileName.free(n.ptr);

            n = FileName.combine(p, sc);
            if (FileName.exists(n) == 1) {
                return n;
            }
            FileName.free(n.ptr);
        }
        return null;
    }

    /**
     * Looks up the given filename from the internal file buffer table.
     * If the file does not already exist within the table, it will be read from the filesystem.
     * If it has been read before,
     *
     * Returns: the loaded source file if it was found in memory,
     *      otherwise `null`
     */
    const(ubyte)[] lookup(FileName filename)
    {
        const name = filename.toString;
        if (auto val = files.lookup(name))
            return val.value;

        if (name == "__stdin.d")
        {
            auto buffer = readFromStdin().extractSlice();
            if (this.files.insert(name, buffer) is null)
                assert(0, "stdin: Insert after lookup failure should never return `null`");
            return buffer;
        }

        if (FileName.exists(name) != 1)
            return null;

        auto readResult = File.read(name);
        if (!readResult.success)
            return null;

        auto fb = readResult.extractSlice();
        if (files.insert(name, fb) is null)
            assert(0, "Insert after lookup failure should never return `null`");

        return fb;
    }

    /**********************************
     * Take `text` and turn it into an InputRange that emits
     * slices into `text` for each line.
     * Params:
     *  text = array of characters
     * Returns:
     *  InputRange accessing `text` as a sequence of lines
     * Reference:
     *  `std.string.splitLines()`
     */
    auto splitLines(const char[] text)
    {
        struct Range
        {
          @safe:
          @nogc:
          nothrow:
          pure:
          private:

            const char[] text;
            size_t index;       // index of start of line
            size_t eolIndex;    // index of end of line before newline characters
            size_t nextIndex;   // index past end of line

            public this(const char[] text)
            {
                this.text = text;
            }

            public bool empty() { return index == text.length; }

            public void popFront() { advance(); index = nextIndex; }

            public const(char)[] front() { advance(); return text[index .. eolIndex]; }

            private void advance()
            {
                if (index != nextIndex) // if already advanced
                    return;

                for (size_t i = index; i < text.length; ++i)
                {
                    switch (text[i])
                    {
                        case '\v', '\f', '\n':
                            eolIndex = i;
                            nextIndex = i + 1;
                            return;

                        case '\r':
                            if (i + 1 < text.length && text[i + 1] == '\n') // decode "\r\n"
                            {
                                eolIndex = i;
                                nextIndex = i + 2;
                                return;
                            }
                            eolIndex = i;
                            nextIndex = i + 1;
                            return;

                        /* Manually decode:
                         *  NEL is C2 85
                         */
                        case 0xC2:
                            if (i + 1 < text.length && text[i + 1] == 0x85)
                            {
                                eolIndex = i;
                                nextIndex = i + 2;
                                return;
                            }
                            break;

                        /* Manually decode:
                         *  lineSep is E2 80 A8
                         *  paraSep is E2 80 A9
                         */
                        case 0xE2:
                            if (i + 2 < text.length &&
                                text[i + 1] == 0x80 &&
                                (text[i + 2] == 0xA8 || text[i + 2] == 0xA9)
                               )
                            {
                                eolIndex = i;
                                nextIndex = i + 3;
                                return;
                            }
                            break;

                        default:
                            break;
                    }
                }
            }
        }

        return Range(text);
    }

    /**
     * Adds the contents of a file to the table.
     * Params:
     *  filename = name of the file
     *  buffer = contents of the file
     * Returns:
     *  the buffer added, or null
     */
    const(ubyte)[] add(FileName filename, const(ubyte)[] buffer)
    {
        auto val = files.insert(filename.toString, buffer);
        return val == null ? null : val.value;
    }
}

private Buffer readFromStdin() nothrow
{
    import core.stdc.stdio;
    import dmd.errors;
    import dmd.root.rmem;

    enum bufIncrement = 128 * 1024;
    size_t pos = 0;
    size_t sz = bufIncrement;

    ubyte* buffer = null;
    for (;;)
    {
        buffer = cast(ubyte*)mem.xrealloc(buffer, sz + 4); // +2 for sentinel and +2 for lexer

        // Fill up buffer
        do
        {
            assert(sz > pos);
            size_t rlen = fread(buffer + pos, 1, sz - pos, stdin);
            pos += rlen;
            if (ferror(stdin))
            {
                import core.stdc.errno;
                error(Loc.initial, "cannot read from stdin, errno = %d", errno);
                fatal();
            }
            if (feof(stdin))
            {
                // We're done
                assert(pos < sz + 2);
                buffer[pos .. pos + 4] = '\0';
                return Buffer(buffer[0 .. pos]);
            }
        } while (pos < sz);

        // Buffer full, expand
        sz += bufIncrement;
    }

    assert(0);
}
