#include "completer_command.h"
#include "last_getopt_opts.h"
#include "wrap.h"
#include "last_getopt_parser.h"

#include <library/cpp/colorizer/colors.h>

#include <util/stream/format.h>
#include <util/charset/utf8.h>

#include <stdlib.h>

namespace NLastGetoptPrivate {
    TString& VersionString() {
        static TString data;
        return data;
    }
    TString& ShortVersionString() {
        static TString data;
        return data;
    }
}

namespace NLastGetopt {
    static const TStringBuf SPad = "  ";

    void PrintVersionAndExit(const TOptsParser*) {
        Cout << (NLastGetoptPrivate::VersionString() ? NLastGetoptPrivate::VersionString() : "program version: not linked with library/cpp/getopt") << Endl;
        exit(NLastGetoptPrivate::VersionString().empty());
    }

    void PrintShortVersionAndExit(const TString& appName) {
        Cout << appName << " version " << (NLastGetoptPrivate::ShortVersionString() ? NLastGetoptPrivate::ShortVersionString() : "not linked with library/cpp/getopt") << Endl;
        exit(NLastGetoptPrivate::ShortVersionString().empty());
    }

    // Like TString::Quote(), but does not quote digits-only string
    static TString QuoteForHelp(const TString& str) {
        if (str.empty())
            return str.Quote();
        for (size_t i = 0; i < str.size(); ++i) {
            if (!isdigit(str[i]))
                return str.Quote();
        }
        return str;
    }

    namespace NPrivate {
        TString OptToString(char c) {
            TStringStream ss;
            ss << "-" << c;
            return ss.Str();
        }

        TString OptToString(const TString& longOption) {
            TStringStream ss;
            ss << "--" << longOption;
            return ss.Str();
        }

        TString OptToString(const TOpt* opt) {
            return opt->ToShortString();
        }
    }

    TOpts::TOpts(const TStringBuf& optstring)
        : ArgPermutation_(DEFAULT_ARG_PERMUTATION)
        , AllowSingleDashForLong_(false)
        , AllowPlusForLong_(false)
        , AllowUnknownCharOptions_(false)
        , AllowUnknownLongOptions_(false)
        , FreeArgsMin_(0)
        , FreeArgsMax_(UNLIMITED_ARGS)
    {
        if (!optstring.empty()) {
            AddCharOptions(optstring);
        }
        AddVersionOption(0);
    }

    void TOpts::AddCharOptions(const TStringBuf& optstring) {
        size_t p = 0;
        if (optstring[p] == '+') {
            ArgPermutation_ = REQUIRE_ORDER;
            ++p;
        } else if (optstring[p] == '-') {
            ArgPermutation_ = RETURN_IN_ORDER;
            ++p;
        }

        while (p < optstring.size()) {
            char c = optstring[p];
            p++;
            EHasArg ha = NO_ARGUMENT;
            if (p < optstring.size() && optstring[p] == ':') {
                ha = REQUIRED_ARGUMENT;
                p++;
            }
            if (p < optstring.size() && optstring[p] == ':') {
                ha = OPTIONAL_ARGUMENT;
                p++;
            }
            AddCharOption(c, ha);
        }
    }

    const TOpt* TOpts::FindLongOption(const TStringBuf& name) const {
        for (const auto& Opt : Opts_) {
            const TOpt* opt = Opt.Get();
            if (IsIn(opt->GetLongNames(), name))
                return opt;
        }
        return nullptr;
    }

    TOpt* TOpts::FindLongOption(const TStringBuf& name) {
        for (auto& Opt : Opts_) {
            TOpt* opt = Opt.Get();
            if (IsIn(opt->GetLongNames(), name))
                return opt;
        }
        return nullptr;
    }

    const TOpt* TOpts::FindCharOption(char c) const {
        for (const auto& Opt : Opts_) {
            const TOpt* opt = Opt.Get();
            if (IsIn(opt->GetShortNames(), c))
                return opt;
        }
        return nullptr;
    }

    TOpt* TOpts::FindCharOption(char c) {
        for (auto& Opt : Opts_) {
            TOpt* opt = Opt.Get();
            if (IsIn(opt->GetShortNames(), c))
                return opt;
        }
        return nullptr;
    }

    const TOpt& TOpts::GetCharOption(char c) const {
        const TOpt* option = FindCharOption(c);
        if (!option)
            ythrow TException() << "unknown char option '" << c << "'";
        return *option;
    }

    TOpt& TOpts::GetCharOption(char c) {
        TOpt* option = FindCharOption(c);
        if (!option)
            ythrow TException() << "unknown char option '" << c << "'";
        return *option;
    }

    const TOpt& TOpts::GetLongOption(const TStringBuf& name) const {
        const TOpt* option = FindLongOption(name);
        if (!option)
            ythrow TException() << "unknown option " << name;
        return *option;
    }

    TOpt& TOpts::GetLongOption(const TStringBuf& name) {
        TOpt* option = FindLongOption(name);
        if (!option)
            ythrow TException() << "unknown option " << name;
        return *option;
    }

    bool TOpts::HasAnyShortOption() const {
        for (const auto& Opt : Opts_) {
            const TOpt* opt = Opt.Get();
            if (!opt->GetShortNames().empty())
                return true;
        }
        return false;
    }

    bool TOpts::HasAnyLongOption() const {
        for (const auto& Opt : Opts_) {
            TOpt* opt = Opt.Get();
            if (!opt->GetLongNames().empty())
                return true;
        }
        return false;
    }

    void TOpts::Validate() const {
        for (TOptsVector::const_iterator i = Opts_.begin(); i != Opts_.end(); ++i) {
            TOpt* opt = i->Get();
            const TOpt::TShortNames& shortNames = opt->GetShortNames();
            for (auto c : shortNames) {
                for (TOptsVector::const_iterator j = i + 1; j != Opts_.end(); ++j) {
                    TOpt* nextOpt = j->Get();
                    if (nextOpt->CharIs(c))
                        ythrow TConfException() << "option "
                                                << NPrivate::OptToString(c)
                                                << " is defined more than once";
                }
            }
            const TOpt::TLongNames& longNames = opt->GetLongNames();
            for (const auto& longName : longNames) {
                for (TOptsVector::const_iterator j = i + 1; j != Opts_.end(); ++j) {
                    TOpt* nextOpt = j->Get();
                    if (nextOpt->NameIs(longName))
                        ythrow TConfException() << "option "
                                                << NPrivate::OptToString(longName)
                                                << " is defined more than once";
                }
            }
        }
        if (FreeArgsMax_ < FreeArgsMin_) {
            ythrow TConfException() << "FreeArgsMax must be >= FreeArgsMin";
        }
        if (!FreeArgSpecs_.empty() && FreeArgSpecs_.rbegin()->first >= FreeArgsMax_) {
            ythrow TConfException() << "Described args count is greater than FreeArgsMax. Either increase FreeArgsMax or remove unreachable descriptions";
        }
    }

    TOpt& TOpts::AddOption(const TOpt& option) {
        if (option.GetShortNames().empty() && option.GetLongNames().empty())
            ythrow TConfException() << "bad option: no chars, no long names";
        Opts_.push_back(new TOpt(option));
        return *Opts_.back();
    }

    TOpt& TOpts::AddCompletionOption(TString command, TString longName) {
        if (TOpt* o = FindLongOption(longName)) {
            return *o;
        }

        return AddOption(MakeCompletionOpt(this, std::move(command), std::move(longName)));
    }

    namespace {
        auto MutuallyExclusiveHandler(const TOpt* cur, const TOpt* other) {
            return [cur, other](const TOptsParser* p) {
                if (p->Seen(other)) {
                    throw TUsageException()
                        << "option " << cur->ToShortString()
                        << " can't appear together with option " << other->ToShortString();
                }
            };
        }
    }

    void TOpts::MutuallyExclusiveOpt(TOpt& opt1, TOpt& opt2) {
        opt1.Handler1(MutuallyExclusiveHandler(&opt1, &opt2))
            .IfPresentDisableCompletionFor(opt2);
        opt2.Handler1(MutuallyExclusiveHandler(&opt2, &opt1))
            .IfPresentDisableCompletionFor(opt1);
    }

    size_t TOpts::IndexOf(const TOpt* opt) const {
        TOptsVector::const_iterator it = std::find(Opts_.begin(), Opts_.end(), opt);
        if (it == Opts_.end())
            ythrow TException() << "unknown option";
        return it - Opts_.begin();
    }

    TStringBuf TOpts::GetFreeArgTitle(size_t pos) const {
        if (FreeArgSpecs_.contains(pos)) {
            return FreeArgSpecs_.at(pos).GetTitle(DefaultFreeArgTitle_);
        }
        return DefaultFreeArgTitle_;
    }

    void TOpts::SetFreeArgTitle(size_t pos, const TString& title, const TString& help, bool optional) {
        FreeArgSpecs_[pos] = TFreeArgSpec(title, help, optional);
    }

    TFreeArgSpec& TOpts::GetFreeArgSpec(size_t pos) {
        return FreeArgSpecs_[pos];
    }

    static TString FormatOption(const TOpt* option, const NColorizer::TColors& colors) {
        TStringStream result;
        const TOpt::TShortNames& shorts = option->GetShortNames();
        const TOpt::TLongNames& longs = option->GetLongNames();

        const size_t nopts = shorts.size() + longs.size();
        const bool multiple = 1 < nopts;
        if (multiple)
            result << '{';
        for (size_t i = 0; i < nopts; ++i) {
            if (multiple && 0 != i)
                result << '|';

            if (i < shorts.size()) // short
                result << colors.GreenColor() << '-' << shorts[i] << colors.OldColor();
            else
                result << colors.GreenColor() << "--" << longs[i - shorts.size()] << colors.OldColor();
        }
        if (multiple)
            result << '}';

        static const TString metavarDef("VAL");
        const TString& title = option->GetArgTitle();
        const TString& metavar = title.empty() ? metavarDef : title;

        if (option->GetHasArg() == OPTIONAL_ARGUMENT) {
            result << " [" << metavar;
            if (option->HasOptionalValue())
                result << ':' << option->GetOptionalValue();
            result << ']';
        } else if (option->GetHasArg() == REQUIRED_ARGUMENT)
            result << ' ' << metavar;
        else
            Y_ASSERT(option->GetHasArg() == NO_ARGUMENT);

        return result.Str();
    }

    void TOpts::PrintCmdLine(const TStringBuf& program, IOutputStream& os, const NColorizer::TColors& colors) const {
        os << colors.BoldColor() << "Usage" << colors.OldColor() << ": ";
        if (CustomUsage) {
            os << CustomUsage;
        } else {
            os << program << " ";
        }
        if (CustomCmdLineDescr) {
            os << CustomCmdLineDescr << Endl;
            return;
        }
        os << "[OPTIONS]";

        ui32 numDescribedFlags = FreeArgSpecs_.empty() ? 0 : FreeArgSpecs_.rbegin()->first + 1;
        ui32 numArgsToShow = Max(FreeArgsMin_, FreeArgsMax_ == UNLIMITED_ARGS ? numDescribedFlags : FreeArgsMax_);

        for (ui32 i = 0, nonOptionalFlagsPrinted = 0; i < numArgsToShow; ++i) {
            bool isOptional = nonOptionalFlagsPrinted >= FreeArgsMin_ || FreeArgSpecs_.Value(i, TFreeArgSpec()).Optional_;

            nonOptionalFlagsPrinted += !isOptional;

            os << " ";

            if (isOptional)
                os << "[";

            os << GetFreeArgTitle(i);

            if (isOptional)
                os << "]";
        }

        if (FreeArgsMax_ == UNLIMITED_ARGS) {
            os << " [" << TrailingArgSpec_.GetTitle(DefaultFreeArgTitle_) << "]...";
        }

        os << Endl;
    }

    void TOpts::PrintUsage(const TStringBuf& program, IOutputStream& osIn, const NColorizer::TColors& colors) const {
        TStringStream os;

        if (!Title.empty())
            os << Title << "\n\n";

        PrintCmdLine(program, os, colors);

        TVector<TString> leftColumn(Opts_.size());
        TVector<size_t> leftColumnSizes(leftColumn.size());
        const size_t kMaxLeftWidth = 25;
        size_t leftWidth = 0;
        size_t requiredOptionsCount = 0;
        NColorizer::TColors disabledColors(false);

        for (size_t i = 0; i < Opts_.size(); i++) {
            const TOpt* opt = Opts_[i].Get();
            if (opt->IsHidden())
                continue;
            leftColumn[i] = FormatOption(opt, colors);
            size_t leftColumnSize = leftColumn[i].size();
            if (colors.IsTTY()) {
                leftColumnSize -= NColorizer::TotalAnsiEscapeCodeLen(leftColumn[i]);
            }
            leftColumnSizes[i] = leftColumnSize;
            if (leftColumnSize <= kMaxLeftWidth) {
                leftWidth = Max(leftWidth, leftColumnSize);
            }
            if (opt->IsRequired())
                requiredOptionsCount++;
        }

        const TString leftPadding(leftWidth, ' ');

        for (size_t sectionId = 0; sectionId <= 1; sectionId++) {
            bool requiredOptionsSection = (sectionId == 0);

            if (requiredOptionsSection) {
                if (requiredOptionsCount == 0)
                    continue;
                os << Endl << colors.BoldColor() << "Required parameters" << colors.OldColor() << ":" << Endl;
            } else {
                if (requiredOptionsCount == Opts_.size())
                    continue;
                if (requiredOptionsCount == 0)
                    os << Endl << colors.BoldColor() << "Options" << colors.OldColor() << ":" << Endl;
                else
                    os << Endl << colors.BoldColor() << "Optional parameters" << colors.OldColor() << ":" << Endl; // optional options would be a tautology
            }

            for (size_t i = 0; i < Opts_.size(); i++) {
                const TOpt* opt = Opts_[i].Get();

                if (opt->IsHidden())
                    continue;
                if (opt->IsRequired() != requiredOptionsSection)
                    continue;

                if (leftColumnSizes[i] > leftWidth && !opt->GetHelp().empty()) {
                    os << SPad << leftColumn[i] << Endl << SPad << leftPadding << ' ';
                } else {
                    os << SPad << leftColumn[i] << ' ';
                    if (leftColumnSizes[i] < leftWidth)
                        os << TStringBuf(leftPadding.data(), leftWidth - leftColumnSizes[i]);
                }

                TStringBuf help = opt->GetHelp();
                while (help && isspace(help.back())) {
                    help.Chop(1);
                }
                size_t lastLineLength = 0;
                bool helpHasParagraphs = false;
                if (help) {
                    os << Wrap(Wrap_, help, SPad + leftPadding + " ", &lastLineLength, &helpHasParagraphs);
                }

                if (opt->HasDefaultValue()) {
                    auto quotedDef = QuoteForHelp(opt->GetDefaultValue());
                    if (helpHasParagraphs) {
                        os << Endl << Endl << SPad << leftPadding << " ";
                        os << "Default: " << colors.CyanColor() << quotedDef << colors.OldColor() << ".";
                    } else if (help.EndsWith('.')) {
                        os << Endl << SPad << leftPadding << " ";
                        os << "Default: " << colors.CyanColor() << quotedDef << colors.OldColor() << ".";
                    } else if (help) {
                        if (SPad.size() + leftWidth + 1 + lastLineLength + 12 + quotedDef.size() > Wrap_) {
                            os << Endl << SPad << leftPadding << " ";
                        } else {
                            os << " ";
                        }
                        os << "(default: " << colors.CyanColor() << quotedDef << colors.OldColor() << ")";
                    } else {
                        os << "default: " << colors.CyanColor() << quotedDef << colors.OldColor();
                    }
                }

                os << Endl;

                if (helpHasParagraphs) {
                    os << Endl;
                }
            }
        }

        PrintFreeArgsDesc(os, colors);

        for (auto& [heading, text] : Sections) {
            os << Endl << colors.BoldColor() << heading << colors.OldColor() << ":" << Endl;

            os << SPad << Wrap(Wrap_, text, SPad) << Endl;
        }

        osIn << os.Str();
    }

    void TOpts::PrintUsage(const TStringBuf& program, IOutputStream& os) const {
        PrintUsage(program, os, NColorizer::AutoColors(os));
    }

    void TOpts::PrintFreeArgsDesc(IOutputStream& os, const NColorizer::TColors& colors) const {
        if (0 == FreeArgsMax_)
            return;

        size_t leftFreeWidth = 0;
        for (size_t i = 0; i < FreeArgSpecs_.size(); ++i) {
            leftFreeWidth = Max(leftFreeWidth, GetFreeArgTitle(i).size());
        }

        if (!TrailingArgSpec_.IsDefault()) {
            leftFreeWidth = Max(leftFreeWidth, TrailingArgSpec_.GetTitle(DefaultFreeArgTitle_).size());
        }

        leftFreeWidth = Min(leftFreeWidth, size_t(30));
        os << Endl << colors.BoldColor() << "Free args" << colors.OldColor() << ":";

        os << " min: " << colors.GreenColor() << FreeArgsMin_ << colors.OldColor() << ",";
        os << " max: " << colors.GreenColor();
        if (FreeArgsMax_ != UNLIMITED_ARGS) {
            os << FreeArgsMax_;
        } else {
            os << "unlimited";
        }
        os << colors.OldColor() << Endl;

        const size_t limit = FreeArgSpecs_.empty() ? 0 : FreeArgSpecs_.rbegin()->first;
        for (size_t i = 0; i <= limit; ++i) {
            if (!FreeArgSpecs_.contains(i)) {
                continue;
            }

            if (auto help = FreeArgSpecs_.at(i).GetHelp()) {
                auto title = GetFreeArgTitle(i);
                os << SPad << colors.GreenColor() << RightPad(title, leftFreeWidth, ' ') << colors.OldColor()
                   << SPad << help << Endl;
            }
        }

        if (FreeArgsMax_ == UNLIMITED_ARGS) {
            auto title = TrailingArgSpec_.GetTitle(DefaultFreeArgTitle_);
            if (auto help = TrailingArgSpec_.GetHelp()) {
                os << SPad << colors.GreenColor() << RightPad(title, leftFreeWidth, ' ') << colors.OldColor()
                   << SPad << help << Endl;
            }
        }
    }
}