#include <fstream>
#include <algorithm>
#include <cstring>

#include "file.h"
#include "error.h"
#include "log.h"
#include "util.h"
#include "show.h"

std::string file_basename(const std::string& filename)
{
    fs::path p(filename);
    return p.stem().string();
}

std::string extension(const std::string& filename)
{
    auto pos = filename.find_last_of(".");
    if (pos != std::string::npos)
        return filename.substr(pos + 1);
    return "";
}

std::string file_directory(const std::string& filename)
{
    fs::path p(filename);
    return p.parent_path().string();
}

std::string absolute_pathname(const std::string& filename, const std::string& base)
{
    fs::path p(filename);
    if (filename.empty()) {
        return fs::current_path().string();
    }
    if (!base.empty()) {
        // Resolve filename relative to base's directory
        fs::path b(base);
        fs::path base_dir = fs::is_directory(b) ? b : b.parent_path();
        p = base_dir / p;
    }
    return fs::absolute(p).string();
}

std::string relative_pathname(const std::string& filename)
{
    /*
    fs::path relative_to_current(const fs::path& input,
                                          const fs::path& currentFile) {
    const auto base = fs::absolute(currentFile).parent_path();
    return fs::relative(fs::absolute(input), base);
    */

    fs::path p(absolute_pathname(filename)); // + "/" + filename);
    auto relpath = fs::relative(p, fs::current_path());
    std::cout << "Absolute: " << fs::current_path() << "   " << p << "->" << relpath << "\n";

    return relpath.string();
}

size_t count_substrings(const std::string& text, const std::string& substring) {
    size_t count = 0;
    size_t pos = 0;
    while ((pos = text.find(substring, pos)) != std::string::npos) {
        count++;
        pos += substring.length();
    }
    return count;
}

fs::path relative_to_cwd(const fs::path& input)
{
    const auto base = fs::current_path();
    std::error_code ec;
    auto abs_input = fs::weakly_canonical(input, ec);
    if (ec) abs_input = fs::absolute(input);
    auto rel = fs::relative(abs_input, base, ec);
    if (ec) rel = abs_input.lexically_relative(base);
    auto result = rel.empty() ? abs_input : rel;
    if (count_substrings(result.string(), "../") > 3) {
        result = input;
    }
    return result;
}


std::string relative_pathname(const std::string& filename, std::string base)
{
    fs::path p(absolute_pathname(filename));
    auto relpath = relative(p, base);
    relpath = relpath.lexically_normal();
    return relpath.string();
}

bool file_exists(const std::string& pathname, bool error_if_not, bool is_directory)
{
    fs::path p(pathname);
    bool exists = fs::exists(p);
    bool regular = fs::is_regular_file(p);
    bool directory = fs::is_directory(p);
    bool valid = exists and (regular or directory);

    std::string filetype = is_directory ? "Directory " : "File";
    Locator no_source("", -1, -1);
    if (error_if_not and not valid) {
        if (not exists)
            throw File_error(filetype + " '" + pathname + "' does not exist", no_source);
        else if (not is_directory and not regular)
            throw File_error(filetype + " '" + pathname + "' is not a regular text file", no_source);
        else if (is_directory and regular)
            throw File_error(filetype + " '" + pathname + "' is not a directory", no_source);
    }
    return valid;
}


// Filename lists (see file.h): standalone-"/" separation, existence-rescue
// grouping, and tilde expansion.

std::string expand_tilde(const std::string& path)
{
    if (path == "~" || (path.size() > 1 && path[0] == '~' && path[1] == '/')) {
        std::string home = get_env_var("HOME");
        if (!home.empty()) {
            return home + path.substr(1);
        }
    }
    return path;
}

static bool filename_exists(const std::string& name, const std::string& base_dir)
{
    std::string resolved = expand_tilde(name);
    if (!base_dir.empty() && !fs::path(resolved).is_absolute()) {
        resolved = base_dir + "/" + resolved;
    }
    return fs::is_regular_file(resolved);
}

strings_t group_filename_tokens(const strings_t& tokens, const std::string& base_dir)
{
    strings_t result {};
    if (std::find(tokens.begin(), tokens.end(), "/") != tokens.end()) {
        // Deterministic form: standalone "/" separates the filenames; the
        // tokens between separators form one name.  No existence checks.
        strings_t group {};
        for (const std::string& t : tokens) {
            if (t == "/") {
                if (!group.empty()) result.push_back(join(group));
                group.clear();
            } else {
                group.push_back(t);
            }
        }
        if (!group.empty()) result.push_back(join(group));
    } else {
        // Rescue: a token naming an existing file stands alone; one that
        // does not is joined with following tokens until the accumulated
        // name exists.  A name that never resolves is kept as given, so the
        // missing-file error downstream reports what the user wrote.
        size_t i = 0;
        while (i < tokens.size()) {
            if (filename_exists(tokens[i], base_dir)) {
                result.push_back(tokens[i]);
                ++i;
                continue;
            }
            std::string acc = tokens[i];
            size_t j = i + 1;
            bool found = false;
            while (j < tokens.size()) {
                acc += " " + tokens[j];
                ++j;
                if (filename_exists(acc, base_dir)) {
                    // A successful regrouping is a DERIVED value -- the
                    // command completed a name the input left ambiguous -- so
                    // it is reported at "-v 1" and not at every run.  Silence
                    // is safe here because a rescue only succeeds when the
                    // joined name names an existing file: a mistyped name
                    // fails rather than resolving to something else.
                    K::log(1, "interpreting", q_(acc), "as one filename");
                    result.push_back(acc);
                    i = j;
                    found = true;
                    break;
                }
            }
            if (!found) {
                // Nothing resolved: the token passes through as written and
                // its DOWNSTREAM OWNER decides -- a bare :files word is the
                // kt/ shortcut (which resolves or errors there), a missing
                // file is a located error.  A failed rescue itself says
                // NOTHING at any verbosity (2026-08-22): every outcome is
                // already either the -v 1 resolution line or an error that
                // stops the run, so a tried-list adds noise to the first and
                // nothing to the second.
                result.push_back(tokens[i]);
                ++i;
            }
        }
    }
    for (std::string& name : result) {
        name = expand_tilde(name);
    }
    return result;
}

strings_t resolve_filename_list(const std::string& text, const std::string& base_dir)
{
    // Split on standalone "/" at the string level first, so a name's inner
    // spacing survives exactly; without a separator, fall back to
    // whitespace tokens and the rescue in group_filename_tokens().
    auto standalone_slash = [&](size_t i) {
        return text[i] == '/'
            && (i == 0 || std::isspace(static_cast<unsigned char>(text[i-1])))
            && (i + 1 == text.size() || std::isspace(static_cast<unsigned char>(text[i+1])));
    };
    bool has_separator = false;
    for (size_t i = 0; i < text.size(); ++i) {
        if (standalone_slash(i)) {
            has_separator = true;
            break;
        }
    }
    if (has_separator) {
        strings_t result {};
        std::string part {};
        for (size_t i = 0; i < text.size(); ++i) {
            if (standalone_slash(i)) {
                part = trim(part);
                if (!part.empty()) result.push_back(expand_tilde(part));
                part.clear();
            } else {
                part += text[i];
            }
        }
        part = trim(part);
        if (!part.empty()) result.push_back(expand_tilde(part));
        return result;
    }
    return group_filename_tokens(word_split(text), base_dir);
}

std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace)
{
    std::regex klammertext_filename_re { R"(.*\.kt?)" };

    fs::path p(pathname);
    std::string result {};
    if (fs::exists(p)) {
        if (fs::is_regular_file(p)) {
            std::ifstream stream { pathname };
            if (!stream.is_open()) {
                throw File_error("Error opening file \"" + pathname + "\"", Locator("", -1, -1));
            } else {
                std::ostringstream buffer {};
                stream >> std::noskipws >> buffer.rdbuf();
                if (stream.fail() && !stream.eof()) {
                    throw File_error("Error reading file \"" + pathname + "\"", Locator("", -1, -1));
                } else {
                result = buffer.str();

                result = string_replace(result, "\r\n", ""); // Urgh.


                if (std::regex_match(pathname, klammertext_filename_re)) {
                    //std::cout << "Read Klammertext source file: " << pathname << "\n";
                    //   result = encode(result);
                } else {
                    //std::cout << "Read file: " << pathname << "\n";        
                }    

                if (strip_surrounding_whitespace)
                    result = trim(result);
                return result;
                }
            }
        } else {
            throw File_error("File \"" + pathname + "\" is not a regular text file", Locator("", -1, -1));
        }
    } else {
        throw File_error("File '" + pathname + "' does not exist", Locator("", -1, -1));
    }
    return result;
}


void string_to_file(const std::string& pathname, std::string contents)
{
    fs::create_directories(file_directory(fs::absolute(pathname)));
    std::ofstream out(pathname);
    if (!out) {
        throw File_error("Could not write file " + pathname);
    }
    out << contents;
    out.close();
}


strings_t get_subdirectories(const std::string& s, std::regex name_match_re)
{
    strings_t result {};
    for (auto& p : fs::recursive_directory_iterator(s)) {
        std::smatch match {};
        //std::cout << "Check dir: " << p.path().string() << "\n";
        std::string base = file_basename(p.path().string());
        if (fs::is_directory(p) and std::regex_match(base, match, name_match_re))
            result.push_back(p.path().string());
    }
    return result;
}

strings_t get_files_in_directory(const std::string& dir)
{
    strings_t result {};
    try {
        for (const auto& entry : fs::directory_iterator(dir)) {
            if (entry.is_regular_file()) {
                // std::cout << entry.path().filename() << std::endl;
                result.push_back(entry.path().filename());
            }
        }
    } catch (const fs::filesystem_error& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
    }
    return result;
}


std::string find_file(const std::string& basename, strings_t search_path, bool error_if_not_found)
{
    bool found = false;
    std::string pathname { "" };
    for (const std::string& s : search_path) {
        pathname = s + "/" + basename;
        if (file_exists(pathname)) {
            found = true;
            break;
        }
    }
    if (error_if_not_found and not found) {
        std::stringstream ss {};
        std::sort(search_path.begin(), search_path.end());
        ss << "File with basename \"" << basename << "\" not found in search path:\n    "
           << join(search_path, "\n    ");
        throw File_error(ss.str(), Locator());
    }
    return pathname;
}


std::vector<fs::path>
find_file_recursive(const fs::path& root, const std::string& filename, bool only_one) //, Locator loc)
{
    std::vector<fs::path> result {};
    if (!fs::exists(root) || !fs::is_directory(root)) {
        return result;
    }
    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        // msg() << "Entry: " << entry.path().string() << "\n";
        if (entry.is_regular_file() && entry.path().filename() == filename) {
            result.push_back(entry.path());
        }
    }
    if (only_one && result.size() > 1) {
        std::stringstream ss {};
        ss << "More than one file named " + q_(filename) + " found:\n";
        for (auto f : result) {
            ss << "  " << f << "\n";
        }
        std::cerr << ss.str();
        throw File_error("More than one file named " + q_(filename) + " found");
    }
    return result;
}


std::vector<fs::path>
find_file_from_roots(const std::vector<std::string>& roots, const std::string& filename, bool only_one)
{
    (void)K::log(3, filename);
    std::vector<fs::path> result {};
    for (std::string root : roots) {
        std::vector<fs::path> filenames = find_file_recursive(root, filename, only_one);
        for (auto f : filenames) {
            if (std::ranges::count(result, f) == 0) {
                result.push_back(f);
            }
        }
        // result.insert(result.end(), filenames.begin(), filenames.end());
    }
    if (result.empty()) {
        throw File_error("File \"" + filename + "\" not found");
    }
    if (only_one && result.size() > 1) {
        std::stringstream ss {};
        ss << "More than one file named " + q_(filename) + " found:\n";
        for (auto f : result) {
            ss << "  " << f << "\n";
        }
        std::cerr << ss.str();
        throw File_error("More than one file named " + q_(filename) + " found");
    }
    for (auto f : result) {
        if (!fs::exists(f)) {
            throw File_error("File \"" + filename + "\" does not exist");
        }
    }
    return result;
}

fs::path klammertext_filename(const std::string& basename, bool error_if_missing, bool make_directory_if_missing)
{
    std::string home { get_env_var("KLAMMERTEXT_HOME") };
    std::string result = home + "/" + basename;
    if (make_directory_if_missing)
        fs::create_directory(file_directory(result));
    if (error_if_missing and !file_exists(result, false, true)) {
        throw File_error(
            "Klammertext file does not exist: " + result);
    }
    return fs::path(result);
}

strings_t sks_dirs()
{
    std::string home { get_env_var("KLAMMERTEXT_HOME") };
    strings_t result = get_subdirectories(home + "/sks", std::regex(R"([a-zA-Z]\w*)"));
    result.emplace(result.begin(), home + "/sks/kutil");
    // result.push_back(home + "/doc/handbook");  // Not included in container yet
    return result;
}

strings_t get_sks_directories(const std::string& s, bool include_argument)
{
    strings_t result;
    if (include_argument)
        result.push_back(s);
    for (auto& p : fs::recursive_directory_iterator(s)) {
        auto basename = file_basename(p.path().string());
        auto parent = p.path().parent_path();
        if (fs::is_directory(p)
            and basename[0] != '_'
            and basename != "css"
            and basename != "sty"
            and basename != "js"
            //and basename != "font"
            and parent != "font"
            and parent != "fonts")
            result.push_back(p.path().string());
    }
    return result;
}

std::string cache_directory(const std::string& subdirectory, std::string parent_directory)
{
    if (parent_directory.empty()) {
        // /dev/shm is a fast RAM-backed tmpfs on Linux; it does not exist on
        // macOS, so fall back to the platform temp directory there.
        if (fs::is_directory("/dev/shm")) {
            parent_directory = "/dev/shm";
        } else {
            parent_directory = fs::temp_directory_path().string();
        }
    }
    std::string result = parent_directory  + "/_klammertext_cache/" + subdirectory;
    // msg() << "Cache directory: " << result << "\n";
    return result;
}


std::time_t to_time_t(const fs::file_time_type& ftime)
{
    auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
        ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
    return std::chrono::system_clock::to_time_t(sctp);
}

bool in_modification_order(std::string filename1, std::string filename2)
{
    if ((!file_exists(filename1)) || (!file_exists(filename2))) {
        return false;
    } else {
        auto time1 = fs::last_write_time(fs::path(filename1));
        auto time2 = fs::last_write_time(fs::path(filename2));
        return time1 < time2;
    }
}

void write_to_cache(const std::string& cache_dir, const std::string& basename, const std::string& text)
{
    if (!file_exists(cache_dir)) {
        //std::cout << "Creating cache directory: " << cache_dir << "\n";
        fs::create_directories(cache_dir);
    }
    // msg() << "Writing file to cache: " << basename << "\n";
    std::string output_filename = cache_dir + "/" + basename;
    string_to_file(output_filename, text);
}

std::string read_from_cache(const std::string& cache_dir, const std::string& basename)
{
    std::string input_filename = cache_dir + "/" + basename;
    // msg() << "Reading file from cache: " << input_filename << "\n";
    return string_from_file(input_filename);
}

bool cache_requires_update(const std::string& cache_dir, const std::string& file_to_cache, const std::string& basename)
{
    std::string cache_filename = cache_dir + "/" + basename;
    return !in_modification_order(file_to_cache, cache_filename);
}
        

std::vector<fs::path> pathnames_with_extension(
    const fs::path& dir, const std::string extension)
{
    std::vector<fs::path> files;
    for (const auto& entry : fs::recursive_directory_iterator(dir)) {
        if (!entry.is_regular_file()) continue;
        auto ext = entry.path().extension().string();
        std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
        if (ext == "." + extension) files.push_back(entry.path());
    }
    return files;
}


/*
std::string find_file(const fs::path& root, const std::string& name)
{
    //std::vector<fs::path> matches;
    strings_t matches {};
  
    auto normalize = [&](std::string s) {
        if (!case_insensitive) return s;
        std::transform(s.begin(), s.end(), s.begin(),
                       [](unsigned char c) { return std::tolower(c); });
        return s;
    };

    const std::string norm_ext = normalize("." + ext);
  
    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        if (!entry.is_regular_file())
            continue;
        //std::string entry_ext = normalize(entry.path().extension().string());
        if (entry == name) {
            matches.push_back(entry.path().string());
        }
    }
    return matches[0];
}
*/


// std::vector<fs::path> find_files_with_extension(
strings_t find_files_with_extension(
    const fs::path& root, const std::string& ext, bool case_insensitive)
{
    //std::vector<fs::path> matches;
    strings_t matches {};

    auto normalize = [&](std::string s) {
        if (!case_insensitive) return s;
        std::transform(s.begin(), s.end(), s.begin(),
                       [](unsigned char c) { return std::tolower(c); });
        return s;
    };

    const std::string norm_ext = normalize("." + ext);

    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        if (!entry.is_regular_file())
            continue;
        std::string entry_ext = normalize(entry.path().extension().string());
        if (entry_ext == norm_ext)
            matches.push_back(entry.path().string());
    }

    return matches;
}

std::string combine_files(
    const std::vector<std::string>& filenames,
    const std::string& prolog, const std::string& epilog,
    const std::function <std::string(std::string)>& processor)
{
    std::string result = prolog;
    for (const std::string& f : filenames) {
        result += "\n/* " + file_basename(f) + " */\n";
        result += string_from_file(f);
    }
    result += "\n" + epilog + "\n";
    if (processor) {
        result = processor(result);
    }
    return result;
}

bool files_differ(const fs::path& p1,
                  const fs::path& p2,
                  std::size_t buffer_size)   // 64 KiB
{
    // 1. Check existence and type
    if (!fs::exists(p1) || !fs::exists(p2)) return true;
    if (!fs::is_regular_file(p1) || !fs::is_regular_file(p2)) return true;

    // 2. Compare sizes
    auto s1 = fs::file_size(p1);
    auto s2 = fs::file_size(p2);
    if (s1 != s2) return true;

    // 3. Open both files in binary mode
    std::ifstream f1(p1, std::ios::binary);
    std::ifstream f2(p2, std::ios::binary);
    if (!f1 || !f2) return true;  // treat I/O error as "different"

    // 4. Compare in chunks
    std::vector<char> buf1(buffer_size);
    std::vector<char> buf2(buffer_size);

    while (f1 && f2) {
        f1.read(buf1.data(), buffer_size);
        f2.read(buf2.data(), buffer_size);

        std::streamsize r1 = f1.gcount();
        std::streamsize r2 = f2.gcount();
        if (r1 != r2) return true;  // should not happen if sizes equal

        if (r1 == 0) break;         // EOF both

        if (std::memcmp(buf1.data(), buf2.data(), static_cast<std::size_t>(r1)) != 0)
            return true;
    }
    return false;  // no difference found
}

// Copy a single file with an explicit binary read/write stream, forcing the
// destination to be world-readable.  Deliberately NOT std::filesystem::copy_file
// or fs::copy: under Apple's `container` runtime the HTML output directory is a
// virtiofs bind mount, and libstdc++'s copy_file/copy create the destination
// with openat(O_WRONLY|O_CREAT|O_TRUNC, 0200) — a mode lacking the owner-read
// bit, which virtiofs rejects with EACCES (apple/container #1344, an OS-level
// Virtualization.framework bug), leaving a 0-byte --w------- file and aborting
// output.  A stream copy creates the destination owner-readable and works
// identically on virtiofs, on in-VM filesystems, and under Docker.  Used for
// every file Klammertext writes into the output tree (fonts, CSS, JS, ...).
void copy_file_stream(const fs::path& src, const fs::path& dst)
{
    {
        std::ifstream in(src, std::ios::binary);
        if (!in)
            throw File_error("Cannot read file for copy:\n  " + src.string());

        std::ofstream out(dst, std::ios::binary | std::ios::trunc);
        if (!out)
            throw File_error("Cannot create output file:\n  " + dst.string());

        // Guard against the empty-source failbit quirk of rdbuf insertion.
        if (in.peek() != std::ifstream::traits_type::eof())
            out << in.rdbuf();
        out.flush();

        if (!out || in.bad())
            throw File_error("Failed to copy file:\n  " + src.string()
                             + "\n  -> " + dst.string());
    }

    fs::permissions(dst,
        fs::perms::owner_read | fs::perms::owner_write |
        fs::perms::group_read | fs::perms::others_read,
        fs::perm_options::replace);
}

void copy_preserving_basename(
    const strings_t& filenames, const std::string& output_directory,
    const std::string& link_directory)
{
    fs::path outdir(output_directory + "/" + link_directory);
    fs::create_directories(outdir);
    for (const std::string& filename : filenames) {
        fs::path pname(filename);
        auto out_path = outdir / pname.filename();
        // Preserve the previous copy_options::update_existing behavior: skip
        // when the destination already exists and is no older than the source.
        if (fs::exists(out_path) &&
            fs::last_write_time(out_path) >= fs::last_write_time(pname))
            continue;
        copy_file_stream(pname, out_path);
    }
}

 fs::path resolve_relative_to(const fs::path& relative, const fs::path& base)
 {
      fs::path base_dir = is_directory(base) ? base : base.parent_path(); 
      return fs::weakly_canonical(base_dir / relative);
 }
