/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/
/** @file mscentity.cpp The implementation of entities and related classes.
 * @ingroup libmscgen_files */

#include <cmath>
#include "mscelement.h"
#include "msc.h"

using namespace msc;

double EntityStatusMap::GetWidth(double pos, double activeEntitySize) const
{
    auto st = *showStatus.Get(pos);
    if (!st.IsOn()) return 0;
    if (st.IsActive()) return activeEntitySize;
    else return styleStatus.Get(pos)->read().vline.LineWidth();
}


 /** Deserialize the signature from a string. Sets 's' to the first char after us in the string.*/
bool EntityCollapseCatalog::Deserialize(std::string_view &s)
{
    //always accept empty string
    if (s.length()==0 || s[0]==0) {
        clear();
        return true;
    }
    EntityCollapseCatalog cat;
    auto ver = DeserializeInt(s);
    //check version is 0
    if (!ver.first || ver.second!=0) {
        _ASSERT(0);
        return false;
    }
    if (!DeserializeNewline(s)) { //no newline
        _ASSERT(0);
        return false;
    }
    auto i = DeserializeInt(s);
    if (!i.first) {
        _ASSERT(0);
        return false;
    }
    if (!DeserializeNewline(s)) { //no newline
        _ASSERT(0);
        return false;
    }
    for (; s.length() && s[0] && i.second>0; i.second--) {
        const bool b = s[0]=='1';
        if (s[1]!=' ')
            return false;
        s.remove_prefix(2);
        auto ss = DeserializeLine(s);
        if (!ss.first) {
            _ASSERT(0);
            return false;
        }
        if (ss.second.length())
            cat[std::string(ss.second)] = b;
    }
    if (i.second) { //too few lines compared to the count at the beginning
        _ASSERT(0);
        return false;
    }
    DeserializeNewline(s); //swallow optional newline
    operator=(std::move(cat));
    return true;
}

/** Serialize the signature to a string */
std::string EntityCollapseCatalog::Serialize() const
{
    std::string ret;
    //version
    ret << "0\n";
    //size
    ret << size() << "\n";
    //"[0/1] <entityname>" lines
    for (auto &i : *this)
        ret << (i.second ? "1 " : "0 ") << i.first << "\n";
    return ret;
}

/** Creates a new entity.
 * @param [in] n The name of the entity.
 * @param [in] l The label of the entity.
 * @param [in] ol The label of the entity as specified by the user.
 * @param [in] p The position of the entity.
 * @param [in] pe The position of the entity, if all group entities were expanded.
 * @param [in] entity_style The style of the entity at definition.
 * @param [in] fp The location of the entity definition in the input file.
 * @param [in] coll True if we are group, but show collapsed.
 * @param [in] larg true if we are group and show a large heading
 */
Entity::Entity(const string &n, const string &l, const string &ol,
    double p, double pe, const MscStyleCoW &entity_style, const FileLineCol &fp,
    bool coll, bool larg) :
    name(n), label(l), orig_label(ol), file_pos(fp),
    shape(entity_style.read().shape.value_or(-1)),
    shape_size(entity_style.read().shape_size.value_or(EArrowSize::SMALL)),
    pos(p), pos_exp(pe), index(0), status(entity_style),
    parse_running_style(entity_style), running_style(entity_style),
    running_shown(EEntityStatus::SHOW_OFF),
    running_defined(false),
    maxwidth(0), collapsed(coll), large(larg)
{
    _ASSERT(n.length() && l.length());
}

/** Add a list of child entities, making us a group one.
 * Check that all entities in "children"
 * - have no parent;
 * - exist in chart->AllEntities;
 * - are actually defined now by this very EntityApp that is included in "children".
 *
 * Then add them to our children list and make us their parent.
 * Also set their and our "pos" to the leftmost of them, if we are collapsed*/
void Entity::AddChildrenList(const EntityAppList *children, MscChart *chart)
{
    if (!children || children->size()==0) return;
    double min_pos = MaxVal(min_pos);
    for (auto i=children->begin(); i!=children->end(); i++) {
        _ASSERT((*i)->defining);
        const EntityRef ei = chart->AllEntities.Find_by_Name((*i)->name);
        _ASSERT(ei);
        //if parent is already set this is a grandchildren
        if (ei->parent_name.length()==0) {
            children_names.insert((*i)->name);
            ei->parent_name = name;
        }
        //search for leftmost even among grandchildren
        min_pos = std::min(min_pos, ei->pos);
    }
    if (collapsed) {
        for (auto i=children->begin(); i!=children->end(); i++) {
            EntityRef ei = chart->AllEntities.Find_by_Name((*i)->name);
            _ASSERT(ei);
            if ((*i)->defining)
                ei->pos = min_pos;
        }
    }
    pos = min_pos; // do this even if we are not collapsed (so that we take up no space)

    if (children_names.size())
        running_style += chart->MyCurrentContext().styles[collapsed?"collapsed_entity":"expanded_entity"];
}

/** Returns the width of the entityline taking active and onoff status into account.
 * It relies on running_shown, running_style.
 * Use this during PostParseProcess.
 * Returns 0 if no entity line (turned off)*/
double Entity::GetRunningWidth(double activeEntitySize) const
{
    if (!running_shown.IsOn()) return 0;
    if (running_shown.IsActive()) return activeEntitySize;
    return running_style.read().vline.LineWidth();
}

/** Prints the entity name and position*/
string Entity::Print(int indent) const
{
    string ss;
    ss << string(indent*2, ' ') << name << " pos:" << pos;
    return ss;
}


EntityAppHelper* EntityAppHelper::Prepend(EntityAppHelper* edh) {
    if (edh) {
        entities.Prepend(&edh->entities);
        std::ranges::move(notes, std::back_inserter(edh->notes));
        std::swap(notes, edh->notes);
        edh->notes.clear();
        edh->entities.clear();
        //leave "target" as that of the latter edh
    }
    return this;
}


/** Create an EntityApp for entity named `s` onto the chart `chart`
 * Make all attributes unset, empty the style (but activate those style
 * elements that are set in the default entity style in chart->Contexts*/
EntityApp::EntityApp(std::string_view s, MscChart* msc) : MscElement(msc),
    name(s),
    only_shown_if_multi(),
    show_is_explicit(false),
    active_is_explicit(false),
    centerlined(false),
    centerline_target(nullptr),
    style(msc->MyCurrentContext().styles["entity"]),  //we will Empty it but use it for f_* values
    defining(false),
    draw_heading(false),
    large_appears(false)
{
    style.write().Empty();
}


/** Create a pseudo Entity App, one that refers to many entities at once.
 * This is used for 'heading' commands, or solo entity prefixes, such as
 * 'show', 'hide', 'activate', 'deactivate'.*/
EntityApp::EntityApp(MscChart* chart, bool shown_only, std::string_view prefix, const FileLineCol &l)
    : MscElement(chart),
    name(),
    only_shown_if_multi(shown_only),
    show_is_explicit(true),
    active_is_explicit(true),
    centerlined(false),
    centerline_target(nullptr),
    style(chart->MyCurrentContext().styles["entity"]),  //we will Empty it but use it for f_* values
    defining(false),
    draw_heading(false),
    large_appears(false)
{
    const bool _show = CaseInsensitiveEqual(prefix, "show");
    const bool _show_or_hide = _show || CaseInsensitiveEqual(prefix, "hide");
    const bool _activate = CaseInsensitiveEqual(prefix, "activate");
    const bool _activate_or_deactivate = _activate || CaseInsensitiveEqual(prefix, "deactivate");
    if (_show_or_hide) show = _show;
    if (_activate_or_deactivate) active = {_activate, l};
    style.write().Empty();
}

/** Take an attribute and apply it to us.
 *
 * We consider attributes 'label`, `pos`, `relative`, `show`, `collapsed`,
 * 'actve', 'color', applicable style attributes and any style at the current
 * context in `chart`.
 * At a problem, we generate an error into chart->Error.
 * @param [in] a The attribute to apply.
 * @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool EntityApp::AddAttribute(const Attribute& a)
{
    if (a.type == EAttrType::STYLE) {
        if (chart->MyCurrentContext().styles.find(a.name) == chart->MyCurrentContext().styles.end()) {
            if (!chart->SkipContent())
                a.InvalidStyleError(chart->Error);
            return true;
        }
        style += chart->MyCurrentContext().styles[a.name];
        return true;
    }
    if (a.Is("large")) {
        if (!defining)
            chart->Error.Error(a, false, "This attribute can only be set when you define an entity. Ignoring it.");
        else if (a.CheckType(EAttrType::BOOL, chart->Error)) {
            large = {a.yes, a.linenum_attr.start};
        }
        return true;
    }
    if ((a.Is("shape") || a.Is("shape.size")) && !defining) {
        //this will be handled as part of style, but we do some checking
        chart->Error.Error(a, false, "This attribute can only be set when you define an entity. Ignoring it.");
        return true;
    }
    if (a.Is("shape.size") && a.type==EAttrType::STRING && CaseInsensitiveEqual(a.value, "auto")) {
        //We indicate the "auto" setting as INVALID
        style.write().shape_size = EArrowSize::INVALID;
        return true;
    }
    string s;
    if (a.Is("label")) {
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        //if EAttrType::CLEAR, we are OK above and a.value is ""
        label = {a.value, a.linenum_attr.start};
        linenum_label_value = a.linenum_value.start;
        return true;
    }
    if (a.Is("pos")) {
        if (a.type == EAttrType::CLEAR) {
            pos = {0, a.linenum_attr.start};
            return true;
        }
        if (a.CheckType(EAttrType::NUMBER, chart->Error))
            pos = {a.number, a.linenum_attr.start};
        return true;
    }
    if (a.Is("relative")) {
        if (a.type == EAttrType::CLEAR || a.value.empty())
            chart->Error.Error(a, true, "Missing entity name.");
        else if (a.CheckType(EAttrType::STRING, chart->Error))
            rel = {a.value, a.linenum_attr.start};
        return true;
    }
    if (a.Is("collapsed")) {
        if (!a.CheckType(EAttrType::BOOL, chart->Error)) return true;
        collapsed = {a.yes, a.linenum_attr.start};
        return true;
    }
    if (a.Is("show")) {
        if (!a.CheckType(EAttrType::BOOL, chart->Error)) return true;
        // EAttrType::CLEAR is handled above
        show = a.yes;
        show_is_explicit = true;
        return true;
    }
    if (a.Is("active")) {
        if (!a.CheckType(EAttrType::BOOL, chart->Error)) return true;
        // EAttrType::CLEAR is handled above
        active = {a.yes, a.linenum_attr.start};
        active_is_explicit = true;
        return true;
    }
    if (a.Is("id")) {
        s << "Attribute '"<< a.name <<"' is no longer supported. Ignoring it.";
        chart->Error.Error(a, false, s, "Try '\\^' inside a label for superscript.");
        return false;
    }
    if (a.Is("url")) {
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        //EAttrType::CLEAR is OK above with value = ""
        url = a.value;
        linenum_url_attr = a.linenum_attr.start;
        return true;
    }
    if (a.Is("text.wrap")) {
        s << "Word wrapping is not available for entity headings. Ignoring this attribute.";
        chart->Error.Error(a, false, s);
        return false;
    }
    if (MscElement::AddAttribute(a)) return true;
    if (style.write().AddAttribute(a, chart)) return true;
    a.InvalidAttrError(chart->Error);
    return false;
};

/** Add a list of attributes to us and a list of potential child objects.
 * This function is always called, even if there are no attributes specified
 * (l will be nullptr in that case), except for automatically generated entities.
 * (Or rather to the automatically generated EntityApp of automatically generated
 * Entity objects.)
 * If the Entity named by this EntityApp does not yet exist, we create one, set all
 * its parameters and add it to chart->AllEntities, except if we are parsing a procedure.
 * Any children are already defined at this point, so we can modify their "parent_name" field.
 * We do not alter the entity's running_status or running_style, except for
 * running_style.aline, which is required for arrows already during parsing.
 * @param al The list of attributes to apply.
 * @param ch The list of arcs, we shall take our children from. This is specified
 *           specified after the entity definition in braces. These may contain objects
 *           other than EntityCommand and Note, in that case we need to give an error.
 * @param [in] l The position of the EntityApp in the file.
 * @return An EntityAppHelper which contains us and our children (if any), plus all notes.
 *         We will be the first EntityApp in the returned list, children will come after.
 * NOTE: ownership of us (this object) is transferred to the returned object by creating a
 *   unique ptr from 'this'!*/
std::unique_ptr<EntityAppHelper> EntityApp::AddAttributeList(AttributeList *al, ArcList *ch, FileLineCol l)
{
    EntityRef our_entity = chart->AllEntities.Find_by_Name(name);
    if (our_entity) {
        // Existing entity: kill auto-assumed "show=on" and "active=no"
        show.reset();
        active.reset();
        // Leave show_explicit as false
        //take the entity's draw_pass as default
        draw_pass = our_entity->running_draw_pass;
    } else {
        //indicate that this EntityApp created the Entity
        defining = true;
        draw_pass = EMscDrawPass::DEFAULT;
    }

    // Process attribute list, "style" is empty (emptied in constructor)
    if (al) {
        for (auto &pAttr : *al)
            AddAttribute(*pAttr);
        //Check that if we have a shape and shape.size=auto then the shape has a label box
        if (auto &s = style.read();
                  s.shape.value_or(-1)>=0
               && s.shape_size.value_or(EArrowSize::BIG)==EArrowSize::INVALID
               && chart->Shapes[*s.shape].GetLabelPos().IsInvalid()) {
            //Find the (last) shape.size attr
            auto i = std::find_if(al->rbegin(), al->rend(), [](const std::unique_ptr<Attribute> &pA) {return pA && pA->type==EAttrType::STRING && CaseInsensitiveEqual(pA->name, "shape.size"); });
            if (i!=al->rend()) {
                chart->Error.Error(**i, true, "Shape '" + chart->Shapes[*s.shape].name +"' has no label box defined.Nothing to size the shape for.Ignoring this attribute.");
            } else {
                _ASSERT(0);
            }
            style.write().shape_size.reset();
        }
        delete al;
    }

    std::unique_ptr<EntityAppHelper> ret = std::make_unique<EntityAppHelper>();
    //If we have children, add them to "ret->entities"
    ret->target = name; //this will be the target for any note after us.
    if (ch) {
        for (auto j = ch->begin(); j!=ch->end(); /*nope*/)
            if (EntityCommand* const ce = dynamic_cast<EntityCommand*>(j->get())) {
                ce->MoveMyContentAfter(*ret);  //ce is emptied out of all EntityDefs and tmp stored notes
                //set the target for notes following to the last child.
                ret->target = ret->entities.back()->name;
                j++;
            } else if (Note* const cn = dynamic_cast<Note*>(j->get())) {
                //Ok, a note, assume it wants to point to us or our last child so far
                ret->notes.emplace_back(std::unique_ptr<Note>(cn), ret->target);
                j->release();
                j = ch->erase(j);
            } else {
                chart->Error.Error((*j)->file_pos.start, "Only entity definitions are allowed here. Ignoring this.");
                j++;
            }
    }
    delete ch;

    //Move children we are not defining to the end
    const auto bads = std::ranges::stable_partition(ret->entities, [](std::unique_ptr<EntityApp>& e) {return e->defining; });
    for (auto &bad : bads) {
        EntityRef ei = chart->AllEntities.Find_by_Name(bad->name);
        _ASSERT(ei);
        chart->Error.Error(bad->file_pos.start, "Cannot make an already existing entity part of a group.",
                            "Entity remains outside this group.");
        chart->Error.Error(ei->file_pos, bad->file_pos.start, "Entity '" + ei->name + "' was defined here.");
    }
    ret->entities.erase(bads.begin(), bads.end());

    //Check that we apply certain attributes the right way for grouped entities
    if (ret->entities.size()) {
        if (pos || rel)
            chart->Error.Error(pos ? pos.file_pos : rel.file_pos,
                               "The position of grouped entities is derived from its member entities.",
                               " Ignoring attribute.");
        pos.reset(); rel.reset();
        if (active && active_is_explicit)
            chart->Error.Error(active.file_pos, "You cannot directly activate or deactivate a grouped entity.",
                               "The active/inactive state of grouped entities is derived from its member entities. Ignoring attribute.");
        active.reset();
        active_is_explicit = false;
        //consider forced collapse
        auto force_itr = chart->force_entity_collapse.find(name);
        if (force_itr != chart->force_entity_collapse.end()) {
            //If force_entity_collapse is same as chart value, remove
            if (collapsed.value_or(false) == force_itr->second)
                chart->force_entity_collapse.erase(force_itr);
            else
                collapsed = force_itr->second;
        }
    } else {
        if (collapsed.value_or(false))
            chart->Error.Error(collapsed.file_pos, "Only grouped entities can be collapsed.",
                " Ignoring attribute.");
        collapsed.reset();
        if (large.value_or(false))
            chart->Error.Error(large.file_pos, "Only grouped entities can have large headings.",
                " Ignoring attribute.");
        large.reset();
    }

    if (our_entity == nullptr) {
        double position = chart->GetEntityMaxPos()+1;
        double position_exp = chart->GetEntityMaxPosExp()+1;
        if (rel && !chart->SkipContent()) {
            //Look up the entity in a potential 'relative' attribute
            EntityRef rel_entity = chart->AllEntities.Find_by_Name(*rel);
            if (rel_entity == nullptr) {
                if (chart->pedantic) {
                    string s = "Could not find entity '" + *rel;
                    s += "' in attribute 'relative'. Ignoring attribute.";
                    chart->Error.Error(rel.file_pos, s);
                } else {
                    std::unique_ptr<EntityApp> ed = std::make_unique<EntityApp>(rel->c_str(), chart);
                    ed->AddAttributeList(nullptr, nullptr, rel.file_pos);
                    chart->AutoGenEntities.Append(std::move(ed));
                    //position remains at the old, not incremented Entity_max_pos
                    //the definedHere of the entityDef above will be set to true in chart->PostParseProcess
                }
            } else if (rel_entity->children_names.size()) {
                //the entity in the "relative" attribute is a grouped one
                if (!pos)
                    chart->Error.Error(rel.file_pos, "Cannot put an entity exactly onto a grouped entity. Specify the 'pos' attribute.",
                                       " Ignoring attribute.");
                else if (pos==0)
                    chart->Error.Error(pos.file_pos, "Cannot put an entity exactly onto a grouped entity. Specify the 'pos' attribute.",
                                       " Ignoring positioning attributes.");
                else {
                    const EntityRef right = chart->FindLeftRightDescendant(rel_entity, *pos<0, false);
                    position     = right->pos;
                    position_exp = right->pos_exp;
                }
            } else {
                position     = rel_entity->pos;
                position_exp = rel_entity->pos_exp;
            }
        }

        //Add the effect of pos attribute to position
        if (pos) {
            if (*pos<-10 || *pos>10) {
                string msg = "Exterme value for 'pos' attribute: '";
                msg << *pos << "'. Ignoring it.";
                chart->Error.Error(pos.file_pos, msg, "Use a value between [-10..10].");
            } else {
                position     += *pos;
                position_exp += *pos;
            }
        }

        if (!chart->SkipContent()) {
            const char *style_name;
            if (ret->entities.size()) //we are group entity
                style_name = collapsed.value_or(false) ? "entitygroup_collapsed" :
                                large.value_or(false) ? "entitygroup_large" : "entitygroup";
            else
                style_name = "entity";

            //Determine a proper starting style, which is the default style and text
            //Do not add the contents of "style" (from attributes), those will be applied
            //at EntityCommand->PostParseProcess() to the running_style
            //(which is initialized with what we specify here)
            //(parse_running_style OTOH will be added "style" right now, to have the current
            //style during parsing)
            MscStyleCoW style_to_use = chart->MyCurrentContext().styles[style_name];
            style_to_use.write().text = chart->MyCurrentContext().text;                     //default text
            style_to_use.write().text +=chart->MyCurrentContext().styles[style_name].read().text;  //entity style text

            //Unset word wrapping as something not available for entity headings
            //(not implemented by EntityApp::Width() and EntityApp::Height() anyway)
            style_to_use.write().text.UnsetWordWrap();

            //If "entity" style contains no "indicator" value (the default in plain)
            //we use the value from the context (true by default)
            if (!style_to_use.read().indicator)
                style_to_use.write().indicator = chart->MyCurrentContext().indicator.value();
            //Copy shape and shape size from 'style' (where set by attributes) to 'style_to_use'
            //which contains the default shape.
            //Note that all other attributes will be reflected via the 'running_style' mechanism,
            //so we need not copy them, but this is a const member of Entity and we need it in
            //Entity::Entity in the style we pass.
            if (style.read().shape)
                style_to_use.write().shape = style.read().shape;
            if (style.read().shape_size)
                style_to_use.write().shape_size = style.read().shape_size;

            //Create a fully specified string format for potential
            //\s() \f() \c() and \mX() in label that are resolved here
            StringFormat label_format = style_to_use.read().text;
            label_format += style.read().text;
            //Create parsed label
            string orig_label = label.value_or(name);
            string proc_label = orig_label;
            StringFormat::RemovePosEscapes(orig_label);
            //Make the label a URL, if user has specified a url attribute.
            if (url.length()) {
                if (StringFormat::HasLinkEscapes(proc_label.c_str()))
                    chart->Error.Error(linenum_url_attr, "The label of the entity contains '\\L' escapes, ignoring 'url' attribute.",
                        "Use only one of the 'url' attribute and '\\L' escapes.");
                else
                    proc_label.insert(0, "\\L("+url+")").append("\\L()");
            }
            StringFormat::ExpandReferences(proc_label, chart, linenum_label_value,
                                           &label_format, false, true, StringFormat::LABEL, true);
            if (proc_label.length()==0)
                proc_label = "\\c(0,0,0,0)I"; //instead of an empty string we use an invisible 'I'.

            //Allocate new entity with correct label and children and style
            chart->AllEntities.emplace_back(name, proc_label, orig_label, position, position_exp,
                                            style_to_use, file_pos.start,
                                            collapsed.value_or(false), large.value_or(false));
            Entity &e = chart->AllEntities.back();
            e.AddChildrenList(&ret->entities, chart);  //also fixes positions & updates running_style
            e.running_draw_pass = draw_pass;
            e.parse_running_style += style; //for parsing we need running style immediately now
            if (!chart->IsVirtualEntity(e))
                ret->target = name;  //if we were a group entity, use us as target for a subsequent note
            //If we used a Shape register it (so that we can save it in OLE objects)
            if (style.read().shape && style.read().shape.value()>=0) {
                chart->used_shapes.insert(style.read().shape.value());
                if (large.value_or(false)) {
                    chart->Error.Error(large.file_pos, "Cannot make large an entity with shape. Ignoring this.");
                    large = false;
                }
            }
        }
    } else {
        FileLineCol p;
        // An existing entity. Disallow attributes that change labels or drawing positions
        std::string ll = label.value_or(std::string{});
        StringFormat::RemovePosEscapes(ll);
        if (label && ll != our_entity->label) {
            p = label.file_pos;
            string mscgen_msg;
            string plain_label = label.value_or(std::string{});
            StringFormat::ConvertToPlainText(plain_label);
            if (chart->mscgen_compat != EMscgenCompat::FORCE_MSCGEN && plain_label.length() &&
                (plain_label[0]=='>' || plain_label[0]==':')) {
                mscgen_msg = "This may be an mscgen arrow symbol (':";
                mscgen_msg.push_back(plain_label[0]);
                mscgen_msg.append("'). Use '");
                if (plain_label[0]==':')
                    mscgen_msg.append("==>' and the attribute [arrow.type=none] instead.");
                else
                    mscgen_msg.append("=>' or '==>' instead.");
            }
            chart->Error.Error(p, "Cannot change the label of an entity after declaration. Keeping old label: '"
                             + our_entity->orig_label + "'.", mscgen_msg);
        }

        if (pos) {
            p = pos.file_pos;
            chart->Error.Error(p, "Cannot change the position of an entity after declaration. Ignoring attribute 'pos'.");
        }
        if (rel) {
            p = rel.file_pos;
            chart->Error.Error(p, "Cannot change the position of an entity after declaration. Ignoring attribute 'relative'.");
        }
        if (collapsed) {
            p = collapsed.file_pos;
            chart->Error.Error(p, "You can only declare an entity collapsed at its definition. Ignoring attribute 'collapsed'.");
        }
        if (large) {
            p = large.file_pos;
            chart->Error.Error(p, "You can only declare an entity large at its definition. Ignoring attribute 'large'.");
        }
        //if any of the above errors, add extra info
        if (p.file!=-1)
            chart->Error.Error(our_entity->file_pos, p, "Entity '" + name + "' was defined here.");
        label.reset(); pos.reset(); rel.reset(); collapsed.reset();

        //Also indicate an error at any attempt to add children (even if "ch" did not
        //contain actual entities, it signals we had a "{ ... }" block
        if (ch) {
            chart->Error.Error(l, "You can specify an entity group only when first defining an entity. "
		                      "Ignoring grouping and placing entities just after.");
            chart->Error.Error(file_pos.start, l, "Entity '" + name + "' was defined here.");
        }

        //update running style for parsing
        our_entity->parse_running_style += style;

        //set us as the target of a potential subsequent note or comment
        if (!chart->IsVirtualEntity(our_entity))
            ret->target = name;  //if we were a group entity, use us as target for a subsequent note
    }

    //Prepend this entity to the list (list is empty if no children)
    ret->entities.Prepend(this);
    return ret;
}


/** Add the attribute names we take to `csh`.*/
void EntityApp::AttributeNames(Csh &csh)
{
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "large",
        "For group entities, make its heading span from top all the way down to the end of the chart.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "color",
        "Set the text and line color.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "label",
        "Set the visible text of the entity.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "show",
        "Control if the entity is shown or is invisible.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "collapsed",
        "You can collapse group entities to a single one with this attribute.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "pos",
        "You can specify the position of the entity with this attribute at declaration.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "relative",
        "Use this attribute to indicate which entity is the reference for the value specified in the 'pos' attribute.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "active",
        "Turn this on to activate the entity.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "url",
        "Turn the whole label to a link targeting e.g., an URL or an element documented by Doxygen (when using Doxygen integration). "
        "Use the '\\L' formatting escape if you want to use only part of a label as a link.",
        EHintType::ATTR_NAME));
    csh.AttributeNamesForStyle("entity");
    MscElement::AttributeNames(csh);
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool EntityApp::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr,"color")) {
        csh.AddColorValuesToHints(false);
        return true;
    }
    if (CaseInsensitiveEqual(attr, "label") || CaseInsensitiveEqual(attr, "url")) {
        return true;
    }
    if (CaseInsensitiveEqual(attr,"show") || CaseInsensitiveEqual(attr,"collapsed")
        || CaseInsensitiveEqual(attr,"active")|| CaseInsensitiveEqual(attr, "large")) {
        csh.AddYesNoToHints();
        return true;
    }
    if (CaseInsensitiveEqual(attr,"pos")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "This number will be added to the x coordinate of the last entity declared or to that of the entity given by the 'relative' attribute. "
            "It is measured in units of entity distance (130 pixels times the value of the 'hscale' chart option), defaulting to 1.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEqual(attr, "relative")) {
        csh.AddEntitiesToHints();
        return true;
    }
    if (CaseInsensitiveEqual(attr, "shape.size")) {
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"auto",
            "Sets the size of the entity as large as needed to accommodate the entity label. "
            "Valid only for shapes with a label placeholder.",
            EHintType::ATTR_VALUE, true));
       //continue adding regular shape.size hints below
    }
    if (MscElement::AttributeValues(attr, csh)) return true;
    if (csh.AttributeValuesForStyle(attr, "entity")) return true;
    return false;
}

/** Merges EntityApp objects referring to the same entity.
 * Used if the same entity is mentioned twice on the same list
 * or in a following list.
 * This function moves notes of `ed` to us,
 * combines show, active & style members.*/
void EntityApp::Combine(EntityApp *ed)
{
    if (ed->show) show = ed->show;
    if (ed->active) active = ed->active;
    style += ed->style;
    CombineComments(ed);
 }

/** Returns how wide the entity is with this formatting, not including its shadow.*/
double EntityApp::Width() const
{
    if (entity->shape>=0) {
        //Calculate outer_edge to be around x=0 and positioned at y=0
        outer_edge.y.from = 0;
        double w;
        if (entity->shape_size==EArrowSize::INVALID) {
            //This is the 'shape_size=auto' setting
            _ASSERT(chart->Shapes[entity->shape].GetLabelPos().IsInvalid()==false);
            const XY label_place_size = chart->Shapes[entity->shape].GetLabelPos().Spans();
            const XY label_size = parsed_label.getTextWidthHeight();
            const double scale = std::max(label_size.x/label_place_size.x, label_size.y/label_place_size.y);
            const XY size = chart->Shapes[entity->shape].GetMax().Spans() * scale;
            outer_edge.y.till = size.y;
            w = size.x;
        } else {
            outer_edge.y.till = chart->entityShapeHeight * ArrowScale(entity->shape_size);
            const XY wh = chart->Shapes[entity->shape].GetMax().Spans();
            w = wh.x/wh.y*outer_edge.y.till;
        }
        outer_edge.x.from = -w/2;
        outer_edge.x.till = w/2;
        if (chart->Shapes[entity->shape].GetLabelPos(outer_edge).IsInvalid())
            return std::max(w, parsed_label.getTextWidthHeight().x);
        else
            return w;
    }
    //Entity labels cannot be word wrapped
    double inner = parsed_label.getTextWidthHeight().x;
    if (entity->children_names.size() && style.read().indicator.value() && entity->collapsed)
        inner = std::max(inner, GetIndicatorSize().x + 2*chart->boxVGapInside);
    const double width = ceil(style.read().line.LineWidth()*2 + inner);
    return width + fmod_negative_safe(width, 2.); //always return an even number
}

/** Lay out the entity head at y position zero.
 * We fill in `indicator_ypos_offset`, `outer_edge` and `area`, `area_draw`
 * and `area_important`. We add ourselves to the list of elements
 * which should not be covered by notes. In short this is a mini version of
 * the ArcBase::Layout() function called from EntityCommand::Layout().
 * Called only if the EntityApp displays a header or if it is a non-compressed
 * large group entity.
 * Must not be called twice.
 * @param [out] cover Returns our cover and mainline.
 * @param [in] children A list of our children showing here, already laid out.
 * @returns The y range we occupy. Can `from` be negative if we are a group entity,
 *          since non-group children entities will be laid out to y==0. */
Range EntityApp::Height(Area &cover, const NPtrList<EntityApp> &children)
{
    indicator_ypos_offset = -1;
    const double x = chart->XCoord(entity->pos); //integer
    if (entity->shape>=0) {
        _ASSERT(children.size()==0);
        //Here outer_edge is calculated to be around x=0 and positioned at y=0
        outer_edge.x.Shift(x);
        area = chart->Shapes.Cover(entity->shape, outer_edge);
        if (chart->Shapes[entity->shape].GetLabelPos(outer_edge).IsInvalid()) {
            const double w = parsed_label.getTextWidthHeight().x;
            area += parsed_label.Cover(chart->Shapes, x-w/2, x+w/2, outer_edge.y.till);
        }
        area.arc = this;
        area_important = area;
        cover = area;
        cover.mainline = Block(chart->GetDrawing().x, area.GetBoundingBox().y);
        return area.GetBoundingBox().y;
    }
    const XY wh = draw_heading ? parsed_label.getTextWidthHeight() : XY(0,0);
    const double lw = style.read().line.LineWidth();
    if (children.size()==0 && !entity->large) {
        if (entity->children_names.size() && style.read().indicator.value())
            indicator_ypos_offset = wh.y + lw + chart->boxVGapInside;
        const double width = Width();
        const double indicator_height = (indicator_ypos_offset > 0) ? GetIndicatorSize().y + 2*chart->boxVGapInside : 0;
        const double height = ceil(chart->headingVGapAbove + wh.y + indicator_height + 2*lw + chart->headingVGapBelow);

        //do not include shadow in anything... but the returned height (uses for non-compressed placement)
        outer_edge = Block(x-ceil(width/2), x+ceil(width/2),
                           chart->headingVGapAbove, height - chart->headingVGapBelow);

        area = style.read().line.CreateRectangle_OuterEdge(outer_edge.CreateExpand(-lw/2));
        area_draw.clear();
        draw_is_different = false;
        area_draw_is_frame = false;
    } else {
        //We execute here for non-collapsed group entities even if they are large
        //In the latter case draw_heading may be false (then wh==(0,0))
        if (entity->large) {
            //in case a large entity, we have to take the same left and right offset everywhere
            //we could not do it in Width, when left_offset and right_offset is done,
            //since then we did not have Entity::maxwidth of our children set to its final value
            outer_edge.x.from = chart->XCoord(left_ent) - (left_ent->maxwidth/2 + chart->boxVGapInside + style.read().line.LineWidth());
            outer_edge.x.till = chart->XCoord(right_ent) + (right_ent->maxwidth/2 + chart->boxVGapInside + style.read().line.LineWidth());
        } else {
            outer_edge.x.from = chart->XCoord(left_ent) - left_offset;
            outer_edge.x.till = chart->XCoord(right_ent) + right_offset;
        }
        double top = 0, bottom = 0;
        for (auto i = children.begin(); i!=children.end(); i++) {
            top =    std::min(top,    (*i)->outer_edge.y.from);
            bottom = std::max(bottom, (*i)->outer_edge.y.till + (*i)->style.read().shadow.offset.value());
        }
        outer_edge.y.from = top - chart->headingVGapAbove - ceil(wh.y + lw);
        outer_edge.y.till = bottom + chart->headingVGapBelow + lw;

        area = style.read().line.CreateRectangle_OuterEdge(outer_edge.CreateExpand(-lw/2));
        area_draw = area.CreateExpand(chart->trackFrameWidth) - area;
        draw_is_different = true;
        area_draw_is_frame = true;
    }
    area.arc = this;
    //Add shadow to outer_edge and place that to cover
    Area my_cover(Block(outer_edge).Shift(XY(style.read().shadow.offset.value(), style.read().shadow.offset.value())) += outer_edge, this);
    my_cover.mainline = Block(chart->GetDrawing().x, outer_edge.y);
    cover = std::move(my_cover);
    const Block b = outer_edge.CreateExpand(-lw/2);
    area_important = parsed_label.Cover(chart->Shapes, b.x.from, b.x.till, b.y.from + lw/2);
    if (children.size())
        area_draw += area_important;
    chart->NoteBlockers.Append(this);
    return Range(outer_edge.y.from, outer_edge.y.till + style.read().shadow.offset.value());
}

/** Add a small block blocking notes for EntityApp objects displaying no heading.
 * This function is called iff the EntityApp shows no header.*/
void EntityApp::AddAreaImportantWhenNotShowing()
{
    //we do not draw this, but nevertheless define a small block here
    //if we are hidden, find someone who shows and has a valid "pos"
    const CEntityRef e = chart->FindWhoIsShowingInsteadOf(entity, false);
    //"e" may be equal to "entity" if we are not hidden
    const double xpos = chart->XCoord(e);
    const double w2 = style.read().line.LineWidth()/2;
    area_important = Block(xpos - w2, xpos + w2, -chart->compressGap/2, +chart->compressGap/2);
    area_to_note = area_important;
    chart->NoteBlockers.Append(this);
}

/** Record y-position dependent status of the Entity.
 * Called after the EntityApp is ShiftBy()ed to its final position.
 * We hide entity lines behind us, record our style and status
 * in Entity::status and if we are a group entity we
 * create a control (for the GUI).
 * If `centerline_target` is not nullptr, we do this at the
 * centerline of it, not at yPos.*/
void EntityApp::PostPosProcess(Canvas &canvas, Chart *ch)
{
    //If we have a centerline target, we must not draw a heading
    _ASSERT((centerline_target==nullptr) || !draw_heading);
    if (draw_heading) {
        chart->HideEntityLines(outer_edge);
        if (entity->children_names.size())
            MscElement::controls.push_back(entity->collapsed ? EGUIControlType::EXPAND : EGUIControlType::COLLAPSE);
    }
    const double yUse = centerline_target  && centerline_target->GetCenterlineAtXPos(chart->XCoord(entity))>=0 ?
        centerline_target->GetCenterlineAtXPos(chart->XCoord(entity)) + centerline_target->GetFormalPos() :
        draw_heading ?
            yPos + chart->headingVGapAbove :
            yPos;
    const EEntityStatus old_status = entity->status.GetStatus(yUse);
    EEntityStatus new_status = old_status;
    if (show) new_status.Show(*show);
    if (active) new_status.Activate(*active);
    if (!(new_status == old_status))
        entity->status.SetStatus(yUse, new_status);
    entity->status.ApplyStyle(yUse, style);

    //Hide entity lines of a label that is placed below a Shape
    if (entity->shape >= 0 && draw_heading) {
        const XY twh = parsed_label.getTextWidthHeight();
        const Block b = chart->Shapes[entity->shape].GetLabelPos(outer_edge);
        if (b.IsInvalid()) {
            const double x = outer_edge.x.MidPoint();
            chart->HideEntityLines(parsed_label.Cover(chart->Shapes, x-twh.x/2, x+twh.x/2, outer_edge.y.till));
        }
    }
    MscElement::PostPosProcess(canvas, ch);
}

void EntityApp::RegisterLabels()
{
    //Register the label
    if (entity->shape >= 0) {
        const XY twh = parsed_label.getTextWidthHeight();
        const Block b = chart->Shapes[entity->shape].GetLabelPos(outer_edge);
        if (b.IsInvalid()) {
            const double x = outer_edge.x.MidPoint();
            chart->RegisterLabel(parsed_label, LabelInfo::ENTITY,
                x-twh.x/2, x+twh.x/2, outer_edge.y.till);
        } else {
            const double r = std::min(1., std::min(b.x.Spans()/twh.x, b.y.Spans()/twh.y));
            const double yoff = (b.y.Spans() - twh.y*r)/2;
            chart->RegisterLabel(parsed_label, LabelInfo::ENTITY,
                b.x.from, b.x.till, b.y.from + yoff);
        }
    } else {
        const double lw = style.read().line.LineWidth();
        Block b2(outer_edge);
        b2.Expand(-lw/2);
        chart->RegisterLabel(parsed_label, LabelInfo::ENTITY,
            b2.x.from, b2.x.till, b2.y.from + lw/2);
    }
}

void EntityApp::CollectIsMapElements(Canvas &canvas)
{
    //Register the label
    if (entity->shape >= 0) {
        const XY twh = parsed_label.getTextWidthHeight();
        const Block b = chart->Shapes[entity->shape].GetLabelPos(outer_edge);
        if (b.IsInvalid()) {
            const double x = outer_edge.x.MidPoint();
            parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
                x-twh.x/2, x+twh.x/2, outer_edge.y.till);
        } else {
            const double r = std::min(1., std::min(b.x.Spans()/twh.x, b.y.Spans()/twh.y));
            const double yoff = (b.y.Spans() - twh.y*r)/2;
            parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
                b.x.from, b.x.till, b.y.from + yoff);
        }
    } else {
        const double lw = style.read().line.LineWidth();
        Block b2(outer_edge);
        b2.Expand(-lw/2);
        parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            b2.x.from, b2.x.till, b2.y.from + lw/2);
    }
}


/** Draw an entity heading
 * We use the layout calculated in Height() and affected by ShiftBy()
 * and we use the style we finalized in EntityCommand::PostParseProcess()*/
void EntityApp::Draw(Canvas &canvas)
{
    Block b(outer_edge);
    const bool large = entity->large && !entity->collapsed;
    const auto compatible = [](const MscStyleCoW& a, const MscStyleCoW& b) {
        if (a.is_same_as(b)) return true;
        return a.read().f_line && b.read().f_line
            && a.read().f_fill && b.read().f_fill
            && a.read().f_shadow && b.read().f_shadow
            && a.read().line==b.read().line
            && a.read().fill==b.read().fill
            && a.read().shadow==b.read().shadow;
    };
    if (large) {
        //expand to the bottom of our current status
        do {
            b.y.till = std::min({ entity->status.ShowTill(b.y.till), entity->status.StyleTill(b.y.till), chart->GetTotal().y.till });
        } while (b.y.till < chart->GetTotal().y.till
                 && entity->status.GetStatus(b.y.till).IsOn()
                 && compatible(style, entity->status.GetStyle(b.y.till)));
    }
    canvas.Entity(b, parsed_label, style.read(), chart->Shapes, entity->shape, !entity->children_names.empty());

    //Draw indicator
    if (indicator_ypos_offset > 0)
        DrawIndicator(XY(outer_edge.Centroid().x, outer_edge.y.from + indicator_ypos_offset),
        &canvas);

    //For large keep drawing all the span of 'on'-ness
    if (!large) return;
    while (b.y.till < chart->GetTotal().y.till
           && entity->status.GetStatus(b.y.till).IsOn()) {
        b.y.from = b.y.till;
        MscStyleCoW local_style = entity->status.GetStyle(b.y.till);
        do {
            b.y.till = std::min({ entity->status.ShowTill(b.y.till), entity->status.StyleTill(b.y.till), chart->GetTotal().y.till });
        } while (b.y.till < chart->GetTotal().y.till
                 && entity->status.GetStatus(b.y.till).IsOn()
                 && compatible(local_style, entity->status.GetStyle(b.y.till)));
        canvas.Entity(b, {}, local_style.read(), chart->Shapes);
    }
}

