#include "PbbamInternalConfig.h"

#include <pbbam/SequenceInfo.h>

#include <cassert>
#include <cstdint>

#include <limits>
#include <sstream>
#include <tuple>
#include <type_traits>

#include <pbbam/SamTagCodec.h>
#include <pbbam/StringUtilities.h>

namespace PacBio {
namespace BAM {
namespace {

const std::string token_SN{"SN"};
const std::string token_LN{"LN"};
const std::string token_AS{"AS"};
const std::string token_M5{"M5"};
const std::string token_SP{"SP"};
const std::string token_UR{"UR"};

}  // namespace

static_assert(std::is_copy_constructible<SequenceInfo>::value,
              "SequenceInfo(const SequenceInfo&) is not = default");
static_assert(std::is_copy_assignable<SequenceInfo>::value,
              "SequenceInfo& operator=(const SequenceInfo&) is not = default");

static_assert(std::is_nothrow_move_constructible<SequenceInfo>::value,
              "SequenceInfo(SequenceInfo&&) is not = noexcept");
static_assert(std::is_nothrow_move_assignable<SequenceInfo>::value ==
                  std::is_nothrow_move_assignable<std::string>::value,
              "");

SequenceInfo::SequenceInfo(std::string name, std::string length)
    : name_(std::move(name)), length_(std::move(length))
{
}

bool SequenceInfo::operator==(const SequenceInfo& other) const noexcept
{
    return std::tie(assemblyId_, checksum_, length_, name_, species_, uri_, custom_) ==
           std::tie(other.assemblyId_, other.checksum_, other.length_, other.name_, other.species_,
                    other.uri_, other.custom_);
}

bool SequenceInfo::operator!=(const SequenceInfo& other) const noexcept
{
    return !(*this == other);
}

std::string SequenceInfo::AssemblyId() const { return assemblyId_; }

SequenceInfo& SequenceInfo::AssemblyId(std::string id)
{
    assemblyId_ = std::move(id);
    return *this;
}

std::string SequenceInfo::Checksum() const { return checksum_; }

SequenceInfo& SequenceInfo::Checksum(std::string checksum)
{
    checksum_ = std::move(checksum);
    return *this;
}

std::map<std::string, std::string> SequenceInfo::CustomTags() const { return custom_; }

SequenceInfo& SequenceInfo::CustomTags(std::map<std::string, std::string> custom)
{
    custom_ = std::move(custom);
    return *this;
}

SequenceInfo SequenceInfo::FromSam(const std::string& sam)
{
    // pop off '@SQ\t', then split rest of line into tokens
    const auto tokens = Split(sam.substr(4), '\t');
    if (tokens.empty()) return {};

    SequenceInfo seq;
    std::map<std::string, std::string> custom;

    // iterate over tokens
    for (const auto& token : tokens) {
        const auto tokenTag = token.substr(0, 2);
        auto tokenValue = token.substr(3);

        // set sequence info
        // clang-format off
        if      (tokenTag == token_SN) seq.Name(std::move(tokenValue));
        else if (tokenTag == token_LN) seq.Length(std::move(tokenValue));
        else if (tokenTag == token_AS) seq.AssemblyId(std::move(tokenValue));
        else if (tokenTag == token_M5) seq.Checksum(std::move(tokenValue));
        else if (tokenTag == token_SP) seq.Species(std::move(tokenValue));
        else if (tokenTag == token_UR) seq.Uri(std::move(tokenValue));
        // clang-format on

        // otherwise, "custom" tag
        else
            custom[tokenTag] = std::move(tokenValue);
    }

    seq.CustomTags(std::move(custom));
    return seq;
}

bool SequenceInfo::IsValid() const
{
    if (name_.empty()) return false;

    // use long instead of int32_t, just to make sure we can catch overflow
    const long l = atol(length_.c_str());
    return l >= 0 && l <= std::numeric_limits<int32_t>::max();
}

std::string SequenceInfo::Length() const { return length_; }

SequenceInfo& SequenceInfo::Length(std::string length)
{
    length_ = std::move(length);
    return *this;
}

std::string SequenceInfo::Name() const { return name_; }

SequenceInfo& SequenceInfo::Name(std::string name)
{
    name_ = std::move(name);
    return *this;
}

std::string SequenceInfo::Species() const { return species_; }

SequenceInfo& SequenceInfo::Species(std::string species)
{
    species_ = std::move(species);
    return *this;
}

std::string SequenceInfo::ToSam(const SequenceInfo& seq) { return seq.ToSam(); }

std::string SequenceInfo::ToSam() const
{
    std::ostringstream out;
    out << "@SQ" << MakeSamTag(token_SN, name_);

    // clang-format off
    if (!length_.empty())     out << MakeSamTag(token_LN, length_);
    if (!assemblyId_.empty()) out << MakeSamTag(token_AS, assemblyId_);
    if (!checksum_.empty())   out << MakeSamTag(token_M5, checksum_);
    if (!species_.empty())    out << MakeSamTag(token_SP, species_);
    if (!uri_.empty())        out << MakeSamTag(token_UR, uri_);
    // clang-format on

    // append any custom tags
    for (auto&& attribute : custom_)
        out << MakeSamTag(std::move(attribute.first), std::move(attribute.second));

    return out.str();
}

std::string SequenceInfo::Uri() const { return uri_; }

SequenceInfo& SequenceInfo::Uri(std::string uri)
{
    uri_ = std::move(uri);
    return *this;
}

}  // namespace BAM
}  // namespace PacBio
