#include "dirut.h"
#include "path.h"
#include "pathsplit.h"

#include <util/generic/yexception.h>
#include <util/string/cast.h>
#include <util/string/escape.h>
#include <util/system/compiler.h>
#include <util/system/file.h>
#include <util/system/fs.h>

struct TFsPath::TSplit: public TAtomicRefCount<TSplit>, public TPathSplit {
    inline TSplit(const TStringBuf path)
        : TPathSplit(path)
    {
    }
    inline TSplit(const TString& path, const TSimpleIntrusivePtr<TSplit>& thatSplit, const TString::char_type* thatPathData) {
        for (const auto& thatPart : *thatSplit) {
            emplace_back(path.data() + (thatPart.data() - thatPathData), thatPart.size());
        }

        if (!thatSplit->Drive.empty()) {
            Drive = TStringBuf(path.data() + (thatSplit->Drive.data() - thatPathData), thatSplit->Drive.size());
        }

        IsAbsolute = thatSplit->IsAbsolute;
    }
};

void TFsPath::CheckDefined() const {
    if (!IsDefined()) {
        ythrow TIoException() << TStringBuf("must be defined");
    }
}

bool TFsPath::IsSubpathOf(const TFsPath& that) const {
    const TSplit& split = GetSplit();
    const TSplit& rsplit = that.GetSplit();

    if (rsplit.IsAbsolute != split.IsAbsolute) {
        return false;
    }

    if (rsplit.Drive != split.Drive) {
        return false;
    }

    if (rsplit.size() >= split.size()) {
        return false;
    }

    return std::equal(rsplit.begin(), rsplit.end(), split.begin());
}

bool TFsPath::IsNonStrictSubpathOf(const TFsPath& that) const {
    const TSplit& split = GetSplit();
    const TSplit& rsplit = that.GetSplit();

    if (rsplit.IsAbsolute != split.IsAbsolute) {
        return false;
    }

    if (rsplit.Drive != split.Drive) {
        return false;
    }

    if (rsplit.size() > split.size()) {
        return false;
    }

    return std::equal(rsplit.begin(), rsplit.end(), split.begin());
}

TFsPath TFsPath::RelativeTo(const TFsPath& root) const {
    TSplit split = GetSplit();
    const TSplit& rsplit = root.GetSplit();

    if (split.Reconstruct() == rsplit.Reconstruct()) {
        return TFsPath();
    }

    if (!this->IsSubpathOf(root)) {
        ythrow TIoException() << "path " << *this << " is not subpath of " << root;
    }

    split.erase(split.begin(), split.begin() + rsplit.size());
    split.IsAbsolute = false;

    return TFsPath(split.Reconstruct());
}

TFsPath TFsPath::RelativePath(const TFsPath& root) const {
    TSplit split = GetSplit();
    const TSplit& rsplit = root.GetSplit();
    size_t cnt = 0;

    while (split.size() > cnt && rsplit.size() > cnt && split[cnt] == rsplit[cnt]) {
        ++cnt;
    }
    bool absboth = split.IsAbsolute && rsplit.IsAbsolute;
    if (cnt == 0 && !absboth) {
        ythrow TIoException() << "No common parts in " << *this << " and " << root;
    }
    TString r;
    for (size_t i = 0; i < rsplit.size() - cnt; i++) {
        r += i == 0 ? ".." : "/..";
    }
    for (size_t i = cnt; i < split.size(); i++) {
        r += (i == 0 || i == cnt && rsplit.size() - cnt == 0 ? "" : "/");
        r += split[i];
    }
    return r.size() ? TFsPath(r) : TFsPath();
}

TFsPath TFsPath::Parent() const {
    if (!IsDefined()) {
        return TFsPath();
    }

    TSplit split = GetSplit();
    if (split.size()) {
        split.pop_back();
    }
    if (!split.size() && !split.IsAbsolute) {
        return TFsPath(".");
    }
    return TFsPath(split.Reconstruct());
}

TFsPath& TFsPath::operator/=(const TFsPath& that) {
    if (!IsDefined()) {
        *this = that;

    } else if (that.IsDefined() && that.GetPath() != ".") {
        if (!that.IsRelative()) {
            ythrow TIoException() << "path should be relative: " << that.GetPath();
        }

        TSplit split = GetSplit();
        const TSplit& rsplit = that.GetSplit();
        split.insert(split.end(), rsplit.begin(), rsplit.end());
        *this = TFsPath(split.Reconstruct());
    }
    return *this;
}

TFsPath& TFsPath::Fix() {
    // just normalize via reconstruction
    TFsPath(GetSplit().Reconstruct()).Swap(*this);

    return *this;
}

TString TFsPath::GetName() const {
    if (!IsDefined()) {
        return TString();
    }

    const TSplit& split = GetSplit();

    if (split.size() > 0) {
        if (split.back() != "..") {
            return TString(split.back());
        } else {
            // cannot just drop last component, because path itself may be a symlink
            return RealPath().GetName();
        }
    } else {
        if (split.IsAbsolute) {
            return split.Reconstruct();
        } else {
            return Cwd().GetName();
        }
    }
}

TString TFsPath::GetExtension() const {
    return TString(GetSplit().Extension());
}

bool TFsPath::IsAbsolute() const {
    return GetSplit().IsAbsolute;
}

bool TFsPath::IsRelative() const {
    return !IsAbsolute();
}

void TFsPath::InitSplit() const {
    Split_ = new TSplit(Path_);
}

TFsPath::TSplit& TFsPath::GetSplit() const {
    // XXX: race condition here
    if (!Split_) {
        InitSplit();
    }
    return *Split_;
}

static Y_FORCE_INLINE void VerifyPath(const TStringBuf path) {
    Y_ABORT_UNLESS(!path.Contains('\0'), "wrong format of TFsPath: %s", EscapeC(path).c_str());
}

TFsPath::TFsPath() {
}

TFsPath::TFsPath(const TString& path)
    : Path_(path)
{
    VerifyPath(Path_);
}

TFsPath::TFsPath(const TStringBuf path)
    : Path_(ToString(path))
{
    VerifyPath(Path_);
}

TFsPath::TFsPath(const char* path)
    : Path_(path)
{
}

TFsPath::TFsPath(const TFsPath& that) {
    *this = that;
}

TFsPath::TFsPath(TFsPath&& that) {
    *this = std::move(that);
}

TFsPath& TFsPath::operator=(const TFsPath& that) {
    Path_ = that.Path_;
    if (that.Split_) {
        Split_ = new TSplit(Path_, that.Split_, that.Path_.begin());
    } else {
        Split_ = nullptr;
    }
    return *this;
}

TFsPath& TFsPath::operator=(TFsPath&& that) {
#ifdef TSTRING_IS_STD_STRING
    const auto thatPathData = that.Path_.data();
    Path_ = std::move(that.Path_);
    if (that.Split_) {
        if (Path_.data() == thatPathData) { // Path_ moved,  can move Split_
            Split_ = std::move(that.Split_);
        } else { // Path_ copied, rebuild Split_ using that.Split_
            Split_ = new TSplit(Path_, that.Split_, that.Path_.data());
        }
    } else {
        Split_ = nullptr;
    }
#else
    Path_ = std::move(that.Path_);
    Split_ = std::move(that.Split_);
#endif
    return *this;
}

TFsPath TFsPath::Child(const TString& name) const {
    if (!name) {
        ythrow TIoException() << "child name must not be empty";
    }

    return *this / name;
}

struct TClosedir {
    static void Destroy(DIR* dir) {
        if (dir) {
            if (0 != closedir(dir)) {
                ythrow TIoSystemError() << "failed to closedir";
            }
        }
    }
};

void TFsPath::ListNames(TVector<TString>& children) const {
    CheckDefined();
    THolder<DIR, TClosedir> dir(opendir(this->c_str()));
    if (!dir) {
        ythrow TIoSystemError() << "failed to opendir " << Path_;
    }

    for (;;) {
        struct dirent de;
        struct dirent* ok;
        // TODO(yazevnul|IGNIETFERRO-1070): remove these macroses by replacing `readdir_r` with proper
        // alternative
        Y_PRAGMA_DIAGNOSTIC_PUSH
        Y_PRAGMA_NO_DEPRECATED
        int r = readdir_r(dir.Get(), &de, &ok);
        Y_PRAGMA_DIAGNOSTIC_POP
        if (r != 0) {
            ythrow TIoSystemError() << "failed to readdir " << Path_;
        }
        if (ok == nullptr) {
            return;
        }
        TString name(de.d_name);
        if (name == "." || name == "..") {
            continue;
        }
        children.push_back(name);
    }
}

bool TFsPath::Contains(const TString& component) const {
    if (!IsDefined()) {
        return false;
    }

    TFsPath path = *this;
    while (path.Parent() != path) {
        if (path.GetName() == component) {
            return true;
        }

        path = path.Parent();
    }

    return false;
}

void TFsPath::List(TVector<TFsPath>& files) const {
    TVector<TString> names;
    ListNames(names);
    for (auto& name : names) {
        files.push_back(Child(name));
    }
}

void TFsPath::RenameTo(const TString& newPath) const {
    CheckDefined();
    if (!newPath) {
        ythrow TIoException() << "bad new file name";
    }
    if (!NFs::Rename(Path_, newPath)) {
        ythrow TIoSystemError() << "failed to rename " << Path_ << " to " << newPath;
    }
}

void TFsPath::RenameTo(const char* newPath) const {
    RenameTo(TString(newPath));
}

void TFsPath::RenameTo(const TFsPath& newPath) const {
    RenameTo(newPath.GetPath());
}

void TFsPath::Touch() const {
    CheckDefined();
    if (!TFile(*this, OpenAlways).IsOpen()) {
        ythrow TIoException() << "failed to touch " << *this;
    }
}

// XXX: move implementation to util/somewhere.
TFsPath TFsPath::RealPath() const {
    CheckDefined();
    return ::RealPath(*this);
}

TFsPath TFsPath::RealLocation() const {
    CheckDefined();
    return ::RealLocation(*this);
}

TFsPath TFsPath::ReadLink() const {
    CheckDefined();

    if (!IsSymlink()) {
        ythrow TIoException() << "not a symlink " << *this;
    }

    return NFs::ReadLink(*this);
}

bool TFsPath::Exists() const {
    return IsDefined() && NFs::Exists(*this);
}

void TFsPath::CheckExists() const {
    if (!Exists()) {
        ythrow TIoException() << "path does not exist " << Path_;
    }
}

bool TFsPath::IsDirectory() const {
    return IsDefined() && TFileStat(GetPath().data()).IsDir();
}

bool TFsPath::IsFile() const {
    return IsDefined() && TFileStat(GetPath().data()).IsFile();
}

bool TFsPath::IsSymlink() const {
    return IsDefined() && TFileStat(GetPath().data(), true).IsSymlink();
}

void TFsPath::DeleteIfExists() const {
    if (!IsDefined()) {
        return;
    }

    ::unlink(this->c_str());
    ::rmdir(this->c_str());
    if (Exists()) {
        ythrow TIoException() << "failed to delete " << Path_;
    }
}

void TFsPath::MkDir(const int mode) const {
    CheckDefined();
    if (!Exists()) {
        int r = Mkdir(this->c_str(), mode);
        if (r != 0) {
            // TODO (stanly) will still fail on Windows because
            // LastSystemError() returns windows specific ERROR_ALREADY_EXISTS
            // instead of EEXIST.
            if (LastSystemError() != EEXIST) {
                ythrow TIoSystemError() << "could not create directory " << Path_;
            }
        }
    }
}

void TFsPath::MkDirs(const int mode) const {
    CheckDefined();
    if (!Exists()) {
        Parent().MkDirs(mode);
        MkDir(mode);
    }
}

void TFsPath::ForceDelete() const {
    if (!IsDefined()) {
        return;
    }

    TFileStat stat(GetPath().c_str(), true);
    if (stat.IsNull()) {
        const int err = LastSystemError();
#ifdef _win_
        if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
#else
        if (err == ENOENT) {
#endif
            return;
        } else {
            ythrow TIoException() << "failed to stat " << Path_;
        }
    }

    ClearLastSystemError();
    if (stat.IsDir()) {
        TVector<TFsPath> children;
        List(children);
        for (auto& i : children) {
            i.ForceDelete();
        }
        ::rmdir(this->c_str());
    } else {
        ::unlink(this->c_str());
    }

    if (LastSystemError()) {
        ythrow TIoException() << "failed to delete " << Path_;
    }
}

void TFsPath::CopyTo(const TString& newPath, bool force) const {
    if (IsDirectory()) {
        if (force) {
            TFsPath(newPath).MkDirs();
        } else if (!TFsPath(newPath).IsDirectory()) {
            ythrow TIoException() << "Target path is not a directory " << newPath;
        }
        TVector<TFsPath> children;
        List(children);
        for (auto&& i : children) {
            i.CopyTo(newPath + "/" + i.GetName(), force);
        }
    } else {
        if (force) {
            TFsPath(newPath).Parent().MkDirs();
        } else {
            if (!TFsPath(newPath).Parent().IsDirectory()) {
                ythrow TIoException() << "Parent (" << TFsPath(newPath).Parent() << ") of a target path is not a directory " << newPath;
            }
            if (TFsPath(newPath).Exists()) {
                ythrow TIoException() << "Path already exists " << newPath;
            }
        }
        NFs::Copy(Path_, newPath);
    }
}

void TFsPath::ForceRenameTo(const TString& newPath) const {
    try {
        RenameTo(newPath);
    } catch (const TIoSystemError& /* error */) {
        CopyTo(newPath, true);
        ForceDelete();
    }
}

TFsPath TFsPath::Cwd() {
    return TFsPath(::NFs::CurrentWorkingDirectory());
}

const TPathSplit& TFsPath::PathSplit() const {
    return GetSplit();
}

template <>
void Out<TFsPath>(IOutputStream& os, const TFsPath& f) {
    os << f.GetPath();
}

template <>
TFsPath FromStringImpl<TFsPath>(const char* s, size_t len) {
    return TFsPath{TStringBuf{s, len}};
}

template <>
bool TryFromStringImpl(const char* s, size_t len, TFsPath& result) {
    try {
        result = TStringBuf{s, len};
        return true;
    } catch (std::exception&) {
        return false;
    }
}