/**
 * src/class_diagram/model/diagram.cc
 *
 * Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
 *
 * Licensed 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 "diagram.h"

#include "util/error.h"
#include "util/util.h"

#include <cassert>

namespace clanguml::class_diagram::model {

const std::vector<type_safe::object_ref<const class_>> &diagram::classes() const
{
    return classes_;
}

const std::vector<type_safe::object_ref<const enum_>> &diagram::enums() const
{
    return enums_;
}

common::model::diagram_t diagram::type() const
{
    return common::model::diagram_t::kClass;
}

type_safe::optional_ref<const clanguml::common::model::diagram_element>
diagram::get(const std::string &full_name) const
{
    type_safe::optional_ref<const clanguml::common::model::diagram_element> res;

    res = get_class(full_name);

    if (res.has_value())
        return res;

    res = get_enum(full_name);

    return res;
}

bool diagram::has_class(const class_ &c) const
{
    return std::any_of(classes_.cbegin(), classes_.cend(),
        [&c](const auto &cc) { return cc.get() == c; });
}

bool diagram::has_enum(const enum_ &e) const
{
    return std::any_of(enums_.cbegin(), enums_.cend(),
        [&e](const auto &ee) { return ee.get().full_name() == e.full_name(); });
}

type_safe::optional_ref<const class_> diagram::get_class(
    const std::string &name) const
{
    for (const auto &c : classes_) {
        if (c.get().full_name(false) == name) {
            return {c};
        }
    }

    return type_safe::nullopt;
}

type_safe::optional_ref<const enum_> diagram::get_enum(
    const std::string &name) const
{
    for (const auto &e : enums_) {
        if (e.get().full_name(false) == name) {
            return {e};
        }
    }

    return type_safe::nullopt;
}

void diagram::add_type_alias(std::unique_ptr<type_alias> &&ta)
{
    LOG_DBG(
        "Adding global alias: {} -> {}", ta->alias(), ta->underlying_type());

    type_aliases_[ta->alias()] = std::move(ta);
}

bool diagram::add_package(std::unique_ptr<common::model::package> &&p)
{
    LOG_DBG("Adding namespace package: {}, {}", p->name(), p->full_name(true));

    auto ns = p->get_relative_namespace();

    return add_element(ns, std::move(p));
}

bool diagram::add_class(std::unique_ptr<class_> &&c)
{
    const auto base_name = c->name();
    const auto full_name = c->full_name(false);

    LOG_DBG("Adding class: {}::{}, {}", c->get_namespace().to_string(),
        base_name, full_name);

    if (util::contains(base_name, "::"))
        throw std::runtime_error("Name cannot contain namespace: " + base_name);

    if (util::contains(base_name, "<"))
        throw std::runtime_error("Name cannot contain <: " + base_name);

    if (util::contains(base_name, "*"))
        throw std::runtime_error("Name cannot contain *: " + base_name);

    const auto ns = c->get_relative_namespace();
    auto name = base_name;
    auto name_with_ns = c->name_and_ns();
    auto name_and_ns = ns | name;
    const auto &cc = *c;

    auto cc_ref = type_safe::ref(cc);

    if (!has_class(cc)) {
        if (add_element(ns, std::move(c)))
            classes_.push_back(std::move(cc_ref));

        const auto &el = get_element<class_>(name_and_ns).value();

        if ((el.name() != name) || !(el.get_relative_namespace() == ns))
            throw std::runtime_error(
                "Invalid element stored in the diagram tree");

        return true;
    }

    LOG_DBG("Class {} ({}) already in the model", base_name, full_name);

    return false;
}

bool diagram::add_enum(std::unique_ptr<enum_> &&e)
{
    const auto full_name = e->name();

    LOG_DBG("Adding enum: {}", full_name);

    assert(!util::contains(e->name(), "::"));

    auto e_ref = type_safe::ref(*e);
    auto ns = e->get_relative_namespace();

    if (!has_enum(*e)) {
        if (add_element(ns, std::move(e))) {
            enums_.emplace_back(std::move(e_ref));
            return true;
        }
    }

    LOG_DBG("Enum {} already in the model", full_name);

    return false;
}

void diagram::get_parents(
    std::unordered_set<type_safe::object_ref<const class_>> &parents) const
{
    bool found_new = false;
    for (const auto &parent : parents) {
        for (const auto &pp : parent.get().parents()) {
            const auto p = get_class(pp.name());
            if (p.has_value()) {
                auto [it, found] = parents.emplace(p.value());
                if (found)
                    found_new = true;
            }
        }
    }

    if (found_new) {
        get_parents(parents);
    }
}

std::string diagram::to_alias(const std::string &full_name) const
{
    LOG_DBG("Looking for alias for {}", full_name);

    for (const auto &c : classes_) {
        const auto &cc = c.get();
        if (cc.full_name() == full_name) {
            return c->alias();
        }
    }

    for (const auto &e : enums_) {
        if (e.get().full_name() == full_name) {
            return e->alias();
        }
    }

    throw error::uml_alias_missing(
        fmt::format("Missing alias for {}", full_name));
}

}

namespace clanguml::common::model {
template <>
bool check_diagram_type<clanguml::class_diagram::model::diagram>(diagram_t t)
{
    return t == diagram_t::kClass;
}
}
