/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "paimon/fs/local/local_file_system.h"

#include <fcntl.h>

#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <utility>

#include "fmt/format.h"
#include "paimon/common/utils/path_util.h"
#include "paimon/fs/local/local_file_status.h"

namespace paimon {

Result<bool> LocalFileSystem::Exists(const std::string& path) const {
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    return file->Exists();
}

Result<std::unique_ptr<InputStream>> LocalFileSystem::Open(const std::string& path) const {
    PAIMON_ASSIGN_OR_RAISE(bool is_exist, Exists(path));
    if (!is_exist) {
        return Status::NotExist(fmt::format("File '{}' not exists", path));
    }
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalInputStream> in,
                           LocalInputStream::Create(std::move(file)));
    return in;
}

Result<std::unique_ptr<OutputStream>> LocalFileSystem::Create(const std::string& path,
                                                              bool overwrite) const {
    PAIMON_ASSIGN_OR_RAISE(bool is_exist, Exists(path));
    if (is_exist && !overwrite) {
        return Status::Invalid(
            fmt::format("do not allow overwrite, but the file {} already exists", path));
    }
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    std::unique_ptr<LocalFile> parent = file->GetParentFile();
    PAIMON_RETURN_NOT_OK(Mkdirs(parent->GetPath()));
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalOutputStream> out,
                           LocalOutputStream::Create(std::move(file)));
    return out;
}

Status LocalFileSystem::Mkdirs(const std::string& path) const {
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    return MkdirsInternal(std::move(file));
}

Status LocalFileSystem::MkdirsInternal(std::unique_ptr<LocalFile>&& file) const {
    // Important: The 'Exists()' check above must come before the 'IsDir()'
    // check to be safe when multiple parallel instances try to create the directory
    PAIMON_ASSIGN_OR_RAISE(bool is_exist, file->Exists());
    if (is_exist) {
        PAIMON_ASSIGN_OR_RAISE(bool is_dir, file->IsDir());
        if (is_dir) {
            return Status::OK();
        } else {
            // exists and is not a directory -> is a regular file
            return Status::IOError(
                fmt::format("file {} already exists and is not a directory", file->GetPath()));
        }
    }

    std::unique_ptr<LocalFile> parent = file->GetParentFile();
    if (!parent->IsEmpty()) {
        PAIMON_RETURN_NOT_OK(MkdirsInternal(std::move(parent)));
    }
    PAIMON_ASSIGN_OR_RAISE(bool success, file->Mkdir());
    if (!success) {
        PAIMON_ASSIGN_OR_RAISE(bool is_dir, file->IsDir());
        if (is_dir) {
            return Status::OK();
        } else {
            return Status::IOError(fmt::format("create directory '{}' failed", file->GetPath()));
        }
    }
    return Status::OK();
}

Result<std::unique_ptr<FileStatus>> LocalFileSystem::GetFileStatus(const std::string& path) const {
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    PAIMON_ASSIGN_OR_RAISE(bool is_exist, file->Exists());
    if (is_exist) {
        return file->GetFileStatus();
    } else {
        return Status::NotExist(
            fmt::format("File {} does not exist or the user running "
                        "Paimon has insufficient permissions to access it.",
                        file->GetPath()));
    }
}

Status LocalFileSystem::ListDir(
    const std::string& directory,
    std::vector<std::unique_ptr<BasicFileStatus>>* file_status_list) const {
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(directory));
    PAIMON_ASSIGN_OR_RAISE(bool is_exist, file->Exists());
    if (!is_exist) {
        return Status::OK();
    }
    PAIMON_ASSIGN_OR_RAISE(bool is_file, file->IsFile());
    if (is_file) {
        return Status::IOError(
            fmt::format("file {} already exists and is not a directory", file->GetPath()));
    } else {
        std::vector<std::string> file_list;
        PAIMON_RETURN_NOT_OK(file->List(&file_list));
        file_status_list->reserve(file_status_list->size() + file_list.size());
        for (const auto& f : file_list) {
            Result<std::unique_ptr<FileStatus>> file_status =
                GetFileStatus(PathUtil::JoinPath(directory, f));
            if (!file_status.ok() && !file_status.status().IsNotExist()) {
                return file_status.status();
            } else if (file_status.ok()) {
                file_status_list->emplace_back(std::make_unique<LocalBasicFileStatus>(
                    file_status.value()->GetPath(), file_status.value()->IsDir()));
            }
        }
        return Status::OK();
    }
}

Status LocalFileSystem::ListFileStatus(
    const std::string& path, std::vector<std::unique_ptr<FileStatus>>* file_status_list) const {
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    PAIMON_ASSIGN_OR_RAISE(bool is_exist, file->Exists());
    if (!is_exist) {
        return Status::OK();
    }
    PAIMON_ASSIGN_OR_RAISE(bool is_file, file->IsFile());
    if (is_file) {
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStatus> file_status, file->GetFileStatus());
        file_status_list->emplace_back(std::move(file_status));
    } else {
        std::vector<std::string> file_list;
        PAIMON_RETURN_NOT_OK(file->List(&file_list));
        file_status_list->reserve(file_status_list->size() + file_list.size());
        for (const auto& f : file_list) {
            Result<std::unique_ptr<FileStatus>> file_status =
                GetFileStatus(PathUtil::JoinPath(path, f));
            if (!file_status.ok() && !file_status.status().IsNotExist()) {
                return file_status.status();
            } else if (file_status.ok()) {
                file_status_list->emplace_back(std::move(file_status).value());
            }
        }
    }
    return Status::OK();
}

Status LocalFileSystem::Delete(const std::string& path, bool recursive) const {
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> file, LocalFile::Create(path));
    PAIMON_ASSIGN_OR_RAISE(bool is_file, file->IsFile());
    if (is_file) {
        return file->Delete();
    }
    return Delete(std::move(file), recursive);
}

Status LocalFileSystem::Delete(std::unique_ptr<LocalFile>&& file, bool recursive) const {
    PAIMON_ASSIGN_OR_RAISE(bool is_dir, file->IsDir());
    if (is_dir) {
        std::vector<std::unique_ptr<LocalFile>> files;
        PAIMON_RETURN_NOT_OK(file->ListFiles(&files));
        if (recursive == false && !files.empty()) {
            return Status::IOError(
                fmt::format("cannot delete {}, directory is not empty", file->GetPath()));
        }
        for (auto& child : files) {
            PAIMON_RETURN_NOT_OK(Delete(std::move(child)));
        }
    }
    // Now directory is empty
    return file->Delete();
}

Status LocalFileSystem::Rename(const std::string& src, const std::string& dst) const {
    std::string err_msg = fmt::format("rename '{}' to '{}' failed, because: ", src, dst);
    PAIMON_ASSIGN_OR_RAISE(bool is_src_exist, Exists(src));
    if (!is_src_exist) {
        return Status::NotExist(err_msg, "src file not exist");
    }
    PAIMON_ASSIGN_OR_RAISE(bool is_dst_exist, Exists(dst));
    if (is_dst_exist) {
        return Status::Invalid(err_msg, "dst file already exist");
    }
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> src_file, LocalFile::Create(src));
    PAIMON_ASSIGN_OR_RAISE(bool is_file, src_file->IsFile());
    std::string new_file_name = dst;

    if (is_file && new_file_name[new_file_name.length() - 1] == '/') {
        return Status::Invalid(err_msg, "src file is not a dir");
    }
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<LocalFile> dst_file, LocalFile::Create(dst));
    std::unique_ptr<LocalFile> parent = dst_file->GetParentFile();
    PAIMON_RETURN_NOT_OK(Mkdirs(parent->GetPath()));
    if (::rename(src_file->GetPath().c_str(), dst_file->GetPath().c_str()) != 0) {
        int32_t cur_errno = errno;
        return Status::IOError(err_msg, std::strerror(cur_errno));
    }
    return Status::OK();
}

// input stream
Result<std::unique_ptr<LocalInputStream>> LocalInputStream::Create(
    std::unique_ptr<LocalFile> file) {
    PAIMON_RETURN_NOT_OK(file->OpenFile(/*is_read_file=*/true));
    return std::unique_ptr<LocalInputStream>(new LocalInputStream(std::move(file)));
}

LocalInputStream::LocalInputStream(std::unique_ptr<LocalFile>&& file) : file_(std::move(file)) {}

Status LocalInputStream::Seek(int64_t offset, SeekOrigin origin) {
    if (origin != FS_SEEK_SET && origin != FS_SEEK_CUR && origin != FS_SEEK_END) {
        return Status::Invalid(
            "invalid SeekOrigin, only support FS_SEEK_SET, FS_SEEK_CUR, and FS_SEEK_END");
    }
    auto convert_origin = [](SeekOrigin origin) -> int32_t {
        switch (origin) {
            case FS_SEEK_SET:
                return SEEK_SET;
            case FS_SEEK_CUR:
                return SEEK_CUR;
            case FS_SEEK_END:
                return SEEK_END;
            default:
                return SEEK_SET;
        }
    };
    return file_->Seek(offset, convert_origin(origin));
}

Result<int64_t> LocalInputStream::GetPos() const {
    return file_->Tell();
}

Result<int64_t> LocalInputStream::Read(char* buffer, int64_t size) {
    PAIMON_ASSIGN_OR_RAISE(int64_t read_length, file_->Read(buffer, size));
    if (read_length != size) {
        return Status::IOError(fmt::format("file '{}' read size {} != expected {}",
                                           file_->GetPath(), read_length, size));
    }
    return read_length;
}

Result<int64_t> LocalInputStream::Read(char* buffer, int64_t size, int64_t offset) {
    PAIMON_ASSIGN_OR_RAISE(int64_t read_length, file_->Read(buffer, size, offset));
    if (read_length != size) {
        return Status::IOError(fmt::format("file '{}' read size {} != expected {}",
                                           file_->GetPath(), read_length, size));
    }
    return read_length;
}

void LocalInputStream::ReadAsync(char* buffer, int64_t size, int64_t offset,
                                 std::function<void(Status)>&& callback) {
    Result<int64_t> read_size = Read(buffer, size, offset);
    Status status = Status::OK();
    if (!read_size.ok()) {
        status = read_size.status();
    } else {
        assert(read_size.value() == size);
    }
    callback(status);
}

Result<int64_t> LocalInputStream::Length() const {
    return file_->Length();
}

Status LocalInputStream::Close() {
    return file_->Close();
}

// output stream
Result<std::unique_ptr<LocalOutputStream>> LocalOutputStream::Create(
    std::unique_ptr<LocalFile> file) {
    PAIMON_RETURN_NOT_OK(file->OpenFile(/*is_read_file=*/false));
    return std::unique_ptr<LocalOutputStream>(new LocalOutputStream(std::move(file)));
}

LocalOutputStream::LocalOutputStream(std::unique_ptr<LocalFile>&& file) : file_(std::move(file)) {}

Result<int64_t> LocalOutputStream::GetPos() const {
    return file_->Tell();
}
Result<int64_t> LocalOutputStream::Write(const char* buffer, int64_t size) {
    return file_->Write(buffer, size);
}
Status LocalOutputStream::Flush() {
    return file_->Flush();
}
Status LocalOutputStream::Close() {
    return file_->Close();
}

}  // namespace paimon
