#include "local_storage.h"

#include <saas/util/named_lock.h>

#include <library/cpp/logger/global/global.h>

#include <util/folder/pathsplit.h>
#include <util/stream/file.h>
#include <util/string/cast.h>
#include <util/system/event.h>
#include <util/system/mutex.h>
#include <util/system/fs.h>

namespace NSaas {

    IVersionedStorage::TFactory::TRegistrator<TLocalStorage> TLocalStorage::Registrator(IVersionedStorage::TOptions::TStorageType::LOCAL);

    namespace {
        const TString DATA_FILE = "data.";
    }

    TLocalStorage::TNode::TNode(const TLocalStorage& storage, const TFsPath& path)
        : Path(path)
        , Storage(storage)
    {}

    bool TLocalStorage::TNode::RemoveChild(const TString& child) {
        TChildren::iterator i = Children.find(child);
        if (i == Children.end())
            return false;
        if (Storage.GetOptions().LocalOptions.FlushOnWrite)
            i->second->Path.ForceDelete();
        Children.erase(i);
        return true;
    }

    bool TLocalStorage::TNode::GetVersion(i64& version) const {
        if (Data.empty())
            return false;
        version = Version;
        return true;
    }

    void TLocalStorage::TNode::GetChildren(TVector<TString>& result, bool withDirs) const {
        result.clear();
        for (const auto& child : Children)
            if (withDirs || !child.second->Data.empty())
                result.push_back(child.first);
    }

    bool TLocalStorage::TNode::GetValue(TString& result, i64 version) const {
        if (version == -1) {
            if (!Data.empty())
                result = Data.back();
            return !Data.empty();
        }

        if (version >= 0 && version < Data.ysize()) {
            result = Data[version];
            return true;
        }

        if (version == Version && Data.size() == 1) {
            //no stored history
            result = Data[0];
            return true;
        }

        return false;
    }

    void TLocalStorage::TNode::SetValue(const TString& value, bool storeHistory, i64* version = nullptr) {
        if (storeHistory || Data.empty())
            Data.push_back(value);
        else
            Data[0] = value;
        ++Version;
        if (version)
            *version = Version;
        if (Storage.GetOptions().LocalOptions.FlushOnWrite)
            Serialize();
    }

    TLocalStorage::TNode::TPtr TLocalStorage::TNode::GetNearChild(const TStringBuf& name) {
        TChildren::const_iterator i = Children.find(name);
        return (i == Children.end()) ? nullptr : i->second;
    }

    TLocalStorage::TNode::TPtr TLocalStorage::TNode::AddOrGetNearChild(const TStringBuf& name) {
        TPtr& result = Children[TString(name)];
        if (!result)
            result.Reset(new TNode(Storage, Path / name));
        return result;
    }

    TLocalStorage::TNode::TPtr TLocalStorage::TNode::GetChild(const TFsPath& path, TLocalStorage::TNode::TReadLockList* rl, TLocalStorage::TNode::TWriteLockList* wl) {
        Lock(rl, wl);
        TNode::TPtr node = this;
        for (const auto& name : path.PathSplit()) {
            node = node->GetNearChild(name);
            if (!node)
                break;
            node->Lock(rl, wl);
        }
        return node;
    }

    TLocalStorage::TNode::TPtr TLocalStorage::TNode::AddOrGetChild(const TFsPath& path, TLocalStorage::TNode::TReadLockList* rl, TLocalStorage::TNode::TWriteLockList* wl) {
        TNode::TPtr node = this;
        Lock(rl, wl);
        for (const auto& name : path.PathSplit()) {
            node = node->AddOrGetNearChild(name);
            node->Lock(rl, wl);
        }
        return node;
    }

    void TLocalStorage::TNode::Serialize() const {
        Path.MkDirs();
        TFixedBufferFileOutput(Path / "version").Write(&Version, sizeof(Version));
        for (ui32 i = Storage.GetOptions().LocalOptions.FlushOnWrite ? Max(0, Data.ysize() - 1) : 0; i < Data.size(); ++i) {
            TFsPath pathStore = Path / (DATA_FILE + ToString(i));
            {
                TFixedBufferFileOutput fo(pathStore.GetPath() + "~");
                fo.Write(Data[i].data(), Data[i].size());
                fo.Flush();
            }
            NFs::Rename(pathStore.GetPath() + "~", pathStore.GetPath());
        }
        for (const auto& child : Children)
            child.second->Serialize();
    }

    void TLocalStorage::TNode::Deserialize() {
        Data.clear();
        Children.clear();
        if (!Path.Exists())
            return;
        if ((Path / "version").Exists()) {
            TFileInput(Path / "version").LoadOrFail(&Version, sizeof(Version));
            for (ui32 i = 0; true; ++i) {
                TFsPath dataPath = Path / (DATA_FILE + ToString(i));
                if (!dataPath.Exists())
                    break;
                Data.push_back(TFileInput(dataPath).ReadAll());
            }
        }
        TVector<TFsPath> ch;
        Path.List(ch);
        for (const auto& c : ch)
            if (c.IsDirectory())
                AddOrGetNearChild(c.GetName())->Deserialize();
    }

    void TLocalStorage::TNode::Lock(TLocalStorage::TNode::TReadLockList* rl, TLocalStorage::TNode::TWriteLockList* wl) const {
        if (rl)
           rl->push_back(new TReadGuard(Mutex));
        if (wl)
           wl->push_back(new TWriteGuard(Mutex));
    }

    TLocalStorage::TLocalStorage(const TOptions& options)
        : IVersionedStorage(options)
        , RootNode(new TNode(*this, options.LocalOptions.Root))
    {
        RootNode->Deserialize();
    }

    TLocalStorage::~TLocalStorage() {
        if (Options.LocalOptions.FlushOnWrite)
            return;
        Options.LocalOptions.Root.ForceDelete();
        RootNode->Serialize();
    }

    bool TLocalStorage::RemoveNode(const TString& key, bool /*withHistory = false*/) const {
        DEBUG_LOG << "RemoveNode " << key << "..." << Endl;
        TFsPath path = FullPath(key);
        if (!path.GetPath())
            return false;
        bool result = false;
        if (path.GetPath() == "/") {
            TWriteGuard g(RootMutex);
            if (Options.LocalOptions.FlushOnWrite)
                Options.LocalOptions.Root.ForceDelete();
            RootNode = new TNode(*this, Options.LocalOptions.Root);
            result = true;
        } else {
            TReadGuard g(RootMutex);
            TLocalStorage::TNode::TWriteLockList wl;
            TNode::TPtr node = RootNode->GetChild(path.Parent(), nullptr, &wl);
            result = !!node && node->RemoveChild(path.GetName());
        }
        DEBUG_LOG << "RemoveNode " << key << "..." << (result ? "OK" : "FAIL") << Endl;
        return result;
    }

    bool TLocalStorage::ExistsNode(const TString& key) const {
        TReadGuard g(RootMutex);
        TLocalStorage::TNode::TReadLockList rl;
        return !!RootNode->GetChild(FullPath(key), &rl, nullptr);
    }

    bool TLocalStorage::GetVersion(const TString& key, i64& version) const {
        TReadGuard g(RootMutex);
        TLocalStorage::TNode::TReadLockList rl;
        TNode::TPtr node = RootNode->GetChild(FullPath(key), &rl, nullptr);
        return !!node && node->GetVersion(version);
    }

    bool TLocalStorage::GetNodes(const TString& key, TVector<TString>& result, bool withDirs /*= false*/) const {
        DEBUG_LOG << "GetNodes " << key << "..." << Endl;
        TReadGuard g(RootMutex);
        TLocalStorage::TNode::TReadLockList rl;
        TNode::TPtr node = RootNode->GetChild(FullPath(key), &rl, nullptr);
        bool ok = !!node;
        if (ok)
            node->GetChildren(result, withDirs);
        DEBUG_LOG << "GetNodes " << key << "..." << (ok ? "OK" : "FAIL") << Endl;
        return ok;
    }

    bool TLocalStorage::GetValue(const TString& key, TString& result, i64 version /*= -1*/, bool lock /*= true*/) const {
        DEBUG_LOG << "GetValue " << key << "..." << Endl;
        TReadGuard rg(RootMutex);
        TAbstractLock::TPtr g;
        if (lock)
            g = ReadLockNode(key);
        try {
            TLocalStorage::TNode::TReadLockList rl;
            TNode::TPtr node = RootNode->GetChild(FullPath(key), &rl, nullptr);
            bool ok = !!node && node->GetValue(result, version);
            DEBUG_LOG << "GetValue " << key << "..." << (ok ? "OK" : "FAIL") << Endl;
            return ok;
        } catch (...) {
            ERROR_LOG << "error while GetValue of " << key << " v." << version << ":" << CurrentExceptionMessage() << Endl;
        }
        return false;
    }

    bool TLocalStorage::SetValue(const TString& key, const TString& value, bool storeHistory /*= true*/, bool lock /*= true*/, i64* version /*= nullptr*/) const {
        DEBUG_LOG << "SetValue " << key << "..." << Endl;
        TWriteGuard rg(RootMutex);
        TAbstractLock::TPtr g;
        if (lock)
            g = WriteLockNode(key);
        try {
            TLocalStorage::TNode::TWriteLockList wl;
            RootNode->AddOrGetChild(FullPath(key), nullptr, &wl)->SetValue(value, storeHistory, version);
            DEBUG_LOG << "SetValue " << key << "...OK" << Endl;
            return true;
        } catch (...) {
            ERROR_LOG << "error while SetValue of " << key << ":" << CurrentExceptionMessage() << Endl;
        }
        return false;
    }

    bool TLocalStorage::CreatePersistentSequentialNode(const TString& key, const TString& data) const {
        try {
            TFsPath path = FullPath(key);
            TVector<TString> nodes;
            TWriteGuard g(RootMutex);
            TLocalStorage::TNode::TWriteLockList wl;
            TNode::TPtr parent = RootNode->AddOrGetChild(path.Parent(), nullptr, &wl);
            parent->GetChildren(nodes, false);
            i64 version = 0;
            for (const auto& node : nodes)
                if (node.StartsWith(path.GetName()) && node.length() == path.GetName().length() + 10)
                    version = Max(version, FromString<i64>(node.substr(path.GetName().length())) + 1);
            parent->AddOrGetChild(path.GetName() + Sprintf("%010ld", version), nullptr, nullptr)->SetValue(data, false, nullptr);
            return true;
        } catch (...) {
            ERROR_LOG << "Error on node" << key << " construction: " << CurrentExceptionMessage() << Endl;
            return false;
        }
    }

    class TLocalStorage::TLock : public TAbstractLock {
    private:
        class TWaiter : public INamedLockManager::ICallback {
        public:
            TWaiter(TLock* owner)
                : Owner(owner)
            {}

            void OnLock(INamedLockManager::TNamedLockPtr lock) override {
                TGuard<TMutex> g(Mutex);
                if (Owner)
                    Owner->Lock = lock;
                Ev.Signal();
            }

            void Wait(TDuration timeout) {
                Ev.WaitT(timeout);
                TGuard<TMutex> g(Mutex);
                Owner = nullptr;
            }
        private:
            TLock* Owner;
            TManualEvent Ev;
            TMutex Mutex;
        };

        INamedLockManager::TNamedLockPtr Lock;

    public:
        TLock(const TString& path, TDuration timeout)
        {
            DEBUG_LOG << "lock " << path << "..." << Endl;
            TAtomicSharedPtr<TWaiter> waiter(new TWaiter(this));
            INamedLockManager::StartLock(path, waiter);
            waiter->Wait(timeout);
            DEBUG_LOG << "lock " << path << "..." << (IsLocked() ? "OK" : "FAIL") << Endl;
        }

        bool IsLocked() const override {
            return !!Lock;
        }
    };

    TAbstractLock::TPtr TLocalStorage::NativeWriteLockNode(const TString& path, TDuration timeout /*= TDuration::Seconds(100000)*/) const {
        TAbstractLock::TPtr result(new TLock(FullPath(path), timeout));
        return result->IsLocked() ? result : nullptr;
    }

    TAbstractLock::TPtr TLocalStorage::NativeReadLockNode(const TString& path, TDuration timeout /*= TDuration::Seconds(100000)*/) const {
        return WriteLockNode(path, timeout);
    }

    TFsPath TLocalStorage::FullPath(const TString& key) const {
        return TFsPath(key).Fix();
    }
}
